@deftai/directive-content 0.109.0 → 0.110.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: deft-directive-triage
3
3
  description: >-
4
- Triage-cache hygiene and "what's next?" queue selection -- the agent-facing
5
- playbook for syncing the triage cache, classifying candidates, presenting a
6
- ranked queue, walking per-item decisions, and auditing the session.
4
+ Withdrawn (#4070). Classify playbook is off. Work selection is
5
+ plan-sequence:current then a read-only triage:queue listing. Replacement
6
+ sieve is #4071. Triggers still match so packs:slice finds this stub.
7
7
  ---
8
8
 
9
9
  Read and follow: skills/deft-directive-triage/SKILL.md
package/Taskfile.yml CHANGED
@@ -992,12 +992,19 @@ tasks:
992
992
  CLI_ARGS: "{{.CLI_ARGS}}"
993
993
 
994
994
  triage:classify:
995
- desc: "Inspect / validate auto-classification + Tier-1 SCM label mirror (#1129 / #1423). -- task triage:classify -- [--list | --validate | --mirror [--apply]]"
995
+ desc: "Inspect / validate auto-classification. --mirror is withdrawn (#4070). -- task triage:classify -- [--list | --validate]"
996
996
  cmds:
997
997
  - task: triage-classify:classify
998
998
  vars:
999
999
  CLI_ARGS: "{{.CLI_ARGS}}"
1000
1000
 
1001
+ triage:strip-withdrawn-chips:
1002
+ desc: "Remaining-set strip of withdrawn triaged / triage:* chips (#4070). -- task triage:strip-withdrawn-chips -- [--apply] [--emit-digest] [--json] [--repo OWNER/NAME]"
1003
+ cmds:
1004
+ - task: triage-classify:strip-withdrawn-chips
1005
+ vars:
1006
+ CLI_ARGS: "{{.CLI_ARGS}}"
1007
+
1001
1008
  triage:summary:
1002
1009
  desc: "Emit the one-line triage state for the session-start ritual (D2 / #1122). Always exits 0; appends a JSONL record to <lifecycle-root>/.triage-cache/summary-history.jsonl. -- task triage:summary -- [--json] [--no-history]"
1003
1010
  cmds:
package/commands.md CHANGED
@@ -309,7 +309,7 @@ Current status: the validation, extractor, provider, registry, generated MAP, an
309
309
  - `task verify:orphan-active -- --changed-only [--base-ref <ref>]` -- the merge-chokepoint form composed into `check:framework-source` / `check:consumer` (#3893). It evaluates only the active/running briefs the candidate's own diff touches, so a brief stranded by another merge cannot fail an unrelated PR and N stranded briefs no longer make N single-brief lifecycle PRs mutually unmergeable. Base ref defaults to `origin/<deliveryBranch>`. It falls back to the full repo-wide sweep -- never to a narrower scan -- when HEAD is on the delivery line (the delivery-tip check) or when git or the base ref is unresolvable, and every run prints which scope it used. Mutually exclusive with `--issue N` (exit 2). The bare verb stays repo-wide for `deft doctor`, manual runs, and swarm finalize.
310
310
  - `task verify:pr-closeout-attestable -- --pr N` -- fail closed when merging PR `N` would close an issue whose brief is still `running` in `xbrief/active/` with acceptance criteria carrying neither `x-directive/evidence` nor `x-directive/disposition` (#3781). The trigger is the PR's structured closing references, **not** the branch diff: CI runs before the merge and the issue closes on it, so a diff-keyed gate can never fail the PR that creates the orphan, and the brief need not be in the diff at all. Reuses `evaluateAcceptanceEvidenceGate`, the same rule `scope:complete` enforces. The refusal names each unattested criterion and the exact shape it needs, narrowed to the evidence kinds that criterion's axis allows. A PR that leaves an unattested brief **without** closing its issue is unaffected. Reads the working tree at `--project-root`, which at merge time is the PR head checkout. Matching is repository-qualified and case-insensitive on the owner/repo slug: closing references are scoped to the PR's repository, so a same-numbered issue tracked by a brief in another repository cannot block the merge, a mixed-case slug for this repository still matches, and a bare tracking number reads as this repository. Three-state exit (0 attestable or closes nothing / 1 unattested closeout / 2 config or lookup error) -- an unresolved closing-reference lookup, or an unresolvable OWNER/REPO, is 2, never a pass. `task pr:wait-mergeable-and-merge` runs it as the last gate before the merge call.
311
311
  - `task verify:lifecycle-visible` -- warn when a clone's ignore configuration hides `xbrief/` / `vbrief/` lifecycle roots (#3505). Uses `git check-ignore -v` on the stage dirs, a matching-extension sentinel under each, and bounded probes derived from ignore-rule globs in root, exclude, `core.excludesFile`, and nested lifecycle `.gitignore` files (so `2026-06-*.xbrief.json` / `2025-*.xbrief.json` cannot report clean), plus `git ls-files -v` for skip-worktree / assume-unchanged. Names the matching rule and source file. Warn-first from `session:start` (per-clone, not on `task check`). Selective `.triage-cache/*.jsonl` entries do not trip. Pass `--enforce` to fail closed.
312
- - `task verify:completed-write-guard` -- fail closed when a newly added `xbrief/completed/` (or `vbrief/completed/`) artifact was not written by `scope:complete` / `scope:fail` (#3679). Artifacts larger than 1 MiB fail closed before read. Historical corpus is advisory via doctor. Remediation names the leftover land PR after a scope-provenance strip (#3476). `scope:complete` can stamp a brief already in `completed/`. Does not change `verify:completed-tracked`.
312
+ - `task verify:completed-write-guard` -- fail closed when a newly added `xbrief/completed/` (or `vbrief/completed/`) artifact was not written by `scope:complete` / `scope:fail` (#3679), or when a change set deletes or renames away an `xbrief/active/` brief with no paired stamped destination (#3766). Same name-status parser covers A, R, and D. Terminal moves pass as `R active->completed` or `D active` plus `A completed` when the destination has a runTransition stamp. A `cancelled/` destination pairs only when it carries a `lifecycleWrite` cancel stamp. Delete-plus-add pairing also requires the dest `plan.title` to match the deleted source so a copied stamp cannot authorize an unrelated brief. Lone-D untracking cleanup halts and names `scope:complete` or leaving the brief untracked. Artifacts larger than 1 MiB fail closed before read. Historical corpus is advisory via doctor. Remediation names the leftover land PR after a scope-provenance strip (#3476). `scope:complete` can stamp a brief already in `completed/`. Does not change `verify:completed-tracked`.
313
313
  - `task verify:completed-tracked` -- fail closed when closed scoped issues lack a tracked `xbrief/completed/` or `xbrief/cancelled/` artifact on the delivery tip (#3264 / #3476); remediate with `task swarm:finalize-cohort` or a lifecycle PR. `task verify:completed-tracked -- --issue N` is the drive-to DONE form (delivery tip `origin/<deliveryBranch>`, not feature HEAD). Standalone verb (not part of `task check`); use `--tip HEAD` when validating an in-flight land branch. An unresolvable delivery tip fails closed (no silent HEAD fallback) -- fetch the delivery branch or pass an explicit `--tip`. Under `--skip-gh`, a named `--issue` with no cached state fails closed; the unscoped corpus scan keeps the offline allowance. Lifecycle-only lands (completed/cancelled xBRIEFs + optional CHANGELOG) use that verb plus finalize-cohort or the lifecycle PR. ⊗ Full `task check` / the TypeScript suite. ⊗ The drive-to story envelope (pre-pr + review-cycle + suite) for a file-copy land.
314
314
  - `task verify:ac` -- product-first acceptance gate (#3284). Runs `plan.acceptance.commands` (or #3267 literal ledger) **verbatim** before done; records AC-source rung (`stated`/`derived`/`project_floor`). Empty commands require `none_stated: true`. Empty resolution is not a green run when the project has no suite floor (`soft_empty` + stamp-acceptance remedy, #3334). Primary name used first in `task check` (fail-fast); `--soft-missing-xbrief` for check composition. Rapid ceremony = AC-only; pressure/degraded makes hygiene advisory. `--capture-only` lists resolved commands without executing. Extends #3267 / #973.
315
315
  - `task verify:literal-ac` -- #3267 mechanism alias for verbatim stated-command run (same flags/cwd); prefer `verify:ac` for product-first done-gate.
@@ -366,6 +366,8 @@ When the workflow needs an Approach 1 monitor, scope the Cursor leaf `stop-at: p
366
366
  - **Readiness model (#3100):** reports registration → command functionality → host trust → interception coverage as four separate states. Structural registration fails first; only then does the live check invoke the installed `deft-hook` shim with allow/deny fixtures for enabled Claude, Grok, Cursor, and Codex codecs. Missing/drifted registration, unavailable shim, timeout, empty/invalid required output, or a wrong decision envelope exits non-zero. The probe does not prove host interception. Full contract and latency budget: [contracts/agent-hook-readiness.md](./contracts/agent-hook-readiness.md).
367
367
  - **Post-deposit report (#3100):** `directive init` and `deft update` run readiness after writing hook deposits. A red post-check returns non-zero but does not roll back the completed deposit; JSON distinguishes `deposit_completed` from `agent_hook_readiness.ready`.
368
368
  - Repair missing/drifted entries: `deft update`.
369
+ - **Host tool-surface coverage (#3987):** `--scope=agent` also fails closed when a supported host's audited mutation tool names are absent from the **deposited** matchers, when the runtime classifier does not place a catalogued name in the same group, or when a host has no audit entry at all — so a new host or a renamed tool cannot silently drop out of coverage. Coverage or a written out-of-scope reason per tool name, never silence. Record: [docs/host-tool-surface-audit.md](./docs/host-tool-surface-audit.md).
370
+ - **Owner liveness on non-write activity (#3987):** a matched PreToolUse call renews the occupant's lease when the host payload resolves the owner, so a session whose work is shell-shaped does not starve the lease it holds. Bounded: host-authoritative owner only (never an ambient `DEFT_SESSION_ID`), owner-only, `markWrite` false, keyed on the lease's own worktree, and `claimed_at` untouched so `OCCUPANCY_MAX_LEASE_MS` still caps the lease. Discoverable counterpart for long quiet work stays `deft occupancy:heartbeat`.
369
371
  - **Refresh and opt-out (#2790, #2752, #3571):** Upgrade `@deftai/directive`, then run `deft update` to refresh all four deposits to the fast path; do not hand-edit host hook files. To disable a host's Tier-1 enforcement, run `deft policy:disable-host-hooks --host <host> --confirm` — it prints a capability-cost disclosure (guardrails removed; result is tracked) and is not a timeout or live-probe fix. When a host is opted out, `deft update` / `directive init` skip creating or re-merging Directive-managed hook entries for that host; leftover-free files write `{}`. Inspect with `deft policy:show --field=hostHooks`. Doctor and `verify:hooks-installed --scope=agent` treat opted-out hosts as healthy. Hand-edit of `plan.policy.hostHooks` plus `deft update` still strips (human high-trust bypass).
370
372
  - **Claude matcher scope:** Once `.claude/settings.json` hooks are loaded, Claude's `PreToolUse` matcher keys on tool names (`Edit`, `Write`, …), not target paths — matched tools can be gated for the whole session, including writes outside the project tree. Opt out of Claude hook deposit when that posture is unwanted.
371
373
  - **Compact re-arm + soft AGENTS re-bind (#2113 / #2992 / #2993 / #3171 / #2769):** post-compact posture is **two surfaces**:
@@ -412,13 +414,18 @@ After CLI/deposit upgrade, disk can show the new generation while a long-lived s
412
414
  - ! **USER.md path (#2544):** resolve via `deft session:start` output (`USER.md resolved …`); default platform paths: Windows `%APPDATA%\deft\USER.md`, Unix `~/.config/deft/USER.md`; override `$DEFT_USER_PATH`; workspace `<project>/.deft/USER.md`. ⊗ Invent or search `~/.config/deft` on Windows — AppData Roaming is canonical.
413
415
  - ! At mutation boundaries (code-writing, scope lifecycle moves, `start_agent`, commits, pushes, PR-from-local-changes, release work): run the mutable quick tier then gated verifier below before proceeding.
414
416
  - ? Explicit read-only alignment only: `deft session:start -- --read-only` (no ritual-state write).
415
- - ! **Worktree occupancy contract (#3433 / #3604 / #3611):** mutation `session:start` / `session:ready` claim a gitignored `.deft/occupancy.json` lease. Occupancy is cooperative host-session routing between processes that follow the protocol, not authentication against another same-user process: hook JSON and local owner IDs are forgeable. Name it for what it is (#3755): a **cooperative bearer-id boundary, not a lineage** — the lease admits whoever presents an id the record itself names, nothing observes parentage, and a dispatched child is admitted only because a grant records it. A live lease allows only a matching presented owner or member and a ritual state whose exact verified owner agrees; missing, conflicting, or mismatched identity fails closed. `--read-only` does not claim.
417
+ - ! **Worktree occupancy contract (#3433 / #3604 / #3611 / #3926):** mutation `session:start` / `session:ready` claim a gitignored `.deft/occupancy.json` lease. Occupancy is cooperative host-session routing between processes that follow the protocol, not authentication against another same-user process: hook JSON and local owner IDs are forgeable. Name it for what it is (#3755): a **cooperative bearer-id boundary, not a lineage** — the lease admits whoever presents an id the record itself names, nothing observes parentage, and a dispatched child is admitted only because a grant records it. A live lease allows only a matching presented owner or member and a ritual state whose exact verified owner agrees; missing, conflicting, or mismatched identity fails closed. `--read-only` does not claim. A stored lease whose `worktree_path` is not this checkout is residue, not a holder: first mutation `session:start` claims over it. Same-tree two-session conflict still fails closed.
418
+ - ! **Which tree the lease is read from — root admission (#3794 / #4013):** a gated write is judged against the Git working tree its **target** lands in, not always the tree the host payload names. A target in another worktree sharing `--git-common-dir` is admitted and carries occupancy, ritual and active scope with it; a proven different repository, an unreadable target identity, and a mutation spanning two worktrees are refused. A target with **no Git toplevel at all** (OS temp, a home file, anything outside every checkout) is gated against the payload root — deliberate since #3794, published with its reason and a gate-by-gate disposition in [docs/hook-root-admission.md](./docs/hook-root-admission.md), which also records the three-surface matrix (direct write consults admission; generic server-prefixed MCP and out-of-repo Shell writes do not) and the relative-target canonicalization limitation (#4023).
416
419
  - ! **Host owner namespaces (#3611):** canonical owners are `host:<provider>:v1:<base64url(raw-id)>`. Codex uses payload `session_id` for the parent and its subagents. Claude Code uses `session_id`; `agent_id` does not replace the session-family owner. Cursor uses `conversation_id` and requires a simultaneously supplied `session_id` to agree; no Cursor subagent-granularity claim is made until verified. Grok has no verified payload identity, so its owner is the hook process variable `GROK_SESSION_ID` the host publishes (#3873): the hook is a sibling the host spawns, so an agent shell export cannot reach it, and the payload `session_id` is never read. When that variable is absent the host keeps explicit `--session-id` / `DEFT_SESSION_ID` ownership; a malformed one fails closed, as does an ambient `DEFT_SESSION_ID` that contradicts a resolved host owner.
417
- - ! **Lifecycle identity transport (#3611):** for every host with a resolved owner, PreToolUse rewrites only exact, simple canonical `deft` / `directive` lifecycle commands and source-repo `task <verb> [-- ...]` forms to add the host's `--session-id`. Direct CLI spellings are `session:start`, `session:ready`, `session:end`, `occupancy:steal`, `occupancy:release`, `occupancy:heartbeat`, and `swarm-launch`; the Task spelling for the last verb is `swarm:launch`. For owner-requiring lifecycle commands, the shell execution directory must be absent or realpath-align exactly with the hook project root. Path-bearing executables (`./deft`, `/path/deft`), path/destination flags (`--project-root`, `--paths`, path-valued `--stories`, `--output`, `--worktree-map`, `--gate-clearances`), `--no-audit`, consumer-repo Task indirection, Task aliases (`task deft:<verb>`), and compound, redirected, quoted, aliased, wrapped, or otherwise ambiguous forms are not auto-rewritten. A supported-host hook rejects a recognized lifecycle form outside the narrow rewrite surface until it carries the explicit matching `--session-id`; an execution-root mismatch on an owner-requiring command always denies. With a matching ID on other excluded forms, the original command remains under normal host permission handling. Manual/integrated-terminal callers must likewise pass one matching `--session-id` (or `DEFT_SESSION_ID`) through the complete lifecycle.
420
+ - ! **Occupancy identity on a host with no session derivation (#3877):** the hook process environment (`GROK_SESSION_ID` on Grok, or `DEFT_SESSION_ID` inherited by the hook) or an explicit `--session-id`. a worker-shell `export DEFT_SESSION_ID`. A shell export reaches vitest, CLI verbs, and `scope:complete`; it never reaches `deft-hook`, a sibling process. Nothing in `content/templates` or `content/skills` mentions the variable. Do not invent an envelope mandate.
421
+ - ! **One actor-resolution chain, four terminals (#3954):** claim, release, heartbeat and grant/revoke resolve the acting session by the same lookup order — explicit `--session-id`, then `DEFT_SESSION_ID`, then the owner the running host published. Only the **terminal** differs, and deliberately: `session:start` / `session:ready` **mint**, because claiming establishes an identity where none exists; `occupancy:release`, `occupancy:heartbeat`, `occupancy:grant` and `--revoke` resolve to **empty** and keep the "this process presented no session identity" diagnosis, because they prove an identity rather than establish one. A shared mint would hand those surfaces a plausible id no later hook will present. The practical effect: on a host that publishes an owner, the printed recovery `occupancy:release` is a command the occupant can actually run, instead of one that resolved an empty caller and refused the owner its own lease. Disagreement is reported, not reordered: when `DEFT_SESSION_ID` and the host owner name different sessions the documented order still stands, and a refusal names both ids and the `--session-id` that would act as the host owner. Anonymous release of the occupant recorded in the lease file stays **refused** — possession of that path is not authority to delete a live lease.
422
+ - ! **Where the transport still does not reach (#3873 residue):** the CLI resolves the host owner; the write gate runs in the `deft-hook` sibling process, and on a deployment where the host does not publish `GROK_SESSION_ID` into that sibling the hook presents whatever `DEFT_SESSION_ID` it inherited — possibly another host's session. Nothing in this tree can set that process's environment, and the hook writes no env or payload log, so the condition is inferred from the deny text rather than observed. Recovery meanwhile is the ambient step above: the occupant releases its own lease from its own shell. Closing it needs either host cooperation (publish the variable to the hook sibling) or a hook-process capture facility, and both are separate work.
423
+ - ! **Lifecycle identity transport (#3611):** for every host with a resolved owner, PreToolUse rewrites only exact, simple canonical `deft` / `directive` lifecycle commands and source-repo `task <verb> [-- ...]` forms to add the host's `--session-id`. Direct CLI spellings are `session:start`, `session:ready`, `session:end`, `occupancy:steal`, `occupancy:release`, `occupancy:heartbeat`, `occupancy:grant` (#3954, its `--revoke` arm included), and `swarm-launch`; the Task spelling for the last verb is `swarm:launch`. For owner-requiring lifecycle commands, the shell execution directory must be absent or realpath-align exactly with the hook project root. Path-bearing executables (`./deft`, `/path/deft`), path/destination flags (`--project-root`, `--paths`, path-valued `--stories`, `--output`, `--worktree-map`, `--gate-clearances`), `--no-audit`, consumer-repo Task indirection, Task aliases (`task deft:<verb>`), and compound, redirected, quoted, aliased, wrapped, or otherwise ambiguous forms are not auto-rewritten. A supported-host hook rejects a recognized lifecycle form outside the narrow rewrite surface until it carries the explicit matching `--session-id`; an execution-root mismatch on an owner-requiring command always denies. With a matching ID on other excluded forms, the original command remains under normal host permission handling. Manual/integrated-terminal callers must likewise pass one matching `--session-id` (or `DEFT_SESSION_ID`) through the complete lifecycle.
418
424
  - ! **Transition, steal, and release (#3611):** an existing live UUID lease cannot silently become a host-namespaced owner. Prefer one aligned transition: `session:start --steal --confirm --occupant <reported-session-id> --session-id=<your-session-id>` (an exact host-mediated command receives the final option automatically). Bare `occupancy:steal --confirm --occupant <reported-session-id> --session-id=<your-session-id>` changes only the lease; direct writes remain denied unless ritual state already names the new owner. If they differ, align the same writer ID with `session:start --rearm --session-id=<same-session-id>` when re-arm is eligible, or cold `session:start --session-id=<same-session-id>` otherwise. The occupant drops a live lease with `occupancy:release` or `session:end`; a non-owner cannot clear it. Confirmed steal remains required for a live occupant and prints existing `claimed_at` / `heartbeat_at` / `last_write_at` when available.
419
425
  - ! **Lease refresh (#3599):** a gated write by the owner re-stamps `heartbeat_at`, so a session that keeps working keeps its lease, up to the absolute age cap below. The re-stamp is floored at a quarter of the TTL to avoid rewriting the lease per event, and it records `last_write_at` separately — `heartbeat_at` moves on any lease touch, `last_write_at` only on a gated product write, which is what tells a would-be stealer that the occupant is mid-work. When the owner's own lease is inside the staleness window (three quarters of the TTL) and the automatic re-stamp could not run, the write gate says so instead of letting the lease lapse silently; a write that did re-stamp stays quiet, because the warning would name a state that write just cleared. A re-stamp blocked by the lock leaves the lease untouched and the gate re-decides against the file on disk: contention alone keeps the rightful owner writing, while a takeover that completed during the wait denies. Long quiet stretches produce no gated write to ride on, so the owner refreshes explicitly with `deft occupancy:heartbeat` (Task: `task occupancy:heartbeat`). Refresh extends a lease the caller already holds — it never claims a free worktree, never takes a foreign lease, and never mints an owner; an unheld lease exits non-zero. Under a supported host the exact command receives the matching `--session-id` automatically; manual and Grok callers pass `--session-id <id>` or `DEFT_SESSION_ID`.
420
426
  - ! **Absolute lease age cap (#3599):** a lease is also dead 12 hours after `claimed_at`, whatever its heartbeat says, and no refresh path may extend that — `claimed_at` survives every re-stamp. Occupancy admits whoever presents the occupant's session id, so refresh alone would let any process holding that string keep a worktree forever and disable the crash-recovery reclaim the TTL exists for. The two dead states call for different responses and read differently: a stale heartbeat means refresh sooner; past the cap the tree is no longer held, so the capped holder's own gated writes are **refused** until it re-claims with `session:start --session-id=<your-session-id>` — allowing them would let the very bearer the cap bounds keep mutating a worktree a peer may claim at any moment. A lease that is both stale and capped reads as capped, because refreshing cannot revive it and the stale wording would send the holder somewhere that does not help. A different session is unaffected: taking over an aged-out tree is the reclaim the cap exists to enable. Twelve hours is sized by the stalled owner, not the busy one: refresh keys on writes, so an agent that finishes overnight and waits for its operator stops refreshing while staying legitimate, and 12 hours spans a 23:00 dispatch to a 09:00 handoff while still bounding reclaim inside a day. Reaching the cap costs one re-claim, never the work. A time cap cannot tell a stalled-but-live owner from a dead one — the only liveness signal on this path is a write; if that bites, the answer is a write-free liveness signal (explicit parked state, or refresh on non-write activity), not a larger number.
421
- - ! **Lease membership (#3755):** the occupant admits a dispatched child with `deft occupancy:grant --session-id=<owner> --child-session-id=<child> --role <worker-role> [--ttl-minutes N] [--worktree <path>] [--host <id>] [--address <name>] [--join-protocol none|heartbeat-file|parent-message]` (Task: `task occupancy:grant`), and withdraws it with the same command plus `--revoke`. The grant records owner id, child id, worktree, role and expiry, so a write resolves to a session rather than to whoever held the string. Admission splits: **writes** are admitted for the owner **or** a valid unexpired member; **release, steal, heartbeat and cohort close-out stay owner-only**, so a grant never escalates into the lease itself. An expired grant is refused on read, expiry is clamped to the lease's own absolute age cap, a lease carries at most 32 live grants, and a steal or a fresh claim starts with none — grants belong to the lease that issued them. A member's write re-stamps `heartbeat_at` and `last_write_at` on the same quarter-TTL floor as the owner's: a tree a granted child is actively writing is in use, and letting it lapse would hand the worktree to a peer mid-edit. `claimed_at` is untouched, so the absolute age cap and the grant's own expiry still bound it. The verb is outside the narrow rewrite surface above, so pass `--session-id` (or `DEFT_SESSION_ID`) explicitly. All of it is cooperative, like the rest of this lease: it names who may write, it does not authenticate them. Ritual state stays single-owner, so a member writes under the occupant's ceremony: the composite hook write gate measures the verified ritual owner against the occupant that issued the grant rather than against the writer, and a revoked or expired member is refused there as well.
427
+ - ! **Lease membership (#3755):** the occupant admits a dispatched child with `deft occupancy:grant --session-id=<owner> --child-session-id=<child> --role <worker-role> [--ttl-minutes N] [--worktree <path>] [--host <id>] [--address <name>] [--join-protocol none|heartbeat-file|parent-message]` (Task: `task occupancy:grant`), and withdraws it with the same command plus `--revoke`. The grant records owner id, child id, worktree, role and expiry, so a write resolves to a session rather than to whoever held the string. Admission splits: **writes** are admitted for the owner **or** a valid unexpired member; **release, steal, heartbeat and cohort close-out stay owner-only**, so a grant never escalates into the lease itself. An expired grant is refused on read, expiry is clamped to the lease's own absolute age cap, a lease carries at most 32 live grants, and a steal or a fresh claim starts with none — grants belong to the lease that issued them. The child id is checked at grant time (#3954): a value under the reserved `host:` prefix must be a well-formed canonical owner, and one that re-prefixes the owner's own host payload under a second provider is refused as the self-grant it is. Both were granted before, and the write gate then admitted them as `member`, so the lease read as membership while admitting nobody. An id outside that prefix is still accepted, because a child on a host with no identity contract presents whatever `DEFT_SESSION_ID` holds. Withdrawal skips the check, so a malformed grant written earlier stays revocable. A member's write re-stamps `heartbeat_at` and `last_write_at` on the same quarter-TTL floor as the owner's: a tree a granted child is actively writing is in use, and letting it lapse would hand the worktree to a peer mid-edit. `claimed_at` is untouched, so the absolute age cap and the grant's own expiry still bound it. The verb is inside the rewrite surface since #3954, so a supported host adds the matching `--session-id`; a `--worktree` rebind stays outside it and must carry the owner explicitly. All of it is cooperative, like the rest of this lease: it names who may write, it does not authenticate them. Ritual state stays single-owner, so a member writes under the occupant's ceremony: the composite hook write gate measures the verified ritual owner against the occupant that issued the grant rather than against the writer, and a revoked or expired member is refused there as well.
428
+ - ! **Parent and child, per identity-source kind (#3954 / #3999):** there is no single answer, so do not look for one. On a **`host-env`** host (Grok) the host publishes a different id into each agent session, so a parent and its dispatched children are **different actors**: each resolves its own owner through the chain above and claims its own worktree, which is where the dispatch envelope already puts it. Membership is **not** automatic there — 32 grants at a four-hour expiry against a twenty-minute lease means granting on every dispatch exhausts a busy parent's lease inside a day — and the revocation trigger stays the owner's own `--revoke` or the grant's expiry. A dispatched child's owner lease is released when orchestration observes `phase: terminal`, using the occupancy owner and exact worktree root the parent recorded at dispatch under `.deft/child-occupancy/` (lease-gated; not `.deft-scratch/**`). Release is compare-and-release under the occupancy lock: only if that recorded child still owns that recorded tree. A successor owner is left alone. On a **`payload`** host (Codex, Claude Code, Cursor) parent and subagents share one id, so there is no foreign child lease to admit and nothing to grant; the live consequence is the inverse one, that a parent's `occupancy:release` removes a working child's lease mid-flight with no denial, because both present the same string. The terminal transition is a no-op on that arm — auto-release would drop the parent's live lease. Swarm close-out still releases only the launcher's `occupancy_session_id`. No new reap verb.
422
429
  - ! **Lifetime and enforcement boundaries:** expired heartbeat (20 min) is free for claim-over-expired, and the release path may clear residue; no `reap` verb. No host-to-lease map or credential file is persisted; supported hooks re-derive their owner from each payload. Gated writes renew the heartbeat (#3599), so the 20-minute window runs from the owner's last write rather than from its claim, bounded by the absolute age cap above. Missing or drifted hooks remain #3742, and spawn tools remain outside occupancy enforcement. Join negotiation is owner-issued membership (`occupancy:grant`, #3755); the parked child-initiated `occupancy:request` queue stays unimplemented and is no longer named in remediation. `swarm:launch` carries the same explicit owner and persists `occupancy_session_id` in a cohort-keyed record; close-out uses that cohort entry as authority, requires any ambient owner to corroborate it, and never substitutes the current live lease owner. Swarm close-out stays `releaseSwarmOccupancy` on complete-cohort.
423
430
  - ~ Operators MAY still explicitly request full `deft session:start`, `deft triage:welcome`, sync, or doctor in read-only sessions.
424
431
 
@@ -557,7 +564,7 @@ User-facing surface for the Phase 0 triage workflow and the unified content cach
557
564
 
558
565
  Directive does not guess your mix. Either you name the next units in order (**ordered plan**), or you let the ranked backlog suggest (**queue**). Labels bias the queue; they do not override an active plan.
559
566
 
560
- **See also (pre-promotion portfolio):** when the need is to cluster competing RFCs/issues into a propose-not-apply priority brief (not single-item queue ranking), use `skills/deft-directive-portfolio-priority/SKILL.md` (#3201 / #3198) classify is filter-only; no SCM label writes.
567
+ **See also (pre-promotion portfolio):** when the need is to cluster competing RFCs/issues into a propose-not-apply priority brief (not single-item queue ranking), use `skills/deft-directive-portfolio-priority/SKILL.md` (#3201 / #3198). Classify `--mirror` is withdrawn (#4070); no SCM label writes from that skill.
561
568
 
562
569
  | Path | When | Who sets it | Bare "what's next?" means |
563
570
  |---|---|---|---|
@@ -585,11 +592,11 @@ Directive does not guess your mix. Either you name the next units in order (**or
585
592
  - `task triage:history -- <issue>` -- show decision history.
586
593
  - `task triage:reset -- <issue>` -- append a reset record so a candidate can be reconsidered.
587
594
  - `task triage:bulk-accept|bulk-reject|bulk-defer|bulk-needs-ac` -- apply predictable decisions over filtered cached candidates.
588
- - `task triage:summary`, `task triage:scope`, `task triage:scope-drift`, `task triage:subscribe`, `task triage:unsubscribe`, `task triage:classify`, `task triage:welcome`, and `task triage:smoketest` -- supporting workflow and onboarding commands.
589
- - `task triage:classify -- --mirror [--apply] [--re-enrich] [--include-closed] [--author LOGIN|@me] [--repo owner/name] [--batch-size N] [--delay-ms N] [--sample-limit N] [--json]` -- **Bootstrap mass-triage / Tier-1 SCM label mirror (#1423 Wave 1–2 / #3125, #3129, #3197).** Runs the existing classify engine over the github-issue cache and mirrors outcomes as labels (`triaged` idempotency marker + optional `plan.policy.triageLabelMirror.actionLabels`). **Default state filter is open-only** (opt-in `--include-closed` for closed archive stamps). Optional `--author` / `--author-mine` scopes plan/apply to matching `author.login` (AND with open-only; digest surfaces the filter) (#3129). Dry-run by default: operator digest with totals (scanned/planned/already_triaged/no_match/closed_skipped/author_skipped/errors), breakdown by **state / rule / action**, and samples (not a full dump as primary UX); `--json` includes the same aggregates. `--apply` writes via the SCM label client / repo-mutation boundary in **batches** (`--batch-size`, default 10) with **rate-limit delay** (`--delay-ms`, default 1000), reports partial failures, and is idempotent on re-run (already-`triaged` skip). **Re-run vs re-enrich (#3124 / #3197):** default re-run keeps the one-shot stamp (`skipped_already_triaged`). After `actionLabels` / auto-classify rule / hold-marker changes, opt in with **`--re-enrich`** (still dry-run by default; pair with `--apply` to write) to re-classify already-stamped issues and plan **additive** label deltas only (v1 never removes obsolete chips; never full reconcile). Digest distinguishes `kind=first-time` vs `kind=re-enrich` planned/applied rows (`planned_kind` / `re_enrich_planned`). Missing labels fail closed per issue with a create-label hint. **Never** calls `triage:accept` and **never** writes `proposed/` xBRIEFs. Wave 3 (agent Tier-2 comments) remains out of scope.
595
+ - `task triage:summary`, `task triage:scope`, `task triage:scope-drift`, `task triage:subscribe`, `task triage:unsubscribe`, `task triage:classify`, `task triage:strip-withdrawn-chips`, `task triage:welcome`, and `task triage:smoketest` -- supporting workflow and onboarding commands.
596
+ - `task triage:classify -- --mirror` -- **Withdrawn (#4070).** Dry-run and `--apply` both fail closed with a pointer at #4070. Replacement sieve is #4071. #3579 is transitively withdrawn for the gap. Do not close #1423, #3579, #2611, or #3923. `--list` / `--validate` remain. Strip leftover chips with `task triage:strip-withdrawn-chips` (remaining-set apply, not classify).
590
597
  - **Triage write-back pairing (#1423 / ADR-005).** When the author stamps that a lean is mechanism-shaped, write **both**: body-text field `mechanism-shaped: true` (the artifact) **and** mirrored label `design-critique:mechanism-shaped` (what `plan.policy.judgmentGates` matches and lists show). Do not compute the classification. No stamp -> the design-critique gate never fires. Clearance is a separate line on the thread: `design-critique: warranted | not warranted, because ...` -- engine evaluates presence, shape, and authority, never the because-clause content. Advisory/observe; `verify:judgment-gates --enforce` stays opt-in unused; `scope:promote` does not gain `--enforce` in this story.
591
598
  - **Design-critique catalog chip (#3642).** Parent attach of `triage-ready` / recut `mechanism-shaped` MUST use `task scm:issue:design-critique-chip -- --issue N --chip triage-ready|mechanism-shaped [--repo OWNER/NAME]` (or `deft scm issue design-critique-chip`). Closed catalog remaining-set replace, one write. ⊗ `gh api POST .../labels`. ⊗ additive `scm:issue:edit --add-label`.
592
- - **Operator discovery for SCM label mirror (#3124).** Cold `session:start` (via `triage:welcome` default mode) surfaces a **throttled** tip until the first successful `--mirror` dry-run or operator ack — **not** on every re-arm. Tip teaches existence **and** get-the-most: dry-run `deft triage:classify -- --mirror` (open-only; `--include-closed` opt-in); `--apply` batches writes and **never** auto-accepts into `proposed/`; defaults only stamp **`triaged` on matches** (control stamp, not disposition); **board usability is greatly decreased without `actionLabels`** — recommend full five-chip map (`defer→triage:deferred`, `archive→triage:archived`, `accept→triage:lifecycle-linked`, `escalate→triage:needs-human` + always `triaged`); more matches → `plan.policy.triageAutoClassify` in PROJECT-DEFINITION; inspect via `deft policy:show --field=plan.policy.triageLabelMirror`; labels must exist on GitHub; applying `triaged` before action chips skips re-enrichment on re-run (use `--re-enrich`); point at consumer kit **#2611** (`content/docs/consumer-issue-label-kit.md`) do not invent vocabulary. Dismiss without dry-run: `deft triage:classify -- --ack-discovery` (production entry for tip ack). Dry-run digests SHOULD footer-hint when `actionLabels` is empty or open `no_match` dominates. **Anti-swallow:** when the tip fires, agents MUST restate existence + get-the-most in the **user-visible** message (not absorb ceremony alone).
599
+ - **Operator discovery for SCM label mirror (#3124).** Withdrawn with classify `--mirror` (#4070). The session/welcome tip does not fire. #3124 discovery and #1423 Wave 1–2 apply are unusable after this lands; those issues stay open.
593
600
 
594
601
  ### Cache Tasks
595
602
 
@@ -155,10 +155,27 @@ panel-deposit
155
155
  round: 1
156
156
  siblings: 3
157
157
  input-ceiling: 5390001612
158
+ families: grok, claude, codex
159
+ ```
160
+
161
+ A sibling-seat record is an allowed substitute for the `families:` line:
162
+
163
+ ```text
164
+ seat: grok launcher: spawn_subagent
165
+ seat: claude launcher: claude
166
+ seat: codex launcher: codex
158
167
  ```
159
168
 
160
169
  **Panel completeness is behavioural.** The deposit MUST above, and every sibling-completeness clause in this document, bind the parent. No code observes them. `evaluateCompletedArcRecord` reads a deposit only as evidence that an arc is in flight; it never counts critic posts and never compares a count against `siblings:`. `evaluateParentAudit` carries no round, sibling, or deposit field. Both halves hold: the obligation on the parent is real, and nothing machine-checks it. A parent that binds on a partial panel breaks this contract and no gate will stop it (#3850).
161
170
 
171
+ **Seat families are a different check (#4067).** Completeness is still unobserved. Family composition is fail-closed over parent-claimed families and a PATH probe, not over live GitHub comments.
172
+
173
+ - ! For spend N≥3, the panel-deposit or a sibling-seat record names three claimed families before the first sibling spawn. Parent claims the family at dispatch. ⊗ Classify family from a model slug.
174
+ - ! A same-family sibling set is not a panel. ⊗ Lean it. Printed recovery is re-seat (or halt), not wait for Stop 5.
175
+ - ! Grok Build launcher tree: Grok seat via `spawn_subagent` (or `grok`); Claude seat via `claude` CLI; Codex seat via `codex exec`. Worktree isolation before each spawn. Probe `claude` / `codex` on PATH before offering paste-ready. Paste-ready is the fallback when a named family's CLI is absent. Close stdin on CLI spawns (Windows `cmd /c "… <nul"`; Unix `… </dev/null`). Pointer: [`docs/grok-build-subscription-setup.md`](../docs/grok-build-subscription-setup.md).
176
+ - ! `evaluatePanelSeatComposition` (`packages/core/src/design-critique/panel-seat-families.ts`) refuses a same-family N≥3 set and refuses paste-ready-first when `claude` / `codex` resolve. It takes claimed families and PATH probe results. It does not observe live comments and does not count siblings.
177
+ - ! After a dispatch-composition miss, offer a prevention issue. On yolo, file it. ⊗ Only re-seat.
178
+
162
179
  ### Comment lead (model then role)
163
180
 
164
181
  Comment-lead field. The first two lines of the triage write-back and of every critic, parent, and #3640 auto-posted comment name the LLM and the posting role. Keep the first line as `model: <slug>`. The second line is `role: triage|critic|parent`.
@@ -211,11 +228,14 @@ Keep the arc in this contract until a verified synthesis is accepted.
211
228
 
212
229
  - ! Each critic dispatch EXITs after posting.
213
230
  - ! Operator (or parent after an operator verb) dispatches the next envelope.
214
- - ! After each critic EXIT, parent posts a successor lean with proposed per-heading takes **before** printing `accept` / `retry differences` / `walk` / `walk all`. That posted lean is the first operator surface. Chat is not the record.
231
+ - ! After this round's same-round siblings are posted, parent posts a successor lean with proposed per-heading takes **before** printing `accept` / `retry differences` / `walk` / `walk all`. That posted lean is the first operator surface. Chat is not the record.
232
+ - ! Do not post the successor lean or print the verb menu while same-round siblings remain unposted. Spend is permission, not the wait rule. Parent dispatch bookkeeping is the trigger; thread posts corroborate.
233
+ - ⊗ Count self-attested `role: critic` lines as panel-complete.
215
234
  - ! Operator confirm or amend binds the proposed takes on that posted lean. Binding takes is not synthesis bind and does not stamp `design-critique:triage-ready`.
216
- - ⊗ Bind synthesis or stamp `design-critique:triage-ready` while same-round siblings remain unposted. The first lean after one critic EXIT is the take-offer, not the bind.
217
- - ! Later successor leans follow accept-X or walk-end, or land before synthesis. This supersedes #3627's "successor lean only after accept-X" for the first lean after critic EXIT. Later leans may still follow accept-X / walk-end.
218
- - ⊗ Print `accept` / `retry differences` / `walk` / `walk all` when no successor lean is posted for this critic EXIT. An empty-lean verb menu is a contract miss.
235
+ - ⊗ Bind synthesis or stamp `design-critique:triage-ready` while same-round siblings remain unposted. The first lean after this round's siblings are posted is the take-offer, not the bind.
236
+ - ! Later successor leans follow accept-X or walk-end, or land before synthesis. This supersedes #3627's "successor lean only after accept-X" for the first lean after this round's siblings are posted. Later leans may still follow accept-X / walk-end.
237
+ - ⊗ Print `accept` / `retry differences` / `walk` / `walk all` when no successor lean is posted for this round. An empty-lean verb menu is a contract miss.
238
+ - ! The halt line remains postable while same-round siblings remain unposted. An unposted sibling with no dispatch-fail evidence is Dual stop envelope exhaustion.
219
239
  - ⊗ Auto-dispatch critics (#3578 / #1702).
220
240
  - ⊗ Hand the arc to `triage:accept` / `scope:promote` until the completed-arc record is present: `design-critique: synthesis accepted, because …` citing the accepted successor lean (and the verified-claims table when posted). Catalog chips (`design-critique:mechanism-shaped` / `design-critique:triage-ready`) are list-visible convenience, not clearance. A lone synthesis-accepted-shaped comment that does not cite an accepted lean does not unblock ingest.
221
241
  - ⊗ Stamp `design-critique:triage-ready` at critic-post.
@@ -225,12 +245,13 @@ Keep the arc in this contract until a verified synthesis is accepted.
225
245
 
226
246
  ## Successor lean
227
247
 
228
- After each critic EXIT, parent posts a successor `**Lean:**` comment with proposed per-heading takes. That posted lean is the first operator surface. Later successor leans follow accept-X or walk-end, or land before synthesis.
248
+ After this round's same-round siblings are posted, parent posts a successor `**Lean:**` comment with proposed per-heading takes. That posted lean is the first operator surface. Later successor leans follow accept-X or walk-end, or land before synthesis.
229
249
 
230
- - ! After critic EXIT, post the successor lean before printing `accept` / `retry differences` / `walk` / `walk all`.
250
+ - ! After this round's same-round siblings are posted, post the successor lean before printing `accept` / `retry differences` / `walk` / `walk all`.
231
251
  - ! Lead that lean with the plain-language summary under the `## In plain English` token. The obligations are in `## Plain-language summary` below.
232
252
  - ! Operator confirm or amend is what makes those takes bindable. An all-accept draft still goes through this offer. Confirming or amending an all-accept first lean binds those takes. It does not auto-stamp synthesis or `design-critique:triage-ready` while same-round siblings remain unposted.
233
253
  - ! Cite accepted critic ids/headings, the still-open residual, and the write-back or prior lean it supersedes.
254
+ - ! A lean that closes a round of two or more names that round's dispatched sibling count and cites each sibling comment id it is leaning over.
234
255
  - ! Carry a per-heading take on the successor lean: `accept-into-contract` | `disagree` | `defer`. Defer is not accepted.
235
256
  - ! The successor lean is the disposition map. Do not post a third map type.
236
257
  - ! The first posted map is an ADR-006 arbitration surface. Record a substantiation token when takes introduce load-bearing premises. Non-self-arbitration applies when the same party authored the triage and the proposed takes.
@@ -286,17 +307,18 @@ The summary leads both artifacts under one fixed heading token: `## In plain Eng
286
307
 
287
308
  Comment bodies are parsed at runtime, so prose in them is not inert. Three predicates in `packages/core/src/design-critique/completed-arc-record.ts` classify a comment by a line-start anywhere in its body: the successor-lean token (`Lean:` with zero to two asterisks on each side, so nine spellings), the verified-claims-table heading, and the fixed accepted sentence. None of the three carries a position predicate, so a fence does not protect a quoted example the way `### Position predicate` protects a citation.
288
309
 
289
- The prohibition is per-artifact, and the asymmetry is the point. Measured at `c6761881` against the built module:
310
+ The prohibition is per-artifact, and the asymmetry is the point. Re-measured at `764f63a6` against the built module, after #3932 and #3929 landed; this supersedes the `c6761881` measurement, which predated both:
290
311
 
291
312
  | Reserved line-start | In a successor lean | In a synthesis |
292
313
  | --- | --- | --- |
293
314
  | successor-lean token, all nine spellings | inert -- the comment already is the lean, so 0 of 9 changed a verdict | ⊗ -- the synthesis reclassifies as the newest lean; 9 of 9 flip a complete arc to blocked, and the operator can satisfy that error only by citing the comment against itself |
294
- | `## Verified-claims table` | ⊗ -- the lean then satisfies the synthesis own table citation: a synthesis citing a table id that is not a table on the thread returns complete with the resolved table id equal to the lean id, where the control blocks on `missing-table-cite`. A silent pass rather than a visible block, and a second instance of the #3932 family | ⊗ -- the synthesis reads as its own table |
315
+ | `## Verified-claims table` | ⊗ -- the lean stands in as the table on the untyped path: a synthesis naming its table with `comment <id>` or a permalink returns complete with the resolved id equal to the lean id, where the control resolves null. A silent misresolution rather than a visible block. A typed claim now blocks whether or not the lean carries the heading, so the silent half survives only where the synthesis does not type its table citation | ⊗ -- the synthesis reads as its own table |
295
316
  | the fixed accepted sentence | ⊗ -- the lean reclassifies as a synthesis and a complete arc flips to blocked. A fence does not help. A blockquote is undetected by this predicate but refused by `### Position predicate`, so no one quoting convention is safe for both parsers | required -- it is the record |
296
317
 
297
- The ghost-table half of the middle cell is the #3932 defect and is repaired separately. What this prohibition covers is the classification collision underneath it: the comment reads as an artifact kind it is not, whatever the resolver later does with that.
318
+ The ghost-table half of the middle cell is the #3932 defect, repaired at `ba3d6a8f` and re-measured above. What this prohibition covers is the classification collision underneath it: the comment reads as an artifact kind it is not, whatever the resolver later does with that.
298
319
 
299
320
  - ! Keep those line-starts out of a summary, per that matrix.
321
+ - ! Read this matrix with `### Verified-claims table heading`. The same token is required on the verified-claims table when a typed claim names it, and refused here on the two artifacts that must not read as one. A reader who meets the token first as a hazard learns only half of it.
300
322
  - ! Read the same matrix for every other comment on the thread. The lean and table predicates scan every comment, not only the two meant to carry them, so a walk comment or an aside that opens a line with the lean token blocks ingest for the whole issue.
301
323
  - ⊗ Quote the fixed accepted sentence anywhere except the completed-arc record. A summary is where an author reaches for it, because what the synthesis would assert is that sentence. Name the outcome instead, or cite the record comment id.
302
324
  - ⊗ Read the inert cell as licence. That cell is inert because the comment is already lean-shaped, not because the token is harmless.
@@ -327,7 +349,7 @@ audit:<id> sha=<git-sha> pointer=<path:start-end|comment:<id>> reading=measured|
327
349
 
328
350
  ## Operator verbs
329
351
 
330
- Contract stops stay internal. Parent prints these phrases when they apply. They apply only after a successor lean is posted for this critic EXIT. Printing the verb menu with no posted successor lean is a contract miss. The operator does not have to remember them.
352
+ Contract stops stay internal. Parent prints these phrases when they apply. They apply only after a successor lean is posted for this round. Printing the verb menu with no posted successor lean is a contract miss. The operator does not have to remember them.
331
353
 
332
354
  - **accept** (cite findings)
333
355
  - **retry differences**
@@ -411,6 +433,21 @@ On the #3640 all-accept path, parent auto-posts the verified-claims table as its
411
433
 
412
434
  Distinguish measured evidence from endorsed evidence. Same-family agreement is correlated, not confirmatory.
413
435
 
436
+ ### Verified-claims table heading
437
+
438
+ `evaluateCompletedArcRecord` identifies the table by shape. `isVerifiedClaimsTableBody` matches a `## Verified-claims table` heading at a line start, and that heading is the only artifact-identity signal the resolver has. It decides a verdict on one citation form.
439
+
440
+ - ! Open the verified-claims table with the `## Verified-claims table` heading whenever the synthesis names that table with a typed `verified-claims table <id>` citation. Without the heading the record blocks on `unshaped-table-cite`.
441
+ - ! State the requirement together with the citation form that makes it operative. The heading is what a typed claim resolves against; it is not a free-standing shape rule.
442
+ - ~ Carry the heading on every verified-claims table. Which form a later synthesis will use is not knowable when the table is posted, and the heading costs nothing on the paths where it decides nothing.
443
+ - ⊗ Publish the heading as a requirement binding on every citation form. On the untyped path it changes no verdict, and a published rule stricter than the evaluator is this defect inverted -- the content-contract tests would lock the overstatement in.
444
+
445
+ **The untyped path has no verdict effect.** When the synthesis names its table with `comment <id>` or a permalink, or does not name it at all, the record completes and records a null `citedTableId` -- whether the table lacks the heading, is uncited, or is not on the thread at all. Re-measured at `764f63a6` against the built module. `resolveCitedTable` defers narrowing the citation contract so that a table claim must be typed; until that lands, the heading binds only where a typed claim names it.
446
+
447
+ The resolved id has no consumer today: `packages/core/src/intake/issue-ingest.ts` calls `assertCompletedArcAllowsIngest` for its throw and discards the return. Giving `citedTableId` a consumer would make that null a decision rather than a record, and this section would need re-measuring.
448
+
449
+ `### Reserved line-starts` refuses the same token on the successor lean and on the synthesis. One string, two polarities, by artifact: required on the table under a typed claim, refused on the two artifacts that must not read as one.
450
+
414
451
  ## Bind after accepted synthesis
415
452
 
416
453
  Two bind paths authorize:
@@ -430,7 +467,13 @@ Closed catalog (last chip wins): `design-critique:mechanism-shaped` (in-flight,
430
467
  - ⊗ `gh api POST .../labels` or additive `scm:issue:edit --add-label` for this facet.
431
468
  - ⊗ Intercept mixed `scm issue edit` adds/removes for this facet.
432
469
  - ⊗ General-purpose labels CLI.
433
- - ! After the completed-arc record is present, `triage:accept` / `scope:promote` / `issue:ingest` / build may proceed. Any identity may run those verbs. Same-session parent continuation is not required. GitHub Triage on the implementer is not required. They read the accepted verified synthesis (latest successor lean plus the verified-claims table).
470
+ - ! After the completed-arc record is present, `triage:accept` / `scope:promote` / `issue:ingest` / build may proceed unless `evaluateCompletedArcRecord` returns `cancelled` or `unrecut-body` (#4057). Any identity may run those verbs. Same-session parent continuation is not required. GitHub Triage on the implementer is not required. They read the accepted verified synthesis (latest successor lean plus the verified-claims table).
471
+ - ! Per-thread body ingest stays the mapper after a set-level bind. Stories the bind wants are recut bodies or newly filed issues. Derived AC stays on that body.
472
+ - ! Un-recut portfolio members fail closed on `cancelled`. Parent posts `design-critique: cancelled, because ...` on that number. Parent `role: parent` dominate prose is not that record. Critic, quoted, or fenced cancel-shaped prose is not that record. Leftover `mechanism-shaped` without cancel stays `missing-record` and is not this control.
473
+ - ! A later successor lean after cancel starts a recut arc. Completing a superseded thread without recut would mint the harvest.
474
+ - ! A complete record whose latest `target shape:` field is `set-level` is `unrecut-body`. Recut the body (and record a non-set-level shape) or file a new issue. Ingest the anchor only is not supported while that field remains set-level.
475
+ - ⊗ Select an author-blind latest successor lean as CurrentShape.
476
+ - ⊗ Put a set-level graph (dominate / split / corpus parsed from comment English) inside ingest.
434
477
  - ! Ingest clearance cites the latest successor lean. An older completed-arc record does not clear a later recut lean. A panel-deposit is in-flight even when the catalog chip missed and no critic has posted.
435
478
  - ! The lexical form of that citation, and the requirement that the occurrence be affirmative, are published in `## Citation grammar`. Ingest reads that grammar, not prose intent.
436
479
  - ! Keep `plan.policy.judgmentGates` matching only `design-critique:mechanism-shaped`. After `triage-ready` replaces it, the issue leaves the gate match.
@@ -500,6 +543,24 @@ The intake cross-ref scanners (`packages/core/src/intake/markdown-scanners.ts`)
500
543
  - ! Clearance is set membership: the record clears when the cited set contains the latest successor lean id. Position in the body does not select the lean, so citing the prior lean that `## Successor lean` requires cannot block.
501
544
  - ! A block detail reports what was scanned, what was found, and the accepted forms. ⊗ Guess at a cause. A guessed detail sends the operator back to re-post the same body and reproduce the refusal.
502
545
 
546
+ `CompletedArcBlockReason` is closed. A block detail names one of these eight:
547
+
548
+ | Reason | What it reports |
549
+ | --- | --- |
550
+ | `missing-record` | no completed-arc record cites the latest successor lean |
551
+ | `lone-shape` | the accepted sentence is present and cites no accepted successor lean |
552
+ | `cite-not-lean` | no cited id is a successor lean on this thread |
553
+ | `missing-table-cite` | a typed table claim names an id that is not a comment on this thread |
554
+ | `unshaped-table-cite` | a typed table claim names a comment on this thread that opens no line with the verified-claims-table heading |
555
+ | `ambiguous-table-cite` | two typed table claims name different tables |
556
+ | `cancelled` | `design-critique: cancelled, because ...` is the latest terminal record; no later successor lean |
557
+ | `unrecut-body` | completed-arc is present and the latest `target shape:` field is `set-level` |
558
+
559
+ - ! Publish a reason in that table before the evaluator returns it. An unpublished reason code is the same gap as an unpublished citation form.
560
+ - ⊗ Merge two states under one reason when their remedies differ. `missing-table-cite` and `unshaped-table-cite` were one reason and one detail until #3942, and the shared detail asserted an absent id in both, so an author whose table was on the thread read a true citation being called false and had no path to the missing heading.
561
+
562
+ The `unshaped-table-cite` detail names the heading because the diagnostics rule above already requires a detail to report what was found and the accepted form. That is conformance to it, not a second rule.
563
+
503
564
  ## Failure and budget stop
504
565
 
505
566
  - ! Failure/budget stop (#2442): Dual stop and Halt line. If a critic run fails or the arc exhausts its envelope, halt with the halt line. Do not thrash.
@@ -515,4 +576,4 @@ This motion ingests untrusted issue threads by design.
515
576
 
516
577
  ## Test surface
517
578
 
518
- `packages/core/src/content-contracts/standards/design_critique_contract.test.ts` locks required pointer strings, the scaffolds framing, the comment-lead field as model then role from the closed set (not an issue label), the operator-gated loop (successor lean, operator verbs including walk / walk all, dual stop, halt line, exclusive remaining-set replace of the two catalog chips, #3640 auto-stamp on a non-empty all-accept map and no-stamp on stubs, first-lean recording obligation after critic EXIT), the parent-side substantiation token and independence rules, the Stop 1 exclusion (pre-critic premises outside the trigger) and `refutation-target:` field tokens rather than full body sentences, the composed auto-bind conjunct (all-accept map AND zero unresolved audit markers) at Operator verbs and Bind path 1, the variant-table evaluation rule (charter selection and spend permission evaluated independently), the critic-method heading and distinctive obligation tokens (exact class tokens, citations-are-claims, existing mechanisms, injection / swarm trigger nouns, failed-reviewer phrase, finding anatomy) rather than full body sentences, the brief-template forbidden-inputs list, and the thin router skill (existence, line cap, pointer resolution, no-normative-content). `evaluateParentAudit` locks the omission failure modes. This suite locks the SoT MUST and the thin skill pointer for the first-lean recording obligation, including the auto-stamp operator-confirm conjunct and the no-bind-while-unposted-same-round-siblings rule. `evaluateCompletedArcRecord` locks ingest on the completed-arc record rather than a catalog chip. It does not fail-close live parent turns. `packages/core/src/design-critique/citation-grammar.test.ts` locks the `## Citation grammar` closed set, the refused positions, and the diagnostics surface; `packages/core/src/design-critique/completed-arc-record.test.ts` locks one parser for both questions, set membership against the latest lean, and the observation-echoing block details (#3831). Runtime parent-turn detection only if `evaluateParentAudit` is extended; that extension is not required to ship the recording obligation. Panel completeness is locked as contract text only. No predicate observes it on a live arc (#3850). `### The arc` and its derived boundaries, the `### Target shape` axis with its twice-run caveat, and the two bind-path-2 non-empty refusals are locked as contract text (#3797). `## Plain-language summary` is locked the same way: the contract test pins the heading token, the MUST-not-SHOULD reasoning, the ADR-006 exemption and its limit, the non-normative marking, and the per-artifact reserved line-start matrix, and `packages/core/src/design-critique/reserved-line-starts.test.ts` exercises each of the three families on each artifact kind against the exported shape predicates and `evaluateCompletedArcRecord`. No predicate observes a summary on a live arc (#3929).
579
+ `packages/core/src/content-contracts/standards/design_critique_contract.test.ts` locks required pointer strings, the scaffolds framing, the comment-lead field as model then role from the closed set (not an issue label), the operator-gated loop (successor lean, operator verbs including walk / walk all, dual stop, halt line, exclusive remaining-set replace of the two catalog chips, #3640 auto-stamp on a non-empty all-accept map and no-stamp on stubs, first-lean recording obligation after this round's siblings are posted), the parent-side substantiation token and independence rules, the Stop 1 exclusion (pre-critic premises outside the trigger) and `refutation-target:` field tokens rather than full body sentences, the composed auto-bind conjunct (all-accept map AND zero unresolved audit markers) at Operator verbs and Bind path 1, the variant-table evaluation rule (charter selection and spend permission evaluated independently), the critic-method heading and distinctive obligation tokens (exact class tokens, citations-are-claims, existing mechanisms, injection / swarm trigger nouns, failed-reviewer phrase, finding anatomy) rather than full body sentences, the brief-template forbidden-inputs list, and the thin router skill (existence, line cap, pointer resolution, no-normative-content). `evaluateParentAudit` locks the omission failure modes. This suite locks the SoT MUST and the thin skill pointer for the first-lean recording obligation, including the auto-stamp operator-confirm conjunct and the no-bind-while-unposted-same-round-siblings rule. `evaluateCompletedArcRecord` locks ingest on the completed-arc record rather than a catalog chip. It does not fail-close live parent turns. `packages/core/src/design-critique/citation-grammar.test.ts` locks the `## Citation grammar` closed set, the refused positions, and the diagnostics surface; `packages/core/src/design-critique/completed-arc-record.test.ts` locks one parser for both questions, set membership against the latest lean, and the observation-echoing block details (#3831). Runtime parent-turn detection only if `evaluateParentAudit` is extended; that extension is not required to ship the recording obligation. Panel completeness is locked as contract text only. No predicate observes it on a live arc (#3850). `evaluatePanelSeatComposition` locks claimed-family composition and PATH paste-ready-first; it is a fixture over claimed families, not a live sibling-count gate. `### The arc` and its derived boundaries, the `### Target shape` axis with its twice-run caveat, and the two bind-path-2 non-empty refusals are locked as contract text (#3797). `## Plain-language summary` is locked the same way: the contract test pins the heading token, the MUST-not-SHOULD reasoning, the ADR-006 exemption and its limit, the non-normative marking, and the per-artifact reserved line-start matrix, and `packages/core/src/design-critique/reserved-line-starts.test.ts` exercises each of the three families on each artifact kind against the exported shape predicates and `evaluateCompletedArcRecord`. No predicate observes a summary on a live arc (#3929). `### Verified-claims table heading`, the closed reason vocabulary, and the re-measured line-start matrix are locked as contract text, and `completed-arc-record.test.ts` exercises the typed refusal partition: the two states, details that differ by more than the id, the untyped null table id, and the seven recorded live arc table ids (#3942).
@@ -37,6 +37,11 @@ target is denied. An ApplyPatch body that names no classifiable mutation target
37
37
  while the fence is active. ⊗ Authorize only the declared path when the patch body names
38
38
  other targets.
39
39
 
40
+ Which tree the fence, occupancy, ritual and active scope are read from is decided before any of
41
+ them run, by root admission on the write target — including the deliberate payload-root fallback
42
+ for a target with no Git toplevel. Contract:
43
+ [`docs/hook-root-admission.md`](../docs/hook-root-admission.md) (#3794 / #4013).
44
+
40
45
  Deny reasons are stable and name the fence source:
41
46
 
42
47
  - `write fence project allowPaths (source: project)` or `project+story`
@@ -52,10 +57,29 @@ deft policy:show --field=runtimeAuthority
52
57
  ### Active-story seam
53
58
 
54
59
  The hook dispatcher loads `file_scope` from the implementation-eligible active xBRIEF
55
- path when `inspectActiveScope` reports one. Residual gaps (document, not silent):
60
+ path when `inspectActiveScope` reports one.
61
+
62
+ When more than one preflight-eligible artifact is in `xbrief/active/` (a cohort
63
+ sharing one tree), first-wins is **not** used: that would fence every worker to one
64
+ story's `file_scope` and over-permit the others (#4007). Bind the dispatched story:
65
+
66
+ - `DEFT_ACTIVE_SCOPE` (absolute, project-relative, or unique basename) must name
67
+ one eligible running brief; that path's `file_scope` is the story fence
68
+ - On win32, pin matching is case-insensitive (the filesystem is). POSIX pins stay
69
+ case-sensitive, and a backslash in a POSIX pin is a filename character, not a
70
+ separator
71
+ - Missing pin + multiple eligible → fail closed (`scope-not-ready`). Recovery: set
72
+ the pin, or keep one running brief in `xbrief/active/`
73
+ - A pin that does not name an eligible brief → fail closed
74
+
75
+ The filed `__tests__` matcher diagnosis is refuted: `matchAny` already admits that
76
+ exact path and `_` is literal. Do not invent a `__`-segment exception. Pre-`c99f6159`
77
+ worktree relativisation is a separate discriminator (the raw unedited deny string)
78
+ and is not closed here.
79
+
80
+ Residual gaps (document, not silent):
56
81
 
57
82
  - Host / worktree cannot identify the active story → story layer omitted; project fence still applies
58
- - Multiple active artifacts → first preflight-eligible path wins (same as scope gate)
59
83
  - Story JSON unreadable → story layer fail-open; project fence still applies
60
84
 
61
85
  Shell/MCP push/merge scopes remain project-only (`runtimeAuthority.scopes`); they are not
@@ -165,10 +189,12 @@ OS-temp dests and commands with no recognized dest (`git status`,
165
189
  - Destinations that are shell **variables** are not recovered. That is most
166
190
  logged shell: 1,089 of 1,131 calls were dynamic, compound, or emitted no
167
191
  target.
168
- - A **directory junction** created without elevation defeats `provablyExternal`:
169
- the path is lexically outside the root and its realpath is inside. That is
170
- **re-entry** polarity. `#3186` `assertProjectionContained` is **escape**
171
- polarity (in-tree dest whose realpath leaves the tree) and does not close it.
192
+ - A **directory junction** created without elevation is **re-entry** polarity.
193
+ `isInRepoShellWritePath` reuses `isOutsideProjectRootWrite` so a lexically-outside
194
+ dest whose realpath is inside the project is gated. `#3186` `assertProjectionContained`
195
+ remains **escape** polarity (in-tree dest whose realpath leaves the tree) and is
196
+ not this check. The hook still runs before the shell, so a link created between
197
+ check and write is TOCTOU.
172
198
 
173
199
  Fail-open at this predicate is the bound posture (#3997). Inverting it to
174
200
  fail-closed on dests the parser cannot prove external is that issue's refuted
@@ -109,10 +109,9 @@ https://github.com/deftai/directive/blob/master/content/docs/consumer-issue-labe
109
109
  - epic (rare multi-ship product root)
110
110
  - status:tracker, status:child
111
111
 
112
- ## Mirror (if using triage:classify -- --mirror)
113
- - triaged
114
- - optional action chips: triage:deferred, triage:archived
115
- (only if plan.policy.triageLabelMirror.actionLabels maps them)
112
+ ## Mirror (withdrawn #4070)
113
+ - `triage:classify -- --mirror` is withdrawn. Do not stamp `triaged` / `triage:*` from classify.
114
+ - Replacement sieve is #4071. Leave these names unused until the replacement recuts the catalog.
116
115
 
117
116
  ## Optional project routing (only if used)
118
117
  - security (advisory security review routing; not a merge gate)
@@ -126,16 +125,7 @@ Do not invent labels outside this file. Prefer existing names over twins.
126
125
 
127
126
  ## Optional: label mirror appendix
128
127
 
129
- After SCM label mirror Waves 1–2 (#1423 / #3125):
130
-
131
- | Topic | Guidance |
132
- |-------|----------|
133
- | Defaults | Mirror is available; on match it stamps **`triaged`** (idempotency). Action chips only when configured. |
134
- | Dry-run | `deft triage:classify -- --mirror` (open-only default). Review the digest before `--apply`. |
135
- | Apply | `deft triage:classify -- --mirror --apply` writes labels in batches; re-run is a no-op for already-`triaged` issues. |
136
- | More matches | Add rules under `plan.policy.triageAutoClassify` in `xbrief/PROJECT-DEFINITION.xbrief.json`. |
137
- | Richer chips | Set `plan.policy.triageLabelMirror.actionLabels` **and** create those GitHub labels first. |
138
- | Accept path | ⊗ Never auto-`triage:accept` / never auto-write `proposed/` from mirror. Accept stays an operator decision. |
128
+ After #4070, SCM label mirror Waves 1–2 (#1423 / #3125) are unusable. `deft triage:classify -- --mirror` (dry-run and `--apply`) fail closed. Strip leftover chips with `deft triage:strip-withdrawn-chips`. Replacement sieve is #4071. #2611 stays open. Accept stays an operator decision (`triage:accept` / ingest unchanged).
139
129
 
140
130
  Minimal policy sketch (clone and edit; broader triage field examples live in the framework repo at [`docs/example-project-definition.md`](https://github.com/deftai/directive/blob/master/docs/example-project-definition.md) — not deposited under `.deft/core/`):
141
131
 
@@ -166,13 +156,10 @@ Minimal policy sketch (clone and edit; broader triage field examples live in the
166
156
  }
167
157
  ```
168
158
 
169
- If you only want the default `triaged` stamp, you can omit `actionLabels` entirely.
170
-
171
- Validate with:
159
+ `triage:classify -- --mirror` is withdrawn (#4070). Do not add `triageLabelMirror` to stamp `triaged` / `triage:*`. Validate remaining classify rules with:
172
160
 
173
161
  ```bash
174
162
  deft triage:classify -- --validate
175
- deft triage:classify -- --mirror
176
163
  ```
177
164
 
178
165
  ---
@@ -221,5 +208,5 @@ Framework source / GitHub (browse when not in a deposit):
221
208
  ## Related reading
222
209
 
223
210
  - Maintainer taxonomy (do not import wholesale): [`.github/ISSUE_LABELS.md`](https://github.com/deftai/directive/blob/master/.github/ISSUE_LABELS.md) (#2609)
224
- - Label mirror behavior: deposit `.deft/core/commands.md` (or framework `content/commands.md`) / `deft triage:classify -- --mirror` (#1423, #3125)
211
+ - Label mirror withdrawn: deposit `.deft/core/commands.md` (or framework `content/commands.md`) / #4070 (do not run `deft triage:classify -- --mirror`)
225
212
  - Example PROJECT-DEFINITION triage fields: [`docs/example-project-definition.md`](https://github.com/deftai/directive/blob/master/docs/example-project-definition.md) (#1186; framework repo only, not deposited)
@@ -79,7 +79,7 @@ checks tools without rewriting consumer-managed files.
79
79
 
80
80
  If you are adopting Deft on a project that already has an issue tracker (existing repo, brownfield migration, an upstream bug list that has been accumulating), you do not have to start from an empty `xbrief/proposed/`. The refinement skill's **Phase 0 triage workflow** lets you walk an existing backlog locally, decide what to keep, and let only the **accepted** items land in `xbrief/proposed/`. Trigger words: **"triage"**, **"work the cache"**, **"pre-ingest"**, **"action menu"**.
81
81
 
82
- **Labels first:** capacity and triage matchers only help when labels exist. For a bare or thin label set, adopt the [consumer issue-label kit](./consumer-issue-label-kit.md) (#2611) before you rely on ranking or `triage:classify -- --mirror`.
82
+ **Labels first:** capacity and triage matchers only help when labels exist. For a bare or thin label set, adopt the [consumer issue-label kit](./consumer-issue-label-kit.md) (#2611) before you rely on ranking. `triage:classify -- --mirror` is withdrawn (#4070).
83
83
 
84
84
  ### Step 1 — Scoped first populate
85
85