feat(specify): record in-context fork resolutions as auditable issue comments

The `spec-skeptic` `scope-fork` juror reads only the seeding issue plus
the spec. On the legitimate `specify` direct-entry path — a fork settled
in a long in-context design discussion — that resolution lives only in
ephemeral chat the juror cannot replay. When the issue body lags the
discussion (still lists the fork open), the juror correctly blocks, and
a design BLOCK escalates without self-correction. The result: auto-sign
was structurally almost unreachable for the in-context entry path.

Close the blind spot by giving the juror an auditable source instead of
weakening the gate. When `specify` enters in-context and a tracker issue
still lists a now-resolved fork as open, the orchestrator posts a
reconciliation comment recording each fork's resolution WITH provenance
(a record of the user's decision, never a fresh orchestrator one) before
writing the spec. The comment is persistent and audit-able — unlike a
carrier digest — so it, not the orchestrator's confidence, is what the
juror checks.

Separation of powers keeps it honest: the orchestrator writes the
comment, the adversarial juror enforces the provenance requirement. A
bare `decision: X` with no provenance does not resolve the fork — the
re-dispatched juror blocks on it. The escalation rule and the
three-field carrier are untouched; only the juror's information changes.

Mechanics:
- specify Step 1.5: reconciliation-comment sub-step, provenance format,
  issue-less fallback (auto-sign -> human sign-off, no weak spec-note).
- spec-skeptic: replace the "quoted in the dispatch" drift; juror reads
  the issue WITH comments via `issue_tracker.show_cmd`; provenance check.
- new profile slot `issue_tracker.show_cmd` (must render comments);
  documented in schema + template.
- issue skill: `tea issues <idx>` is body-only; `--comments` required
  (verified against tea 0.14.1 and Aura #55 — 180 vs 144 lines).
- consistency: design.md out-of-scope, README, pipeline.md, boss skill.
This commit is contained in:
2026-06-12 15:54:47 +02:00
parent a87916e578
commit 268ee705f4
9 changed files with 101 additions and 14 deletions
+6 -1
View File
@@ -158,7 +158,12 @@ not settled), an `INFRA_ERROR`, any objective gate not green, or an
exhausted budget falls back to the human sign-off pause the spec would
have hit anyway. Re-dispatching all five lenses every round is the
backstop that stops an editorial repair from quietly settling a design
question. On a clean sign the orchestrator commits the spec
question. When a fork was settled in-context but the seeding issue still
lists it open, `specify` records the resolution as a provenance-bearing
reconciliation comment on the issue (Step 1.5), which the `scope-fork`
juror reads via `issue_tracker.show_cmd` — closing the blind spot where
the in-context entry path could otherwise never clear that lens. On a
clean sign the orchestrator commits the spec
(`(boss-signed)` in the subject), sends a mandatory informational notify
that names the signed capability and invites a veto, and continues to
`planner` without stopping. A later veto is a forward correction, never
+6
View File
@@ -369,6 +369,12 @@ and defined there. The boss-side contract is just this:
question in the user's place; the full re-panel each round is what
stops an editorial repair from laundering one in. The gate owns this
loop (`../specify/SKILL.md` Step 6) — boss only enforces the notify.
One upstream detail matters here: when a fork was settled in-context
but the seeding issue still lists it open, `specify` Step 1.5 posts a
provenance-bearing reconciliation comment on the issue so the
`scope-fork` juror ratifies the resolution from an auditable source
rather than false-blocking on the stale body. That comment records a
*user* decision; it never lets the orchestrator certify its own.
- **On a clean sign, do not stop.** Commit the spec (the auto-sign
commit subject carries `(boss-signed)` so the act is auditable in
history), fire the auto-sign notify, and continue to `planner` in
+7 -5
View File
@@ -113,8 +113,10 @@ The relationship:
## What's out of scope for this plugin
- **Issue-tracker integration**: the plugin can read the
profile's `issue_tracker.kind` and `issue_tracker.list_cmd`
and invoke the configured listing command, but does not
directly call Gitea / GitHub / Linear APIs. The `boss` skill
uses the configured shell command for queue reads, so the
plugin remains transport-agnostic.
profile's `issue_tracker.kind`, `issue_tracker.list_cmd`, and
`issue_tracker.show_cmd` and invoke the configured listing /
single-issue commands, and `specify` may post a reconciliation
comment via the tracker's comment command — but the plugin does
not directly call Gitea / GitHub / Linear APIs. Every read and
write goes through the configured shell command, so the plugin
remains transport-agnostic.
+5 -1
View File
@@ -113,7 +113,11 @@ signs. A `BLOCK` is never signed over: an editorial one (`criterion` /
`ambiguity` / `plan-readiness`) is repaired in a bounded ≤ 2-round loop
that re-runs the objective gates and re-dispatches all five lenses each
round; a design one (`scope-fork` / `grounding`), an `INFRA_ERROR`, or
an exhausted budget falls back to the human sign-off pause. See
an exhausted budget falls back to the human sign-off pause. When the
entry is in-context and the seeding issue still lists a now-resolved
fork as open, `specify` posts a provenance-bearing reconciliation
comment on the issue (Step 1.5) so the `scope-fork` juror can ratify the
resolution instead of blocking on the stale body. See
`../specify/SKILL.md` Step 6 and `../boss/SKILL.md` §"Spec auto-sign".
### planner
+1
View File
@@ -173,6 +173,7 @@ authoritative semantics live at the `paths` row above.
| `issue_tracker.close_marker` | string | `"closes #N"` | Marker the orchestrator includes in commit bodies to auto-close issues. |
| `issue_tracker.url` | string | (empty) | Human-browsable URL of the issue list — surfaced in notifications and cross-references. |
| `issue_tracker.list_cmd` | string | (empty) | Shell command that lists open issues. Used by the `boss` skill to read the forward queue. Examples: `tea issues ls --repo X/Y --state open`, `gh issue list --repo X/Y --state open`. |
| `issue_tracker.show_cmd` | string | (empty) | Shell command that renders **one** issue **with its comment thread**; the orchestrator and the `spec-skeptic` `scope-fork` juror append the issue index as the final argument (e.g. `tea issues --comments``tea issues --comments 55`). MUST include comments — a `specify` reconciliation comment (see `specify/SKILL.md` Step 1.5) is invisible to the juror otherwise. Examples: `tea issues --comments`, `gh issue view --comments`. If empty, the juror reads only the issue body and a fork resolved in-context but not echoed into the body cannot be ratified — auto-sign falls back to the human sign-off. |
| `protected_branches` | list | `[main]` | Branches that are sacrosanct in the same sense as main. |
## `notifications`
+5 -2
View File
@@ -94,7 +94,7 @@ when running outside the repo (the slug lives in
| Operation | Command |
|---|---|
| List open | `tea issues ls --state open` |
| View one | `tea issues <idx>` |
| View one | `tea issues <idx>` (body only) — add `--comments` to include the discussion thread: `tea issues --comments <idx>` |
| Create | `tea issues create -t "<title>" -d "<body>" -L "<labels>"` |
| Edit body/title | `tea issues edit <idx> -t "<title>" -d "<body>"` |
| Re-label | `tea issues edit <idx> --add-labels a --remove-labels b` |
@@ -111,7 +111,10 @@ Notes:
`in-progress`).
- Before editing, view the issue first (`tea issues <idx>`) — do not
edit blind. Status/discussion updates go in a `tea comment`, not by
overwriting the body.
overwriting the body. Note `tea issues <idx>` prints the **body
only**; pass `--comments` to read the existing thread first (the bare
form would otherwise prompt for it interactively, which hangs a
non-interactive agent).
- Routine closing is done by a `closes #N` line in the commit body on
push (per `~/dev/CLAUDE.md`); `tea issues close` is for closing
without an accompanying commit.
+48
View File
@@ -121,6 +121,54 @@ the reactive-deference failure the gate exists to prevent.
Genuine doubt about whether the sources resolve a decision *is itself*
the fork signal — it resolves to a bounce, not to a guess.
**Reconciliation comment (lagging-issue case).** A load-bearing fork can
be resolved by the in-context design discussion yet still be listed
*open* by the tracker issue that seeded the cycle — the issue lags the
conversation. When that holds (entry is in-context **and** a tracker
issue still carries a now-resolved fork as open), record the resolution
as an auditable issue comment **before** writing the spec, via the
project's issue-tracker comment command (`tea comment <idx>` for Gitea;
a heredoc body per the `issue` skill's conventions). One comment, this
exact shape:
```
## Design reconciliation (specify, in-context entry)
Spec: <spec-name>. The fork(s) below are still listed open on this
issue; they were resolved in the in-context design discussion.
- **Fork: <name>** → <decision>.
Provenance: <verbatim user statement, or explicit attribution + date>.
```
The comment must carry **provenance**, and the distinction is
load-bearing: it is the *record of a user decision* (legitimate — the
user settled the fork in discussion, you are minuting it), never a *new
orchestrator decision* dressed as settled (illegitimate). A bare
`decision: B` with no provenance is the latter. If you cannot point to a
real user statement, that is the signal that *you* picked the fork, not
the user — which is a Step-1.5 bounce, not a comment. The comment is
*persistent and auditable* (author + timestamp, in the forward queue) —
which is exactly why it, and not your ephemeral confidence, is what a
downstream `scope-fork` juror reads to confirm the fork is settled (see
Step 6). Recording it keeps the forward queue honest in either run mode;
under auto-sign it is *load-bearing* — without it the `scope-fork` juror
sees only the stale issue body and (correctly) blocks.
Provenance enforcement is not yours to self-certify: you *write* the
comment, the adversarial `scope-fork` juror *checks* it carries
provenance (Step 6). Writing a provenance-less comment does not buy a
pass — the re-dispatched juror blocks on it.
Gitea writes are pre-authorized (per `~/.claude/CLAUDE.md`); post the
comment without a confirmation prompt.
**Issue-less in-context entry.** If the cycle has *no* tracker issue —
the design was settled in pure chat — there is no independent artefact to
comment on, and a provenance note *inside the spec* would be
self-referential (the spec asserting its own sources settle it). Do not
manufacture a weak spec-note path: under auto-sign this case falls back
to the human sign-off (the safe default), because no auditable source
exists for the juror to check against.
### Step 2 — Apply the project's feature-acceptance criterion
The project's `CLAUDE.md` declares the criterion a feature must
+21 -5
View File
@@ -72,10 +72,22 @@ In addition, every dispatch:
`paths.code_roots` and the project's examples / fixtures directory —
you grep these to confirm or refute ratification.
- For the `scope-fork` lens additionally: the sources the spec was
built from, when the carrier names them (an issue body is reachable
via the issue-tracker show command; an in-context design narrative is
quoted in the dispatch). You judge "resolved vs picked" against what
the sources actually say, not against what reads plausibly.
built from. The seeding issue is reachable via the profile's
`issue_tracker.show_cmd` (issue index appended last) — invoke it so
you read the issue **with its comment thread**, not the body alone.
You judge "resolved vs picked" against what the sources actually say,
not against what reads plausibly. A fork the issue *body* still lists
open may have been resolved in an in-context discussion the dispatch
cannot replay to you; the legitimate channel for that resolution is a
**reconciliation comment** on the issue (`specify` Step 1.5). A
reconciliation comment counts as the sources resolving the fork **only
if it carries provenance** — a record of the user's decision (a
verbatim statement, or explicit attribution + date). A bare
`decision: X` with no provenance is an orchestrator self-assertion
dressed as settled, not a source: it does **not** resolve the fork, and
a load-bearing decision whose only support is such a comment is a
refutation. If `show_cmd` is unset, you have only the body — a fork the
body lists open is unresolved to you.
You do NOT read files under `paths.plan_dir` (the plan does not yet
exist). You do NOT read other specs unless the spec under review
@@ -117,7 +129,11 @@ YOU DO NOT RUN THE FULL TEST SUITE. (TEST LIST, TYPE-CHECK, PER-BLOCK PARSER RUN
`#[ignore]`/`xfail`/disabled does not pin; code presence does not
pin; your own recall does not pin). For `scope-fork`, this means
listing the load-bearing decisions and checking each against the
sources. For the others, read for the specific failure.
sources read via `show_cmd` (issue **with comments**); a fork the
issue body lists open is resolved only by a reconciliation comment
that carries provenance (see the standing reading list) — a
provenance-less one does not count. For the others, read for the
specific failure.
4. If you find a genuine defect along your lens → `BLOCK`, and state
it concretely (section ref, the offending text, why it is a defect).
5. If, after an honest search, you cannot find one → `SOUND`. Saying
+2
View File
@@ -62,6 +62,8 @@ git:
close_marker: "closes #N"
# url: "" # human-browsable issue list URL
# list_cmd: "" # e.g. "tea issues ls --repo X/Y --state open"
# show_cmd: "" # renders one issue WITH comments; index appended last.
# e.g. "tea issues --comments" -> "tea issues --comments 55"
notifications:
# command: "" # e.g. "~/.claude/notify.sh"; boss falls back to chat if empty