Skip to content

v0.17.0

Latest

Choose a tag to compare

@TBarregren TBarregren released this 23 Jul 10:19

This release makes /orchestrate observable and interrupt-safe: a new /orchestrate status board, a continuous mid-run milestone heartbeat, and a durable cross-session restart guard — alongside a batch of orchestrate integrity fixes (landing verification, marker attribution, and test-fixture git isolation).

Added

  • /orchestrate now emits a continuous, out-of-band per-issue milestone heartbeat over a multi-hour run (#48, ADR-0006). Between #49's terminal landed/opened-PR marker there was no within-ticket signal: an issue churning in fix round 3 for two hours was indistinguishable from steady progress, and a parked/failed issue left no durable marker at all — and the only in-flight view was the in-session progress tree, the exact place peeking has repeatedly killed the background run. Now a mechanical-tier reporter sub-agent posts a durable, machine-readable milestone comment on the issue at each lifecycle boundary the engine reaches — orchestrate: started #<n>, run <runId>, implementation green, verification cleared, fix round <k> (only when one runs), and parked #<n> (<reason>) when the issue dies — extending #49's single-source-of-truth grammar (format_started_marker / format_implementation_green_marker / format_verification_cleared_marker / format_fix_round_marker / format_parked_marker and the widened Marker verb domain + parse_landed_marker in scripts/orchestrate.py). The reporter is the second authorized outward writer, but strictly weaker than integrate: comment-only, never close/push/merge (it carries the same shared prohibition the workers do). started is posted only for an issue the preflight decided to dispatch — never for one skipped as already-landed / already-open / stale; the heartbeat is emitted in both merge and PR modes; reporting is best-effort (a failed or absent reporter never blocks, parks, or fails an issue); and each reporter dispatch is awaited so an issue's comments post in lifecycle order (which #50's status board reads back). A milestone verb carries an issue number, never a SHA, so it never parses as a landed marker — #49's four preflight verdicts are unchanged and no false already-landed skip occurs. skills/orchestrate/orchestrate.workflow.js gains the reporter agent, REPORTER_SCHEMA, the best-effort report wrapper, the milestone templates, and the wave-loop / buildAndVerify boundary dispatches; skills/orchestrate/SKILL.md documents the reporter lifecycle step, the milestone vocabulary, the two-authorized-writers operating-contract bullet, and out-of-band progress watching (the issue comment timeline / GitHub notifications, never the TUI); ADR-0006 records the decision and amends ADR-0005 §3 / ADR-0001 §7 by reference. tests/test_orchestrate*.py cover every milestone verb's round-trip, malformed-comment rejection, the no-milestone-parses-as-landed non-regression, the extended SKILL↔parser consistency binding, and the reporter engine structure, red-first. The parked reason is now sanitised at format time in both mirrored sources (sanitize_parked_reason in scripts/orchestrate.py, sanitizeReason in the engine): collapsed to one line, reduced to an allowlist — which drops : so a reason can never reconstruct an orchestrate: prefix and smuggle a second marker that would parse back as a landed verb (a false already-landed skip), drops () so it cannot break the single-line grammar #50 reads, and drops quotes/backticks/$ so it cannot inject into the reporter's gh command — and truncated to a short cap so no unbounded verifier detail is disclosed durably on a public issue. Every per-issue park now emits a durable parked heartbeat, not only the ones inside buildAndVerify: the prerequisite-cascade park (a blocked dependent) and the loud landed-marker-stale park both post one, so a maintainer watching the timeline never sees a prerequisite park followed by silence about the issues it cascades to; the token-budget park (dispatching a reporter after the budget floor is breached would contradict it) and the run-level integration-review / hotfix parks (no per-issue number to post on) stay silent by design.
  • /orchestrate is now interrupt-safe across a dead cross-session resume (#49, ADR-0005). A run killed mid-flight and blindly relaunched used to re-run the whole plan from scratch — Workflow resumeFromRunId is same-session-only, so a cross-session relaunch gets zero cache hits — re-implementing already-landed issues on fresh branches and landing redundant commits. Three mechanisms close this. (1) Durable landed-markers: at integrate time the integrate step posts a machine-readable comment on the issue — canonical orchestrate: landed <sha> on <branch>, run <runId> in merge mode, orchestrate: opened PR #<pr>, run <runId> in PR mode — with one source of truth (format_landed_marker / parse_landed_marker and the Marker dataclass in scripts/orchestrate.py), designed to extend to #48's milestone vocabulary. (2) Close-at-integrate (Decision D1): a merge-mode issue is now closed by the integrate step at integrate time — the moment it has both cleared independent verification and landed — not by the orchestrator at finalize; PR mode leaves the issue open. This is the one narrow exception to the sub-agents' ban on outward writes (the integrate step alone may post the marker and close), forced by the orchestrator being blocked on the single Workflow call mid-run and the engine having no I/O of its own. (3) Preflight idempotence guard: before dispatching an implementer, a cheap mechanical preflight sub-agent gathers each issue's durable state (gh issue view --comments, git merge-base --is-ancestor, any orchestrate PR's state), and a pure, unit-tested decision function (preflightDecision in lib/orchestrate/engine-helpers.mjs, mirrored byte-identically inline in skills/orchestrate/orchestrate.workflow.js) maps the facts to one of four verdicts — already-landed (skip, no implementer, marked landed so dependents proceed), landed-marker-stale (park loudly for a human, never rebuild from zero), already-open (benign PR-mode skip), or dispatch. So even a naive relaunch scoped to the same issues re-implements nothing that landed. skills/orchestrate/SKILL.md gains a Restarting a stopped run section stating Workflow resume is same-session-only and prescribing the cross-session restart (re-plan the remainder, launch fresh, lean on the guard — never blind-resume), corrects the old "resumable via its runId" advice, documents the preflight lifecycle step and the durable-marker + close-at-integrate behaviour, and carries the narrow-exception operating-contract bullet. scripts/orchestrate.py's report gains the already-landed / already-open (rendered as "already complete") and landed-marker-stale (rendered as a blocker) statuses. ADR-0005 records the decision and amends ADR-0001 §5/§7 by reference; the mandatory final integration review handles a cross-issue defect at run level and never reopens an already-closed issue. tests/test_orchestrate.py, tests/test_orchestrate_skill.py, and tests/test_orchestrate_workflow.py cover the marker round-trip and rejection, the four preflight verdicts (through node), the integrate marker+close structure gated to a real issue, the restart protocol, the SKILL↔parser consistency binding, and ADR-0005's existence and cross-reference.
  • /orchestrate status — a deterministic per-issue run board you can watch from outside the run (#50). The durable milestone and landed comments #48/#49 post are the only out-of-band progress signal, but reading them meant scrolling each issue's timeline by hand. A new status subcommand sits beside plan/redgreen/report in scripts/orchestrate.py as a pure stdin→stdout renderer — no network I/O in the Python — that folds those comments into one board, a row per issue in one of four states: queued, working (with the current phase), done (with the landed SHA), or parked (with the reason). All marker reading goes through the single-source-of-truth parse_landed_marker, so the board and the preflight guard never diverge on what a comment means. The board is scoped to a single run — the latest by default, --run <id> to pin an earlier one — so a restarted run's fresh markers don't blur with an older run's. skills/orchestrate/SKILL.md documents the gh issue list … | status one-liner and its watch variant (both driven with --state all so a landed-and-closed issue still renders a done row) and the first-page comment cap the reader honours. tests/test_orchestrate_status.py and tests/test_orchestrate_skill.py cover the four states, run scoping and the latest-run default, the queued fallback, and the SKILL↔renderer consistency binding, red-first.

Fixed

  • /orchestrate plan now reads dependency edges from an issue's Agent Brief comment, not only its body (#51). The comment above HARD_EDGE_KEYWORDS already promised the planner reads triage's inline **Depends on:** #N labels, but load_issues fed only entry["body"] to parse_dependencies — and triage posts the Agent Brief as a comment, so a hard dependency written only there was invisible and a coupled set collapsed into one unsafe wave (the #50#48 reproduction planned [[48, 50]] instead of [[48], [50]]). load_issues now runs the same parse_dependencies discipline over the body and every Agent Brief comment — selected by the same AGENT_BRIEF_HEADING_RE heading anchor _has_agent_brief uses, so only genuine briefs qualify — and unions the results (_union_signals), the body's edge provenance winning on a shared number. Non-brief comments are never scanned: triage notes, milestone markers, and grammar-quoting discussion comments are full of other issues' numbers, and scanning them would fabricate edges (the same false-positive class #47 closed). Soft phrases, self-references, and (Related: #N) asides inside a brief follow the existing clause-boundary discipline unchanged — no hard edge — and the code comment above HARD_EDGE_KEYWORDS is corrected to name both sources. No new keywords and no brief-specific grammar: one shared parser, two sources, a union of results. _has_agent_brief now derives from the new _agent_brief_texts helper, giving one authoritative definition of "an Agent Brief comment." tests/test_orchestrate.py covers the #50/#48 reproduction planning as [[48], [50]], the non-brief-comment exclusion, the soft-phrase / self-reference / (Related: #N) cases inside a brief, the body∪brief union with body-wins provenance, a bare-string brief comment, and the unresolved-label warning reaching brief text — red-first.
  • /orchestrate status no longer lets a quoted marker poison another issue's board row (#54). #48's milestone grammar deliberately carries the issue number inside each marker (orchestrate: started #47, run <runId>) so attribution is verifiable, but #50's status consumer never checked it: load_status_universe attributed every orchestrate: marker in an issue's comments to that issue, so any comment that merely quotes the grammar — most sharply #48's own Agent Brief, which fences the templates in code — rendered a bogus row (a live #48 working started scoped to the placeholder run <runId>, demonstrated by the integration review that found this). parse_landed_marker is intentionally prose-tolerant, which was safe only because its prior consumer, preflight, has a second factor (landed SHAs must be ancestors of the default tip); status has none. A new _marker_attributable guard in the consumer/attribution layer restores that second factor without touching the single-source-of-truth parser: a milestone marker whose embedded number differs from the hosting issue is another issue's marker quoted here and is dropped, and a run_id still wearing its <placeholder> angle brackets (_PLACEHOLDER_RUN_ID_RE) is an unsubstituted template example — never a run a reporter posted — so it is dropped too, which also keeps a phantom <runId> run from becoming the default scope in _resolve_run and stops a quoted opened PR #12, run <runId> (number-less, so unreachable by number attribution) from marking its host done through the any-run terminal→done fallback. tests/test_orchestrate_status.py covers the #48-brief quoted payload, number attribution under a fully-substituted run id, and the placeholder terminal case, red-first; all existing status tests stay green.
  • /orchestrate's integrate step now structurally verifies a merge-mode landing reached disk, instead of trusting the agent not to strand it (#53). The #48 × #50 hotfix added a prompt-level guard forbidding the integrator from forcing a landing with git update-ref / git branch -f / git push . or by flipping core.bare, but that was advisory instruction to a mechanical-tier agent, with no positive post-condition check that the land actually reached disk and no defence when the repo was already corrupt at integrate time — a ref-only advance still looked "landed" while the working tree stayed at the pre-run commit, so the gates and the mandatory final re-run silently tested stale code. Now the integrator reports the raw post-land facts in INTEGRATE_SCHEMAbareRepo (git rev-parse --is-bare-repository), defaultCheckedOut, headSha (git rev-parse HEAD on the default after the fast-forward), featureSha (the feature-branch tip), and worktreeClean (git status --porcelain empty) — and a pure, unit-tested decision helper (landStrandedBlocker in lib/orchestrate/engine-helpers.mjs, mirrored byte-identically inline in skills/orchestrate/orchestrate.workflow.js) decides whether the land is sound: a bare repo or a default not checked out in the integrating worktree is refused before any sha comparison, and a missing sha, a HEAD that did not advance to the feature tip (a ref-only advance), or a dirty tree is a failed land. The engine — not the agent's own integrated flag — parks the issue with a specific blocker whenever those facts do not prove the tree advanced to the feature tip (merge mode only; PR mode lands nothing on the default), so a run can never record "landed" over stranded code. The merge-mode integrate prompt now instructs the integrator to gather and report those facts, and skills/orchestrate/SKILL.md documents the engine-side check. tests/test_orchestrate_workflow.py covers the schema fields, the merge-gated engine call and its park, the prompt requirement, and landStrandedBlocker's verdicts through node — red-first; the existing drift guard binds the two mirrored copies.
  • /orchestrate status no longer regresses a landed-and-closed issue to queued on a restarted run (#48 × #50 integration). The board defaults its scope to the newest run, but a cross-session restart (#49) re-runs the whole plan, so an issue that landed-and-closed in an earlier run carries only that run's markers. Scoping to the newest run rendered every such issue queued — silently defeating #50's own --state all amendment (which exists precisely so the board can show a done row) and wrongly answering the board's headline "was it done?". build_status in scripts/orchestrate.py now falls back, when an issue has no marker in the scoped run, to its latest terminal done marker from any run (landed / opened-pr, named by the new STATUS_TERMINAL_VERBS); a non-terminal earlier-run marker still does not carry over, so an issue merely started in an old run and untouched in this one stays queued. tests/test_orchestrate_status.py covers the restart universe red-first.
  • /orchestrate's integrate step is now barred from corrupting the repository to force a landing (#48 × #50 integration). A run fast-forwarded the default branch while it was checked out in the primary worktree and set core.bare = true to bypass git's checked-out-branch refusal, leaving the repo (bare), the default's working tree stranded at its pre-run commit (the landed code never reached disk), and no landed markers posted. The merge-mode integrate prompt in skills/orchestrate/orchestrate.workflow.js (mirrored in skills/orchestrate/SKILL.md) now requires the git merge --ff-only be run from the one worktree that actually holds the default branch — so ref and working tree advance together — and explicitly forbids setting core.bare, reconfiguring the repository, or force-advancing the default with git update-ref / git branch -f / git push ., parking a blocker instead when the default is not checked out where the agent runs. tests/test_orchestrate_workflow.py binds the guard structurally, red-first.
  • The doctor/init test fixtures no longer leak the git environment and corrupt the real repository under the pre-commit hook (#52). Git hooks export GIT_DIR (and, for index-touching hooks, GIT_INDEX_FILE / GIT_WORK_TREE) into everything they run, and those outrank a fixture's -C <tmpdir> targeting — so when the pre-commit tests hook launched pytest, the git_project fixture's own git init / add / commit ran against the developer's real .git, corrupting its index and (via a stray git init honouring the exported GIT_DIR) flipping core.bare = true; this was the upstream trigger of the integrate-time corruption fixed above (#48 × #50), and it had already forced --no-verify commits twice in one orchestrate run. A new tests/conftest.py autouse fixture now monkeypatch.delenvs GIT_DIR, GIT_INDEX_FILE, GIT_WORK_TREE, GIT_OBJECT_DIRECTORY, and GIT_COMMON_DIR from the process environment for every test, so every git subprocess beneath pytest — the fixtures' own and the ones scripts/doctor.py / scripts/init.py spawn when driven against temp projects — inherits a clean environment; the fixtures keep their -C <dir> targeting for ergonomics. A decoy-repo regression test in tests/test_doctor.py locks the isolation in: it seeds a throwaway repo, snapshots its .git/config and index bytes, exports the leak at the decoy exactly as the hook would, drives both a fixture-backed test and a doctor.main invocation (its own git) in a child pytest, and asserts the decoy is byte-for-byte unchanged — red before the scrub existed, green after. As defence-in-depth the tests hook entry in .pre-commit-config.yaml now scrubs the same variables with env -u … before pytest starts, so even a future test that bypasses conftest.py stays isolated. pre-commit run --all-files now completes with the working repo's index and config intact — no --no-verify needed.

Full changelog: https://github.com/Kntnt/kntnt-code-skills/blob/v0.17.0/CHANGELOG.md