Filing from an agent session that scoped and ran a full analysis end to end. This cost only a couple of minutes, but it cost them at the very first command, and the cause is a mismatch between the human and JSON surfaces rather than a bug.
What happened
First command in a fresh directory:
$ lc init --json
{
"converged": false,
"created": ["astra.yaml", "pyproject.toml", ".python-version", ".gitignore",
".git", "git-annex", "annex-filter", ".gitattributes",
".datalad/config", "data/", "data/README.md", "results/",
"results/README.md", "myst.yml", "index.md", "uv.lock", ".venv"],
"repaired": [], "unchanged": [], "blocked": [], "warnings": []
}
converged: false, blocked: [], warnings: []. Nothing says whether that is a failure, a partial state, or normal. So I re-ran it. Still not obviously fine. Ran a third time, got converged: true with 17 unchanged, and concluded it was fine all along.
Root cause — and it's deliberate
ConvergenceReport.converged is a pre-state, and the docstring says so, explicitly anticipating this confusion:
https://github.com/LightconeResearch/lightcone-cli/blob/main/src/lightcone/engine/project.py#L52-L60
@property
def converged(self) -> bool:
"""Whether the project needed nothing done to it.
Note the tense: after a write run that created files this is
``False``. It reports what convergence *found*, not whether the
project is now good.
"""
return not self.created and not self.repaired and not self.blocked
So converged: false after a successful lc init is correct and intended.
The human surface already handles this properly. _render_init_output() distinguishes the cases and prints the reassuring thing:
else:
verdict = f"[green]✓[/green] Project converged at {where}"
A human sees ✓ Project converged at /path. A JSON consumer sees "converged": false — first key in the object — and has no equivalent field to read.
Why this bites agents specifically
The lightcone skill tells agents to drive from JSON:
Ask for --json. Every verb that reports takes --json, and that is the form to drive from […] Read JSON; show the user the plain output when they want to look at something themselves.
So the guidance routes agents onto precisely the surface where the reassurance was dropped. Following the skill correctly is what produced the confusion.
Suggested fix
Add an explicit post-state to as_dict(), so the JSON says what the terminal says:
def as_dict(self) -> dict[str, object]:
return {
"ok": not self.blocked, # did this run leave the project good?
"converged": self.converged, # was it already good when we looked?
**asdict(self),
}
Alternatives, if adding a field is unwelcome:
- Rename to
already_converged. The tense is then in the name, and no consumer can read it as a post-condition. Breaking change, but the semantics become self-documenting.
- Emit the verdict string the human path computes, as
"verdict": "Project converged at /path".
Either of the first two would have saved the two redundant runs. I'd favour ok alongside the existing field — additive, and it gives --check consumers something unambiguous to gate on.
Smaller adjacent note
The docstring's "Note the tense" is doing real work and is worth promoting into lc init --help, since that's where someone looks when the JSON surprises them.
Filing from an agent session that scoped and ran a full analysis end to end. This cost only a couple of minutes, but it cost them at the very first command, and the cause is a mismatch between the human and JSON surfaces rather than a bug.
What happened
First command in a fresh directory:
converged: false,blocked: [],warnings: []. Nothing says whether that is a failure, a partial state, or normal. So I re-ran it. Still not obviously fine. Ran a third time, gotconverged: truewith 17unchanged, and concluded it was fine all along.Root cause — and it's deliberate
ConvergenceReport.convergedis a pre-state, and the docstring says so, explicitly anticipating this confusion:https://github.com/LightconeResearch/lightcone-cli/blob/main/src/lightcone/engine/project.py#L52-L60
So
converged: falseafter a successfullc initis correct and intended.The human surface already handles this properly.
_render_init_output()distinguishes the cases and prints the reassuring thing:A human sees
✓ Project converged at /path. A JSON consumer sees"converged": false— first key in the object — and has no equivalent field to read.Why this bites agents specifically
The lightcone skill tells agents to drive from JSON:
So the guidance routes agents onto precisely the surface where the reassurance was dropped. Following the skill correctly is what produced the confusion.
Suggested fix
Add an explicit post-state to
as_dict(), so the JSON says what the terminal says:Alternatives, if adding a field is unwelcome:
already_converged. The tense is then in the name, and no consumer can read it as a post-condition. Breaking change, but the semantics become self-documenting."verdict": "Project converged at /path".Either of the first two would have saved the two redundant runs. I'd favour
okalongside the existing field — additive, and it gives--checkconsumers something unambiguous to gate on.Smaller adjacent note
The docstring's "Note the tense" is doing real work and is worth promoting into
lc init --help, since that's where someone looks when the JSON surprises them.