Polling a feature done-signal in an agent loop
Gate an agent loop on one feature’s completion – not the whole project’s –
using ctxgrd status --lineage and --exit-code.
Background
ctxgrd pools documents by type: all ADRs share docs/adrs/, all SPECs share
docs/specs/, and so on. A repo routinely holds several in-flight features at
once. Running ctxgrd status --exit-code on the whole project asks “is
anything in the entire repo stuck?” – which is almost never the right
question for an agent loop driving a single feature.
--lineage <ID> changes the scope. It follows the depends_on graph in
reverse: every document that transitively depends on <ID>, plus <ID>
itself. That set is one feature’s lineage. Combined with --exit-code, it
turns “is anything in this feature stuck?” into a plain exit code – no
text-scraping needed. stdout stays clean for piping; the exit code carries
the decision.
Step 1 – Identify the lineage root
Pick the document at the root of your feature’s depends_on graph. For a
feature driven by a PRD, that is usually the PRD:
ctxgrd status --lineage PRD-3 --exit-codeA document depended on by more than one lineage root is shared. status
names the other root rather than silently folding the document into just one
lineage: shared with: <ID> in text mode, a top-level "shared": ["<ID>"]
array in JSON. The field is present only when the lineage actually shares a
document with another root. A shared document’s other parents do not need to
be done for your lineage’s exit code to be 0.
Step 2 – Understand what --exit-code checks
ctxgrd status --lineage PRD-3 --exit-code
echo "exit=$?"| Code | Meaning |
|---|---|
0 | No document in scope is blocked by a non-terminal dependency – nothing is stuck. |
1 | At least one document in scope is itself non-terminal and depends on a non-terminal document. |
2 | Config error or dependency cycle – abort. |
Read 0 carefully – it does not mean “every document reached a terminal
status.” A document can sit at a non-terminal status (draft, open,
in_progress) with nothing holding it back. That document is ready, not
blocked, and it does not trip the exit code. --exit-code answers “is
anything stuck on an unresolved dependency,” not “has the whole feature
landed.” If your lineage’s last step still needs someone to flip its own
status to terminal, exit 0 will not tell you that happened – check the
document’s status directly in the JSON output (Step 4).
A terminal document that still depends on a non-terminal one is not “stuck” either – it is finished, and finished work does not trip the signal. The report still names the edge instead of dropping it:
settled on open work:
ADR-001 ← ADR-002
(enable core.dep-status to gate this)Whether that edge is itself a defect is a separate, opt-in question: enable
core.dep-status on the namespace if a finished document depending on open
work should fail your lint, not your done-signal.
The report body is still printed to stdout regardless of which exit code is
returned; no file is modified.
Step 3 – Wire the poll loop
The canonical shell loop for an agent driving PRD-3:
while true; do
ctxgrd status --lineage PRD-3 --exit-code
code=$?
if [ "$code" -eq 0 ]; then
echo "Nothing stuck -- stopping loop."
break
elif [ "$code" -eq 2 ]; then
echo "Config or cycle error -- aborting." >&2
exit 2
fi
# code is 1: something in scope is stuck on a non-terminal dependency;
# do the next unit of work, then poll again
sleep 30
doneExit 0 stops the loop. Exit 1 means something is stuck; the agent does the
next unit of work and polls again. Exit 2 is a hard failure the agent
cannot recover from by itself.
Step 4 – Route on the JSON output for finer decisions
When the loop needs more than a binary branch – for example, to see exactly which document is the current blocker – request structured output:
ctxgrd status --lineage PRD-3 --format jsonKey fields:
documents– every document in the lineage. Each entry carriesid,namespace,status,ready(true when the document is itself non-terminal and nothing blocks it), andblocked_by(the non-terminal dependencies holding it back; empty once they land).shared– present only when this lineage shares a document with another lineage root; names the other root’s id(s) (ADR-059 § LIN-005).
stdout is a clean JSON stream; pipe it directly to jq. Find whatever is
currently blocking the lineage:
ctxgrd status --lineage PRD-3 --format json | jq '.documents[] | select(.blocked_by != [])'{
"id": "TASK-1",
"namespace": "TASK",
"status": "open",
"ready": false,
"blocked_by": [
"SPEC-9"
]
}Step 5 – Fold Definition-of-Done checkboxes into the check (optional)
core.acceptance-complete is an opt-in builtin rule that fires on any
document at a terminal status that still has an unchecked - [ ] item under
its acceptance heading(s). It is a lint diagnostic – ctxgrd lint reports
it – and is independent of status --exit-code: a document flagged by this
rule can still show up as ready or settled in status. Run both checks if
unchecked boxes should also hold up your loop.
Enable it per namespace in ctxgrd.toml:
[TASK."core.acceptance-complete"]
headings = ["Acceptance", "Definition of Done"]
terminal = ["done"]headings defaults to ["Acceptance", "Definition of Done"]. terminal
defaults to the shared terminal-status set (accepted, superseded, done,
fixed, wontfix, invalid, duplicate, closed, implemented,
consumed, n/a); supply a narrower list to restrict the rule to specific
statuses.
The rule scans only the configured heading window(s). An open checkbox under
Out of scope or Open Questions does not fire – those sections are deferred
work, not unmet criteria.
Example. A TASK at status: done with this body:
## Acceptance
- [x] Endpoint returns 200 for valid input
- [ ] Rate-limit header present in response ← fires core.acceptance-complete
## Open Questions
- [ ] Should we log 429s to Datadog? ← does not fireThe open box under Acceptance holds up the rule. After the box is checked
and ctxgrd is re-run, the diagnostic clears.
Cross-references
- SPEC-003 (
docs/specs/003-status-done-gate-and-per-lineage-scope.md) – original requirements for the done-signal and lineage scope. - ADR-056 –
--exit-codeandcore.acceptance-completedesign. - ADR-059 –
--lineagegraph-scope design, including shared-node disclosure. - ADR-118 – removed the namespace stage layer; readiness and
--exit-codeare computed from thedepends_ongraph directly. - ADR-106 –
core.dep-status, the opt-in rule for a terminal document depending on open work.