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 the entire repo done?” – 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 gives a process exit that answers “is this feature done?” without text-scraping – satisfying ctxgrd’s machine-readability contract (stdout clean for piping, exit code for branching).

Exit codes

Code Meaning
0 Selected frontier is empty and there are no blockers – feature is done.
1 Frontier is non-empty or a blocker is present – keep working.
2 Config error or dependency cycle – abort.

The report body is still printed to stdout regardless of which exit code is returned; no file is modified.

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-code

Shared documents – depended on by more than one lineage root – are included in each lineage’s count and marked in the output. A shared document’s other parents do not need to be done for your lineage’s verdict to be 0.

Step 2 – 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 "Feature done -- stopping loop."
    break
  elif [ "$code" -eq 2 ]; then
    echo "Config or cycle error -- aborting." >&2
    exit 2
  fi
  # code is 1: feature still in progress; do the next unit of work, then poll again
  sleep 30
done

Exit 0 stops the loop. Exit 1 means there is still work in the frontier; 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 3 – Route on the JSON output for finer decisions

When the loop needs more than a binary branch – for example, to know which namespace is the current bottleneck or which documents are in the frontier – request structured output:

ctxgrd status --lineage PRD-3 --format json

Key fields:

  • frontier – the namespaces that currently have unresolved documents.
  • next_action – a short human-readable hint (identical to the tip: line in text mode).
  • stages[].state – per-namespace state: done, current, pending, or blocked.
  • stages[].verdict – the gate verdict for the stage (e.g. accepted, or empty when no documents are counted).
  • stages[].shared – documents in this stage that belong to more than one lineage.

stdout is a clean JSON stream; pipe it directly to jq:

ctxgrd status --lineage PRD-3 --format json | jq '.frontier'

Step 4 – Fold Definition-of-Done checkboxes into the gate (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). When it fires, the document’s stage remains blocked – and --exit-code stays 1 – until every box is checked.

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, done, fixed, implemented, closed, superseded, wontfix, invalid, duplicate, 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 fire

The open box under Acceptance holds the stage. After the box is checked and ctxgrd is re-run, the diagnostic clears and the gate can advance.

Cross-references

  • SPEC-003 (docs/specs/003-status-done-gate-and-per-lineage-scope.md) – full requirements for the done-gate and lineage scope.
  • ADR-056 – --exit-code and core.acceptance-complete design.
  • ADR-059 – --lineage graph-scope design.