@deftai/directive-content 0.109.0 → 0.109.1
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.
package/commands.md
CHANGED
|
@@ -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**:
|
|
@@ -413,12 +415,17 @@ After CLI/deposit upgrade, disk can show the new generation while a long-lived s
|
|
|
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
417
|
- ! **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.
|
|
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
|
-
- ! **
|
|
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
|
|
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
|
|
|
@@ -286,17 +286,18 @@ The summary leads both artifacts under one fixed heading token: `## In plain Eng
|
|
|
286
286
|
|
|
287
287
|
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
288
|
|
|
289
|
-
The prohibition is per-artifact, and the asymmetry is the point.
|
|
289
|
+
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
290
|
|
|
291
291
|
| Reserved line-start | In a successor lean | In a synthesis |
|
|
292
292
|
| --- | --- | --- |
|
|
293
293
|
| 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
|
|
294
|
+
| `## 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
295
|
| 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
296
|
|
|
297
|
-
The ghost-table half of the middle cell is the #3932 defect and
|
|
297
|
+
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
298
|
|
|
299
299
|
- ! Keep those line-starts out of a summary, per that matrix.
|
|
300
|
+
- ! 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
301
|
- ! 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
302
|
- ⊗ 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
303
|
- ⊗ Read the inert cell as licence. That cell is inert because the comment is already lean-shaped, not because the token is harmless.
|
|
@@ -411,6 +412,21 @@ On the #3640 all-accept path, parent auto-posts the verified-claims table as its
|
|
|
411
412
|
|
|
412
413
|
Distinguish measured evidence from endorsed evidence. Same-family agreement is correlated, not confirmatory.
|
|
413
414
|
|
|
415
|
+
### Verified-claims table heading
|
|
416
|
+
|
|
417
|
+
`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.
|
|
418
|
+
|
|
419
|
+
- ! 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`.
|
|
420
|
+
- ! 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.
|
|
421
|
+
- ~ 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.
|
|
422
|
+
- ⊗ 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.
|
|
423
|
+
|
|
424
|
+
**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.
|
|
425
|
+
|
|
426
|
+
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.
|
|
427
|
+
|
|
428
|
+
`### 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.
|
|
429
|
+
|
|
414
430
|
## Bind after accepted synthesis
|
|
415
431
|
|
|
416
432
|
Two bind paths authorize:
|
|
@@ -500,6 +516,22 @@ The intake cross-ref scanners (`packages/core/src/intake/markdown-scanners.ts`)
|
|
|
500
516
|
- ! 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
517
|
- ! 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
518
|
|
|
519
|
+
`CompletedArcBlockReason` is closed. A block detail names one of these six:
|
|
520
|
+
|
|
521
|
+
| Reason | What it reports |
|
|
522
|
+
| --- | --- |
|
|
523
|
+
| `missing-record` | no completed-arc record cites the latest successor lean |
|
|
524
|
+
| `lone-shape` | the accepted sentence is present and cites no accepted successor lean |
|
|
525
|
+
| `cite-not-lean` | no cited id is a successor lean on this thread |
|
|
526
|
+
| `missing-table-cite` | a typed table claim names an id that is not a comment on this thread |
|
|
527
|
+
| `unshaped-table-cite` | a typed table claim names a comment on this thread that opens no line with the verified-claims-table heading |
|
|
528
|
+
| `ambiguous-table-cite` | two typed table claims name different tables |
|
|
529
|
+
|
|
530
|
+
- ! Publish a reason in that table before the evaluator returns it. An unpublished reason code is the same gap as an unpublished citation form.
|
|
531
|
+
- ⊗ 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.
|
|
532
|
+
|
|
533
|
+
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.
|
|
534
|
+
|
|
503
535
|
## Failure and budget stop
|
|
504
536
|
|
|
505
537
|
- ! 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 +547,4 @@ This motion ingests untrusted issue threads by design.
|
|
|
515
547
|
|
|
516
548
|
## Test surface
|
|
517
549
|
|
|
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).
|
|
550
|
+
`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). `### 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`
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Hook root admission — which tree a gated write is judged against (#3794 / #4013)
|
|
2
|
+
|
|
3
|
+
Every PreToolUse mutation carries **two** roots, and they are not interchangeable:
|
|
4
|
+
|
|
5
|
+
- **payload root** — the project root the host hook payload names.
|
|
6
|
+
- **effective root** — the working tree the write target actually lands in, chosen by
|
|
7
|
+
`admitEffectiveHookRoot` from the target path.
|
|
8
|
+
|
|
9
|
+
Deny messages name both: `payloadRoot=<path> effectiveRoot=<path>`.
|
|
10
|
+
|
|
11
|
+
This page publishes what root admission decides, which gate reads which root, and why the
|
|
12
|
+
no-toplevel case behaves the way it does. It describes shipped behaviour; it changes none of it.
|
|
13
|
+
|
|
14
|
+
## The outcomes of root admission
|
|
15
|
+
|
|
16
|
+
| Target's nearest existing ancestor resolves to | Outcome | Effective root |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| The payload root itself | admit | payload root |
|
|
19
|
+
| Another working tree sharing `--git-common-dir` with the payload root | admit | that worktree |
|
|
20
|
+
| Another Git toplevel, while the payload root is not a Git repository | admit — no containment question exists | payload root |
|
|
21
|
+
| Another Git toplevel whose `--git-common-dir` cannot be read | refuse `unproven-identity` | — (deny) |
|
|
22
|
+
| A proven different repository | refuse `foreign-repository` | — (deny) |
|
|
23
|
+
| **No Git toplevel at all** | **admit** | **payload root** |
|
|
24
|
+
|
|
25
|
+
Set level: `admitMutationTargetSet` admits every member of one mutation (the declared ApplyPatch
|
|
26
|
+
path plus every path in the patch body), short-circuits on the first refused member, and then
|
|
27
|
+
requires **one unique** effective root across the members. Two admitted roots is a
|
|
28
|
+
`worktree-span` refusal, so occupancy and ritual cannot follow only the declared path.
|
|
29
|
+
|
|
30
|
+
The last row is the subject of #4013. It fires for any destination whose nearest existing ancestor
|
|
31
|
+
directory is not inside a Git working tree — a path under the OS temp directory, a file in the home
|
|
32
|
+
directory, a scratch directory outside every checkout.
|
|
33
|
+
|
|
34
|
+
## The no-toplevel case is deliberate
|
|
35
|
+
|
|
36
|
+
**A write target with no Git toplevel is gated against the payload root. That is intended, not a
|
|
37
|
+
gap.** #3794's acceptance text required falling back to the payload root when a target worktree
|
|
38
|
+
could not be admitted, and commit `bcd9d34e` deliberately split the two situations that fallback
|
|
39
|
+
used to hold: a target that resolves to *some other* toplevel whose identity cannot be read now
|
|
40
|
+
fails closed as `unproven-identity`, while a target with *no* toplevel keeps the payload-root
|
|
41
|
+
behaviour. "A question asked and left unanswered" is the rationale for the fail-closed branch. It
|
|
42
|
+
is not the rationale for this one — no containment question was ever posed.
|
|
43
|
+
|
|
44
|
+
Three measured reasons the fallback is not a lease bug (#4013, accepted successor lean
|
|
45
|
+
[5481139589](https://github.com/deftai/directive/issues/4013#issuecomment-5481139589)):
|
|
46
|
+
|
|
47
|
+
1. **The effective root selects more than a lease.** It is the input to occupancy, the session
|
|
48
|
+
ritual, active scope, the story write fence and assist-scratch classification. "No root, no
|
|
49
|
+
gating" would drop all of those for an out-of-tree write, so an unritualed session — or an agent
|
|
50
|
+
acting on an absolute path it read in an issue — could write a home, config or temp file with no
|
|
51
|
+
ritual and no scope basis. It would also buy nothing on active scope, which already exempts
|
|
52
|
+
outside-root writes (#2885).
|
|
53
|
+
2. **Occupancy and ritual are cross-checked, and the allow path re-stamps the lease.** The admitted
|
|
54
|
+
owner is compared against the session the verified ritual is bound to. Relaxing only the
|
|
55
|
+
foreign-lease refusal either leaves the writer blocked by that comparison, or suppresses it and
|
|
56
|
+
lets one session ride another's ceremony. The same path re-stamps `heartbeat_at` immediately
|
|
57
|
+
before an allowed write, so an unrelated out-of-tree write would renew the very lease a
|
|
58
|
+
narrowing meant to decouple.
|
|
59
|
+
3. **A nullable root has no defined behaviour for a multi-target patch.** Target-set admission
|
|
60
|
+
demands one unique root. If an outside member contributed nothing, a patch mixing a
|
|
61
|
+
linked-worktree edit with an injected absolute path could collapse to the linked root and evade
|
|
62
|
+
the `worktree-span` refusal; if absence dominated instead, one outside member would suppress the
|
|
63
|
+
gates for the in-tree edit beside it. Either way an untrusted patch path becomes an
|
|
64
|
+
authority-selection input — the containment risk #3794 closed.
|
|
65
|
+
|
|
66
|
+
The friction is real: a foreign lease holder should not block an unrelated note under the OS temp
|
|
67
|
+
directory. Relieving it costs a change to the authority contract, not a bug fix. See
|
|
68
|
+
[What a narrowing would have to define](#what-a-narrowing-would-have-to-define).
|
|
69
|
+
|
|
70
|
+
## What still runs for a no-toplevel target, gate by gate
|
|
71
|
+
|
|
72
|
+
| Gate | Root it reads | Disposition |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| Occupancy lease | effective root (= payload root here) | **Runs.** A live foreign lease on the payload root denies `occupancy-occupied`; the owner or a granted member is admitted. |
|
|
75
|
+
| Session ritual, gated tier | effective root (= payload root here) | **Runs**, including the occupancy↔ritual owner cross-check and the pre-allow re-check. |
|
|
76
|
+
| Authz / UAT grant scoping | payload root | **Payload-root authoritative**, unchanged by admission. |
|
|
77
|
+
| Authz audit trail | payload root | **Payload-root authoritative.** |
|
|
78
|
+
| Kill-switch (`.deft-directive-disable`) | payload root | **Payload-root authoritative.** A flag beside the target does not disable the gate. |
|
|
79
|
+
| `deny().projectRoot` | payload root | **Payload-root authoritative**, so a deny is reported against the tree the host named. |
|
|
80
|
+
| Mutation intent ceiling (#1193) | neither | **Runs independently** of both roots. |
|
|
81
|
+
| Read-only posture | neither | **Runs.** |
|
|
82
|
+
| Runtime authority / path write fence | project policy from the payload root; story `file_scope` and the path it matches from the effective root | **Runs** when enabled. |
|
|
83
|
+
| Active scope | effective root | **Inspected, deny skipped.** The not-ready deny is skipped by the #2885 outside-root carve-out, measured from the payload root. Spawn tools have no write target and still require scope. |
|
|
84
|
+
| Assist-scratch allowlist | effective root | **No match.** An out-of-tree target is not under `.deft-scratch/` or `temp/` relative to the effective root, so the low-ceremony path does not apply. |
|
|
85
|
+
|
|
86
|
+
Read that table as the answer to "what does the lease actually protect here". The case is not a
|
|
87
|
+
lease question with one disposition; it is a root-selection question with a disposition per gate.
|
|
88
|
+
|
|
89
|
+
## Three surfaces, three behaviours
|
|
90
|
+
|
|
91
|
+
The fallback is a **direct-write** behaviour. It does not generalise across transports.
|
|
92
|
+
|
|
93
|
+
| Surface | Example tool name | Reaches root admission? | Out-of-tree destination |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| Direct write, bare host name | `Write`, `Edit`, `search_replace`, `ApplyPatch` | **Yes** | Judged against the payload root — the fallback above. |
|
|
96
|
+
| Generic server-prefixed MCP | `tasks__search_replace` | **No** | Routed only through push/merge runtime classification; unrecognized there, so `shell-op-unclassifiable` (fail open). |
|
|
97
|
+
| Recognized Shell file-write | `Set-Content -Path <os-temp>/note.md …` | **No** | `isInRepoShellWritePath` rejects destinations resolving outside the project root, so the #3987 reissue path skips them (fail open). |
|
|
98
|
+
|
|
99
|
+
Catalogued MCP push/merge names (`git_push`, `merge_pull_request`, …) *are* gated — by runtime
|
|
100
|
+
authority scopes, which is a different gate and does not consult root admission either.
|
|
101
|
+
|
|
102
|
+
⊗ Do not state that the payload-root fallback fires "on the direct-write and MCP surfaces". #4013's
|
|
103
|
+
issue body said that and it is wrong for generic MCP. Three surfaces, three behaviours.
|
|
104
|
+
|
|
105
|
+
## Known limitation — relative targets have no declared base (#4023)
|
|
106
|
+
|
|
107
|
+
`existingAncestorDir` resolves the write target with `resolve(targetPath)` and no base argument, so
|
|
108
|
+
a **relative** target is canonicalized against the hook **process** working directory rather than
|
|
109
|
+
the payload root or a host-supplied cwd. The same lexical target can therefore land in different
|
|
110
|
+
rows of the first table depending only on where the hook runs. Symlink and Windows-junction targets
|
|
111
|
+
shift the class the same way: a junction into a linked worktree admits that worktree, a junction
|
|
112
|
+
into a non-repository directory falls back to the payload root.
|
|
113
|
+
|
|
114
|
+
Recorded, not fixed by #4013. Canonicalization changes admission classification, so it needs its own
|
|
115
|
+
arc: **#4023**.
|
|
116
|
+
|
|
117
|
+
## What a narrowing would have to define
|
|
118
|
+
|
|
119
|
+
A narrowing is refused *as filed*, not forever. A viable successor design would:
|
|
120
|
+
|
|
121
|
+
1. Name a target-class lattice: same admitted worktree, same-repository other worktree, foreign
|
|
122
|
+
repository, unproven identity, proven no-toplevel.
|
|
123
|
+
2. State the combining rule for every mixed target set, so `admitMutationTargetSet` still yields one
|
|
124
|
+
answer.
|
|
125
|
+
3. Keep the `foreign-repository`, `unproven-identity` and `worktree-span` refusals ahead of any
|
|
126
|
+
exemption.
|
|
127
|
+
4. State, per gate in the table above, whether the exemption changes that gate — including whether a
|
|
128
|
+
no-toplevel write may refresh any worktree lease, and whose ritual satisfies it.
|
|
129
|
+
5. Canonicalize targets first (#4023).
|
|
130
|
+
|
|
131
|
+
None of that would make #4013's filed premise — that the fallback is a defect — true.
|
|
132
|
+
|
|
133
|
+
## Scope of this guarantee
|
|
134
|
+
|
|
135
|
+
Root admission is **cooperative host-session routing**, the same posture as the occupancy lease it
|
|
136
|
+
selects: hook payloads and local session ids are forgeable by a same-user process. It bounds
|
|
137
|
+
careless cross-tree writes; it is not an authentication boundary against an adversarial agent. See
|
|
138
|
+
[`contracts/path-write-fence.md`](../contracts/path-write-fence.md) for the same limit stated for
|
|
139
|
+
the write fence.
|
|
140
|
+
|
|
141
|
+
## Code
|
|
142
|
+
|
|
143
|
+
| Concern | Where |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| Root admission, single target and target set | `packages/core/src/hooks/dispatcher.ts` — `admitEffectiveHookRoot`, `admitMutationTargetSet` |
|
|
146
|
+
| Which gate reads which root | `packages/core/src/hooks/dispatcher.ts` — `inspectMutationGates` header comment |
|
|
147
|
+
| Nearest existing ancestor, toplevel and common-dir lookups | `packages/core/src/session/git.ts` — `existingAncestorDir`, `worktreePathOrNull`, `gitCommonDir` |
|
|
148
|
+
| Shell write-dest classification and the in-repo predicate | `packages/core/src/hooks/shell-write-targets.ts` — `isInRepoShellWritePath` |
|
|
149
|
+
| Tool-name surfaces | `packages/core/src/hooks/tools.ts` — `isDirectWriteTool`, `isShellTool`, `isMcpTool` |
|
|
150
|
+
| Behaviour lock | `packages/core/src/hooks/dispatcher-effective-root.test.ts` |
|
|
@@ -116,6 +116,8 @@ Where the host supports hooks, **deny writes to directive-managed paths** withou
|
|
|
116
116
|
| **Tier-1 host hooks** | PreToolUse / equivalent intercept Write/Edit/Shell that target managed paths (AGENTS managed section, deposit, pin skill paths the host exposes) |
|
|
117
117
|
| **Path write fence** | Project + story `file_scope` evaluation — [`path-write-fence.md`](../contracts/path-write-fence.md) |
|
|
118
118
|
| **Agent hook readiness** | `deft verify:hooks-installed --scope=agent --live` — [`agent-hook-readiness.md`](../contracts/agent-hook-readiness.md) |
|
|
119
|
+
| **Host tool-surface coverage** | Which tool names each host's matcher actually selects, and which are recorded out of scope — [`host-tool-surface-audit.md`](./host-tool-surface-audit.md) (#3987) |
|
|
120
|
+
| **Root admission** | Which working tree a gated write is judged against, gate by gate, and which surfaces consult it — [`hook-root-admission.md`](./hook-root-admission.md) (#3794 / #4013) |
|
|
119
121
|
| **Policy** | `plan.policy.hostHooks.<host>` — inspect with `deft policy:show --field=hostHooks` |
|
|
120
122
|
|
|
121
123
|
! When hooks are available, treat managed-path deny as the enforcement home for constitution bytes — not agent good behavior alone.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Host tool-surface audit (#3987)
|
|
2
|
+
|
|
3
|
+
The PreToolUse write gate only runs on tool names the host's deposited matcher
|
|
4
|
+
selects. A name nobody listed is not a permissive policy — it is a gate that
|
|
5
|
+
never executes. #3987 was exactly that: Grok Build's shell tool
|
|
6
|
+
`run_terminal_command` was in no matcher, so a write the gate refused could be
|
|
7
|
+
reissued through the shell and land unobserved.
|
|
8
|
+
|
|
9
|
+
This file is the recorded answer to acceptance item 2: **for every tool name a
|
|
10
|
+
supported host emits, either the deposited matcher covers it, or there is a
|
|
11
|
+
written reason it stays out of scope.** The machine-readable form is
|
|
12
|
+
`HOST_TOOL_SURFACE_AUDIT` in `packages/core/src/hooks/tools.ts`, and
|
|
13
|
+
`deft verify:hooks-installed --scope=agent` fails closed on any silence in it.
|
|
14
|
+
|
|
15
|
+
Legend (RFC2119): `!`=MUST, `~`=SHOULD, `⊗`=MUST NOT.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Two layers that can disagree
|
|
20
|
+
|
|
21
|
+
| Layer | Form | Where |
|
|
22
|
+
|-------|------|-------|
|
|
23
|
+
| Deposited matcher | literal `a\|b\|c` alternation the host matches against | `.claude/settings.json`, `.grok/hooks/deft.json`, `.cursor/hooks.json`, `.codex/hooks.json` |
|
|
24
|
+
| Runtime classifier | lowercases and strips non-alphanumerics, then set-membership | `isDirectWriteTool` / `isShellTool` / `isSpawnTool` |
|
|
25
|
+
|
|
26
|
+
Both read the same constants, so they normally agree — but a name can be
|
|
27
|
+
present in one and useless in the other. A matcher entry the classifier does not
|
|
28
|
+
recognize invokes the hook and then falls through to `not-direct-write`; a
|
|
29
|
+
classifier entry the deposit omits is never reached at all. The coverage check
|
|
30
|
+
asserts **both** for every catalogued name, so a fix has to say which layer it
|
|
31
|
+
changed.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Grok Build — fully observed
|
|
36
|
+
|
|
37
|
+
Source: this host's published tool list, plus the 5,354-call session census
|
|
38
|
+
recorded on [#3987](https://github.com/deftai/directive/issues/3987).
|
|
39
|
+
|
|
40
|
+
| Tool | Disposition |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `write` | covered — direct write |
|
|
43
|
+
| `search_replace` | covered — direct write |
|
|
44
|
+
| `run_terminal_command` | covered — shell (#3990) |
|
|
45
|
+
| `monitor` | covered — shell; it runs an arbitrary background shell command, so omitting it reproduces the #3987 gap one tool over |
|
|
46
|
+
| `spawn_subagent` | covered — spawn |
|
|
47
|
+
| `read_file`, `grep`, `list_dir`, `search_tool`, `web_search`, `web_fetch` | out of scope — read |
|
|
48
|
+
| `todo_write` | out of scope — session-local non-product scratch |
|
|
49
|
+
| `get_command_or_subagent_output`, `wait_commands_or_subagents` | out of scope — poll over already-dispatched work |
|
|
50
|
+
| `kill_command_or_subagent` | out of scope — process control |
|
|
51
|
+
| `scheduler_delete`, `scheduler_list` | out of scope — scheduler control and read; mutate no product path |
|
|
52
|
+
| `enter_plan_mode`, `exit_plan_mode` | out of scope — session posture |
|
|
53
|
+
| `image_gen`, `image_edit`, `image_to_video`, `reference_to_video` | out of scope — generated media lands in session scratch, never a tracked product path |
|
|
54
|
+
| `scheduler_create` | **known gap, not covered** — see below |
|
|
55
|
+
| `use_tool` | **known gap, not covered** — see below |
|
|
56
|
+
|
|
57
|
+
### `scheduler_create` — spawn-class, needs a policy decision
|
|
58
|
+
|
|
59
|
+
It dispatches a background subagent on an interval, which is spawn-class by the
|
|
60
|
+
same reading that puts `spawn_subagent` in the matcher. Covering it routes a
|
|
61
|
+
scheduling primitive through the full spawn stack (session ritual plus an active
|
|
62
|
+
xBRIEF), which denies a shape that works today — a new deny class, not a
|
|
63
|
+
coverage repair. That is a deliberate policy call and belongs in its own change,
|
|
64
|
+
not in a matcher edit.
|
|
65
|
+
|
|
66
|
+
### `use_tool` — mcp-class, needs a classifier change
|
|
67
|
+
|
|
68
|
+
`use_tool` is a proxy: the MCP tool actually invoked is nested in
|
|
69
|
+
`tool_input.tool_name`, and the dispatcher classifies on the outer name only. A
|
|
70
|
+
matcher entry alone would buy a hook invocation and no enforcement, because
|
|
71
|
+
`isMcpTool("use_tool")` is false and `classifyMcpTool` returns null. Reading the
|
|
72
|
+
inner name is a classifier change with its own untrusted-input surface.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Claude, Codex, Cursor — partly or wholly unobserved
|
|
77
|
+
|
|
78
|
+
The deposit asserting a spelling is **not** evidence that the host emits it. All
|
|
79
|
+
four deposits carry the identical shell matcher, which is a matcher-string fact,
|
|
80
|
+
not a coverage fact ([#3987 comment 5471374558](https://github.com/deftai/directive/issues/3987#issuecomment-5471374558) F8).
|
|
81
|
+
|
|
82
|
+
| Host | Established | Unobserved |
|
|
83
|
+
|------|-------------|------------|
|
|
84
|
+
| Claude Code | shell `Bash` | direct-write and spawn spellings |
|
|
85
|
+
| Codex | shell `shell`; `apply_patch` write form (#3614) | everything else |
|
|
86
|
+
| Cursor | nothing | the whole surface |
|
|
87
|
+
|
|
88
|
+
Cursor is the one that matters: nothing in this tree observes which tool names
|
|
89
|
+
it emits on `preToolUse`. The fixture corpus asserts the framework's own
|
|
90
|
+
assumption, and `agent-hooks.test.ts` asserts the deposit contains a string.
|
|
91
|
+
Neither observes the host. Cursor could be a second zero-coverage host by the
|
|
92
|
+
exact mechanism that produced #3987, and the evidence available here would not
|
|
93
|
+
distinguish that from working coverage.
|
|
94
|
+
|
|
95
|
+
! Closing these requires an **observed** PreToolUse payload per host, not a
|
|
96
|
+
re-reading of the deposit.
|
|
97
|
+
|
|
98
|
+
⊗ Fill a host's catalog from the deposit, the matcher constants, or the fixture
|
|
99
|
+
corpus. That makes the check assert its own input and hides the gap it exists
|
|
100
|
+
to find.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The check
|
|
105
|
+
|
|
106
|
+
`deft verify:hooks-installed --scope=agent` (also run by `deft doctor`) fails
|
|
107
|
+
closed on four silences:
|
|
108
|
+
|
|
109
|
+
| Finding | Meaning |
|
|
110
|
+
|---------|---------|
|
|
111
|
+
| `missing-audit` | a supported host has no entry here at all — the "new host drops out of coverage" case |
|
|
112
|
+
| `uncovered-tool` | a catalogued mutation name is absent from every **deposited** matcher — the "renamed tool" case, read from the file rather than regenerated |
|
|
113
|
+
| `unclassified-tool` | a catalogued name the runtime classifier does not place in the same group |
|
|
114
|
+
| `unexplained-entry` | an out-of-scope entry with no reason, a host claiming full observation while naming no mutation tool, or a non-mutation entry the classifier actually gates |
|
|
115
|
+
|
|
116
|
+
A hand-edited deposit reads as **stale** first, and `deft update` is the remedy
|
|
117
|
+
for that; the coverage message is for gaps a current deposit still leaves.
|
|
118
|
+
|
|
119
|
+
Implementation: `packages/core/src/init-deposit/host-tool-coverage.ts`.
|
|
120
|
+
Catalog: `packages/core/src/hooks/tools.ts`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Why coverage alone did not close #3987
|
|
125
|
+
|
|
126
|
+
Matcher coverage is necessary and not sufficient — all three seats of the #3987
|
|
127
|
+
panel agreed. Coverage closes the reissue bypass. It does not keep the occupancy
|
|
128
|
+
lease alive, because the lease renews on a **gated write**, and eligibility
|
|
129
|
+
keeps almost all real shell traffic off that path: five recognized write verbs,
|
|
130
|
+
no dest carrying `$` / `*` / `?`, nothing out of root or in OS temp, and any
|
|
131
|
+
compound command refused before the gate. `cd <root>; <command>` is compound,
|
|
132
|
+
and the mandated Windows body-file flow (#2646 / #2744) fails all three
|
|
133
|
+
conditions at once.
|
|
134
|
+
|
|
135
|
+
The renewal half is `restampOwnerLivenessOnHookEvent`
|
|
136
|
+
(`packages/core/src/hooks/owner-liveness.ts`): the hook already resolves the
|
|
137
|
+
owner from the host payload, so a matched tool call renews the lease even when
|
|
138
|
+
it writes nothing. It is bounded to a host-authoritative owner, keeps
|
|
139
|
+
`markWrite` false, and never touches `claimed_at`, so `OCCUPANCY_MAX_LEASE_MS`
|
|
140
|
+
still ends the lease at 12 hours. It renews the tree the mutation gates
|
|
141
|
+
authorized against — a linked worktree under `.deft-scratch/`, not the payload
|
|
142
|
+
root — so a worktree write does not keep the primary checkout's lease alive
|
|
143
|
+
while the tree actually in use expires.
|
|
144
|
+
|
|
145
|
+
! A call with **no write target** proves only the tree the host named.
|
|
146
|
+
`projectRootFromHookPayload` takes that from the payload's own `cwd`-class
|
|
147
|
+
fields, so a session working inside a worktree names the worktree and renews
|
|
148
|
+
it. When the host reports a root that is not the tree the work is in, and no
|
|
149
|
+
write target says otherwise, nothing is renewed. That is deliberate: renewing a
|
|
150
|
+
guessed tree would keep a lease alive for a tree nobody occupies, and reclaiming
|
|
151
|
+
an unused tree is what the TTL is for. `deft occupancy:heartbeat --session-id
|
|
152
|
+
<owner>` remains the explicit path for a long, quiet session.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Cross-links
|
|
157
|
+
|
|
158
|
+
| Topic | Where |
|
|
159
|
+
|-------|-------|
|
|
160
|
+
| Gate integrity (do not clear red by editing the gate) | [`gate-integrity.md`](./gate-integrity.md) |
|
|
161
|
+
| Host honesty limits | [`host-surface-assumptions.md`](./host-surface-assumptions.md) |
|
|
162
|
+
| Agent hook readiness | [`agent-hook-readiness.md`](../contracts/agent-hook-readiness.md) |
|
|
163
|
+
| Occupancy lease and its bounds | `packages/core/src/session/occupancy.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-content",
|
|
3
|
-
"version": "0.109.
|
|
3
|
+
"version": "0.109.1",
|
|
4
4
|
"deftConsumerDeposit": true,
|
|
5
5
|
"description": "Shippable Directive framework content in the consumer .deft/core/ layout (C1 flatten), plus the engine surfaces (.githooks/, Taskfile.yml, tasks/) the deposit wires. Python-free per #2022 Phase 3. Refs #11, #1669, #1967.",
|
|
6
6
|
"license": "MIT",
|