@deftai/directive-content 0.106.0 → 0.108.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.
Files changed (46) hide show
  1. package/Taskfile.yml +14 -1
  2. package/UPGRADING.md +24 -5
  3. package/commands.md +29 -4
  4. package/contracts/design-critique.md +369 -14
  5. package/contracts/issue-eval.md +77 -0
  6. package/contracts/path-write-fence.md +126 -1
  7. package/contracts/runtime-authority.md +2 -0
  8. package/contracts/scm-readiness.md +2 -2
  9. package/docs/delivery-attempt.md +2 -1
  10. package/docs/freshness-contract.md +6 -1
  11. package/docs/getting-started.md +10 -11
  12. package/docs/hook-runtime-unavailable.md +54 -0
  13. package/docs/orphan-active-verdict-basis.md +166 -0
  14. package/docs/scope-provenance.md +1 -1
  15. package/package.json +1 -1
  16. package/packs/skills/skills-pack-0.1.json +24 -10
  17. package/scm/github.md +65 -2
  18. package/skills/deft-directive-build/SKILL.md +2 -2
  19. package/skills/deft-directive-cost/SKILL.md +7 -11
  20. package/skills/deft-directive-design-critique/SKILL.md +22 -6
  21. package/skills/deft-directive-design-critique/references/motion-shape.md +19 -0
  22. package/skills/deft-directive-feedback/SKILL.md +11 -2
  23. package/skills/deft-directive-interview/SKILL.md +10 -10
  24. package/skills/deft-directive-issue-eval/SKILL.md +48 -0
  25. package/skills/deft-directive-release/SKILL.md +10 -6
  26. package/skills/deft-directive-review-cycle/SKILL.md +33 -0
  27. package/skills/deft-directive-setup/SKILL.md +53 -22
  28. package/skills/deft-directive-swarm/references/core-ops.md +4 -0
  29. package/skills/deft-directive-swarm/references/core-phase-1-2.md +1 -1
  30. package/skills/deft-directive-swarm/references/core-phase-3.md +3 -1
  31. package/skills/deft-directive-swarm/references/core-phase-4.md +11 -8
  32. package/skills/deft-directive-swarm/references/host-cursor.md +1 -0
  33. package/skills/deft-directive-swarm/references/host-grok-build.md +1 -0
  34. package/skills/deft-directive-triage/SKILL.md +3 -2
  35. package/tasks/engine.yml +4 -0
  36. package/tasks/feedback.yml +1 -1
  37. package/tasks/occupancy.yml +34 -1
  38. package/tasks/prd.yml +4 -5
  39. package/tasks/scm.yml +14 -2
  40. package/tasks/session.yml +13 -2
  41. package/tasks/toolchain.yml +2 -2
  42. package/tasks/triage-evaluate.yml +22 -0
  43. package/tasks/verify.yml +21 -1
  44. package/templates/agent-prompt-preamble.md +28 -4
  45. package/templates/agents-entry.md +10 -5
  46. package/templates/design-critique-brief.md +19 -5
@@ -52,7 +52,132 @@ path when `inspectActiveScope` reports one. Residual gaps (document, not silent)
52
52
  - Story JSON unreadable → story layer fail-open; project fence still applies
53
53
 
54
54
  Shell/MCP push/merge scopes remain project-only (`runtimeAuthority.scopes`); they are not
55
- re-scoped by `file_scope`.
55
+ re-scoped by `file_scope`. Recognized Shell dest-forms (`git checkout --`, `git restore`,
56
+ `rm`/`rmdir`) use the same write fence as Edit/Write, including story `file_scope` (#3438).
57
+
58
+ ### Dest-form enforcement is opt-in (#3438 / #3594)
59
+
60
+ ```jsonc
61
+ // xbrief/PROJECT-DEFINITION.xbrief.json
62
+ { "plan": { "policy": { "runtimeAuthority": {
63
+ "shellDestForms": "off" // default — Shell exactly as before #3438
64
+ // "shellDestForms": "enforce" // opt in
65
+ } } } }
66
+ ```
67
+
68
+ `off` is the default and leaves Shell mutations unrecognized and fail-open, as they were before
69
+ this gate existed, so landing the classifier denies nothing a consumer runs today. `enforce`
70
+ turns on **both** halves together: recognized dest-forms go through `inspectMutationGates`, and
71
+ targets that cannot be proved fail closed.
72
+
73
+ - ⊗ Do not split the two halves behind separate switches. Enforcing only resolved dests would
74
+ allow `cd x && rm y` while denying `rm x/y`; enforcing only the fail-closed branch would deny
75
+ the compound while letting the in-scope simple form through unchecked.
76
+ - Independent of `enabled` in both directions: opting into the gate does not require the
77
+ `runtimeAuthority` grant ladder, and enabling the ladder does not silently opt into the gate.
78
+ - An unknown value (`"warn"`, `"on"`, a typo) resolves to `off` — the no-new-denials direction —
79
+ and `validateRuntimeAuthority` reports it, so it is never silent.
80
+ - An unreadable policy also resolves to `off` rather than failing closed.
81
+ - Tracked project policy may only **enable** this gate. A tracked switch that *disabled* it would
82
+ contradict `policy/deft-directive-disable.ts`, where repository-controlled content must not
83
+ disable hooks for downstream clones.
84
+
85
+ ⊗ There is no `warn` state. Its only purpose would be staging a breaking change, and with `off`
86
+ as the default there is nothing to stage. It is also unimplementable today: `renderHostDecision`
87
+ emits no text on the allow path for `tool.before`, so a warned denial would be
88
+ indistinguishable from `git status` in the decision record. Revisit only alongside an allow-path
89
+ sink (#3620).
90
+
91
+ ### Dest-form threat model (#3438) — read this first
92
+
93
+ The Shell dest-form gate is a **guardrail for cooperative-but-careless agents, not a security
94
+ boundary against adversarial ones.** An agent that wants out of the fence has unbounded exits
95
+ and this layer cannot close them. State that plainly before reading the rules below, because
96
+ every rule is scoped by it.
97
+
98
+ Why the limit is structural: Edit/Write payloads are **declarative** — the target path is data
99
+ in the payload, so gating them is sound. Shell payloads are **imperative** — the target is the
100
+ output of running a program, so gating them by parsing the command string means predicting what
101
+ a program will do without running it. Recognition of *destructive spellings* is decidable;
102
+ prediction of *mutation* is not.
103
+
104
+ What that means concretely — all of these are **fail-open today**:
105
+
106
+ - Unrecognized mutators: `git reset --hard`, `git clean -fd`, `git stash drop`, `git checkout`
107
+ without `--`, `mv`, `cp`, `sed -i`, `truncate`, `find -delete`, and `>` / `>>` redirection
108
+ - Interpreters: `bash -c 'rm x'`, `python -c`, `node -e`, `cmd /c`
109
+ - Non-literal verbs: `\rm x`, `rm${IFS}x` — the tokenizer cannot see the verb, so even the
110
+ fail-closed branch does not fire
111
+ - **cmd / PowerShell mutators are not recognized at all**: `del`, `erase`, `rd`, `move`,
112
+ `copy /y`, `Remove-Item`, `Out-File`. Only POSIX-shaped verbs are on the list, and the hook
113
+ cannot tell which shell will run the command (#3624)
114
+ - Mutations by allowed programs: `npm run build`, `node scripts/clean.js`, `make` — inherent
115
+ to any string recognizer, since writing files is what those commands are *for*
116
+ - **Nothing on the allow path is audited**, so a bypass currently leaves no trace
117
+
118
+ Do not describe this gate as closing the Bash bypass. It raises the floor on the four
119
+ recognized verbs in simple commands. The bypass class remains open.
120
+
121
+ ### Dest-form target recognition (#3438)
122
+
123
+ The fence resolves a target for exactly one shape: **a single simple command**. Everything
124
+ else that is *recognized* is denied rather than resolved. An **absolute** dest is checked
125
+ soundly; a **relative** dest is checked under the assumption that the shell's working
126
+ directory is the project root, which persistent-shell hosts do not guarantee across tool
127
+ calls (see the cwd residual below).
128
+
129
+ A command is simple when it has no unquoted `&&`, `||`, `|`, `&`, `;`, or newline, no
130
+ grouping or substitution (`(`, `)`, `{`, `}`, `` ` ``, `$`), and no git context option. Then
131
+ each dest token is checked against the same fence as Edit/Write.
132
+
133
+ Everything else **fails closed** — denied regardless of whether the path would have been in
134
+ scope:
135
+
136
+ | Fail-closed | Why |
137
+ | --- | --- |
138
+ | Any compound command (`cd x && rm y`, pipelines, `;`, `&`) | cwd is not provable |
139
+ | Grouping / substitution (`(…)`, `{…;}`, `$(…)`, backticks) | target is computed at runtime |
140
+ | Git context options (`-C`, `--work-tree`, `--git-dir`, `-c core.workTree`, `--config-env`, `GIT_WORK_TREE=`, `GIT_DIR=`) | relocates the tree; resolution depends on the git dir |
141
+ | Glob / variable dests, or a leading `~` | expands at runtime (a *trailing* `~` as in `foo.ts~` is an ordinary path) |
142
+ | A **retained** backslash — one not consumed as an escape (`rm C:\Repos\a.ts`, `rm foo\bar`) | dialect-ambiguous: a path separator on win32, an escape under a POSIX shell including Git Bash *on* win32, and the payload does not say which shell runs. Rewrite with forward slashes, which git and node accept on Windows (#3624) |
143
+ | `git checkout\|restore --pathspec-from-file=<f>` / `--pathspec-file-nul` | the targets live inside a file; reading it means hook-time I/O plus resolving against an unknown cwd (#3624) |
144
+
145
+ ⊗ **Do not add cwd or git-context reconstruction back.** It was implemented and withdrawn
146
+ (#3438): the target depends on operator precedence (`&` binds looser than `&&`, which binds
147
+ looser than `|`), on exit status (`cd x || …` runs only when the `cd` failed), on subshell
148
+ boundaries, and on git config — and every resolution rule added produced its own fence
149
+ bypass. Recognition of a *legible* verb is cheap; resolution was not. Neither is total —
150
+ see the threat model above.
151
+
152
+ Rewrite guidance the deny message carries: name a concrete path in one simple command
153
+ (`rm x/y`, not `cd x && rm y`), or issue one command per tool call. Prefer an **absolute**
154
+ path: absolute dests are checked soundly, relative ones assume the shell is at the project
155
+ root.
156
+
157
+ **Cwd residual:** the classifier never consults the shell's working directory (`input.cwd`
158
+ only supplies project-root candidates). A relative dest is resolved against the project root
159
+ unconditionally, so whenever the shell's cwd differs — including a benign in-project `cd` in
160
+ an earlier tool call — the fence checks a different path from the one mutated. Absolute dests
161
+ are unaffected. Tracked in #3594.
162
+
163
+ **Cost of the narrowing, accepted deliberately:** legitimate compound commands are denied,
164
+ with the rewrite above. Cross-repo work has an escape: an absolute out-of-root dest is
165
+ allowed, so `cd /other/repo` then `git checkout -- /other/repo/f.ts` works where
166
+ `git -C /other/repo checkout -- f.ts` is denied. Quoting is honoured (an unquoted backslash
167
+ escapes only a character that needs escaping, so `rm protected\ file` is ONE dest while
168
+ `C:\Repos\file.ts` keeps its separators; `rm\ secret` is one word naming a nonexistent
169
+ program and is correctly not a dest-form).
170
+
171
+ **The fail-closed branch reaches no exemptions.** Because it never calls
172
+ `inspectMutationGates`, assist/scratch, proposed-lifecycle, and story `file_scope` do not
173
+ apply to it: `rm .deft-scratch/a.txt` is allowed under assist posture but
174
+ `rm .deft-scratch/a.txt && rm .deft-scratch/b.txt` is denied. Split the calls. This is
175
+ structural — a fail-closed dest has no path, so a path-conditional exemption cannot be
176
+ evaluated.
177
+
178
+ **Known-open — recognition, not resolution:** `python -c`, `cmd /c copy`, and obfuscated
179
+ `bash -c 'rm …'` are not recognized as dest-forms at all, so they stay fail-open. Narrowing
180
+ bounds what resolution can get wrong; it does not close the recognition gap.
56
181
 
57
182
  ## Skill behavior (build / swarm)
58
183
 
@@ -46,6 +46,8 @@ When `enabled: true`:
46
46
  | **MCP merge** | tool names matching `merge_pull_request`, `pr_merge`, … | `scopes.merge` |
47
47
  | **MCP push** | tool names matching `git_push`, `push_branch`, … | `scopes.push` |
48
48
 
49
+ Product dest-forms (`git checkout --`, `git restore`, `rm`/`rmdir`) are a separate PreToolUse slice (#3438) and are not classified here as push or merge.
50
+
49
51
  **Fail open (allow)** when:
50
52
 
51
53
  - the tool is Shell/MCP but the command/tool name is **not** classifiable as push or merge (e.g. `git status`, unrelated MCP tools)
@@ -37,8 +37,8 @@ on interactive auth prompts in headless envs without a clear diagnostic.
37
37
  | --- | --- | --- |
38
38
  | `session:start` default | shallow (PATH + token + `gh auth status`) | no |
39
39
  | `session:start --with-network` | deep (API + optional repo) | no |
40
- | `deft scm:status` | shallow default; `--deep` opt-in | n/a (exit 0/1/2) |
41
- | `deft github-auth-modes` | mode validation (#1557) | n/a |
40
+ | `deft scm:status` | shallow default; `--deep` opt-in (derives target repo; expected user login via flags/env) | n/a (exit 0/1/2) |
41
+ | `deft github-auth-modes` | mode + principal validation (#1557 / #3665) | n/a |
42
42
 
43
43
  JSON field shape (`session:start --json` → `scm`, or `scm:status --json`):
44
44
 
@@ -102,7 +102,8 @@ task swarm:pre-dispatch -- --scope-id <story|issue|xbrief-id> --target-id <workt
102
102
  Default unit key: `scopeId` + `targetId` + `workflowId=drive-to:merge-ready`.
103
103
  Actions: `begin` (default), `complete` (`--status succeeded|failed|cancelled|blocked`), `cancel` (takeover step 1).
104
104
  Monitors MUST run begin before any peer implement spawn; spawn only on exit 0.
105
- Takeover: cancel prior attempt, then begin again — never concurrent dual active.
105
+ Takeover after REDISPATCH_OK: cancel prior attempt, then begin again — never concurrent dual active.
106
+ A killed worker stays `queued`/`running` until cancel; REDISPATCH_OK does not lift `DENY_DUPLICATE_ACTIVE` (#3730).
106
107
  Implementation: `packages/core/src/swarm/pre-dispatch.ts` + `task swarm:pre-dispatch`.
107
108
 
108
109
  ## Skill routing
@@ -35,10 +35,15 @@ When a mutation `session:start` (cold or re-arm) loads payload surfaces into
35
35
  runtime context, it binds the live generation:
36
36
 
37
37
  ```text
38
- .deft/session-binds/<safe-session-id>.json # multi-agent isolation (preferred)
38
+ .deft/session-binds/<sha256-slice>.json # multi-agent isolation (preferred)
39
39
  .deft/session-bind.json # default / last-bind convenience
40
40
  ```
41
41
 
42
+ The per-session record is named by a 24-hex SHA-256 slice of the session id, so
43
+ a directory listing carries no fragment of the id (#3768). Records written
44
+ before that rename also carried a 32-character id prefix; they are read-accepted
45
+ until the next bind supersedes them.
46
+
42
47
  Multi-agent hosts **must** pin session identity so one session cannot certify
43
48
  another as current:
44
49
 
@@ -16,21 +16,18 @@ Before the mechanics below, it helps to see the whole loop. Directive is two con
16
16
 
17
17
  ## Prerequisites
18
18
 
19
- - **Node 20+** and **pnpm** for live gates and the npm distribution channel (see `.nvmrc` in the framework payload).
19
+ - **Node 20+** and the package manager declared by the project (`npm` or `pnpm`) for live consumer gates and the npm distribution channel (see `.nvmrc` in the framework payload). npm is bundled with Node; a pnpm-pinned project enables pnpm through Corepack.
20
+ - **Git** and **GitHub CLI (`gh`)** are also checked by the consumer toolchain gate.
20
21
  - **Go 1.22+** only if you use the bootstrap Go installer or build from source.
21
- - **Python 3.11+**, **uv**, **task**, **git**, and **gh** for full framework workflows run `directive toolchain:check` (or `deft toolchain:check`) after install.
22
+ - **Python 3.11+**, **uv**, and **task** for the framework-maintainer and session workflows that name them. The consumer toolchain probe itself does not check those tools. Run `directive toolchain:check --consumer --project-root .` after install.
22
23
 
23
24
  ---
24
25
 
25
26
  ## Installation
26
27
 
27
- ### npm (coming soon — not yet published)
28
+ ### npm (canonical)
28
29
 
29
- > ⚠️ **The `@deftai/directive` npm package is not yet published** to the registry (provisioning tracked by [#1909](https://github.com/deftai/directive/issues/1909)). **Until it goes live, install via the [Go installer](#go-installer-bootstrap) below.** The commands shown here are what the install will become once the package is published.
30
-
31
- <!-- TODO(#1909): flip to npm-canonical and remove this "coming soon" notice when @deftai/directive is published -->
32
-
33
- Once published, when Node is already available you will install Directive globally:
30
+ With Node already available, install Directive globally from npm:
34
31
 
35
32
  ```bash
36
33
  npm i -g @deftai/directive
@@ -38,16 +35,18 @@ directive --version # primary command
38
35
  deft --version # alias — same binary
39
36
  ```
40
37
 
41
- One-shot without a global install (also once published):
38
+ One-shot without a global install:
42
39
 
43
40
  ```bash
44
41
  npx @deftai/directive doctor
45
42
  npx @deftai/directive session:start
46
43
  ```
47
44
 
48
- This npm path is the emerging primary distribution channel under `@deftai/directive` ([#11](https://github.com/deftai/directive/issues/11)); the Go installer below remains the install/bootstrap option today and during the staged retire window.
45
+ This npm path has been the canonical distribution channel since v0.55.1. A pnpm-managed repository can install the same package with `pnpm add -g @deftai/directive` or project-locally with `pnpm add -D @deftai/directive`.
46
+
47
+ ### Go installer (legacy bridge)
49
48
 
50
- ### Go installer (bootstrap)
49
+ The Go installer is a frozen legacy bridge for older installs and source-oriented recovery. New consumer installs should use npm above; see [UPGRADING.md](../UPGRADING.md#one-time-migration-from-the-go-installer-legacy--npm) when migrating an existing Go-installer layout.
51
50
 
52
51
  Download a platform installer from the [Directive release page](https://github.com/deftai/directive/releases) and run it from the project you want to adopt:
53
52
 
@@ -0,0 +1,54 @@
1
+ # Hook runtime unavailable: `deft-hook` is not on PATH
2
+
3
+ On a host that cannot execute `deft-hook`, the Cursor `preToolUse` registration is `failClosed: true`, so **every mutation is denied** — and because the binary never runs, no Directive code is left to say why. The visible symptom is an opaque non-zero exit, typically **127** (command not found).
4
+
5
+ Tracker: [#3785](https://github.com/deftai/directive/issues/3785). Related: [#3736](https://github.com/deftai/directive/issues/3736) (timeout), [#3571](https://github.com/deftai/directive/issues/3571) (unused-host recovery), [#2752](https://github.com/deftai/directive/issues/2752) (per-host toggles).
6
+
7
+ ## Who hits this
8
+
9
+ The registration travels via git; the runtime does not. `.cursor/hooks.json` is trackable by design and the `.deft/core/` deposit is born-ignored, so any environment that gets the repo without a Node install of the CLI inherits the fence without the implementation:
10
+
11
+ - cloud agent VMs whose image has no Node and no global install
12
+ - CI runners and containers that never run `npm i -g @deftai/directive`
13
+ - a fresh clone on a new workstation
14
+
15
+ ## Recovery is out of band
16
+
17
+ There is no in-session escape. Run this from a shell where Node and the CLI **are** available — your workstation, or the repo before you push it:
18
+
19
+ ```bash
20
+ deft policy:disable-host-hooks --host cursor --confirm
21
+ ```
22
+
23
+ That sets `plan.policy.hostHooks.cursor = false` in `xbrief/PROJECT-DEFINITION.xbrief.json`, and the next `deft update` strips the Cursor registration. Commit and push; the locked-out environment clears on its next pull or fresh clone.
24
+
25
+ ⚠ Capability cost: disabling `hostHooks` for a host removes `deft-hook` pre-execution guardrails for anyone who later opens this repo in that host. The change is tracked and recorded to `meta/policy-changes.log`. Inspect with `deft policy:show --field=hostHooks`; reverse by setting the host back to `true` and running `deft update`.
26
+
27
+ ## Preferred fix: make the runtime travel with the registration
28
+
29
+ If Cursor is a host you rely on, restore the runtime rather than removing the fence. Either:
30
+
31
+ - commit a `package.json` dependency on `@deftai/directive` — a published version or another spec a clone can fetch, since a location spec (`file:`, `link:`, `../directive`, an absolute path) resolves only on the machine that wrote it — and run `npm install` in the image or clone, which puts `deft-hook` in `node_modules/.bin`; or
32
+ - add `npm i -g @deftai/directive` to the image build.
33
+
34
+ `deft init` and `deft update` warn when a hook registration travels with the repository — already committed, or merely untracked and not ignored, so the next `git add` carries it — while neither anchor is present. The warning names the affected file and this document.
35
+
36
+ ## ⊗ Do not hand-edit `failClosed` in the deposited hook file
37
+
38
+ Setting `failClosed: false` in `.cursor/hooks.json` clears the block exactly once. That file is a managed deposit: the next `deft update` rewrites the entries with `failClosed: true` and **silently re-arms the lockout**, usually long after anyone remembers editing it. Use the policy verb above, which is durable and tracked.
39
+
40
+ ## Why the flags do not help
41
+
42
+ | Escape hatch | Why it does not reach this failure |
43
+ |---|---|
44
+ | `.deft-directive-disable` | Evaluated *inside* `deft-hook`. When the binary is missing, nothing reads the flag. |
45
+ | `.no-deft-directive` | Consulted at `session.start` only; it never reaches `preToolUse`. |
46
+ | Host-side "skip hook" | A host concept. Directive has no such bypass. |
47
+
48
+ ## Absent, crashed, and timed out are one class
49
+
50
+ The hook exit contract already decouples the exit code from the verdict: exit `0` means a decision was **rendered** — allow *or* deny — so every non-zero exit is by construction a *non-decision*. Absence, a crash, and a host timeout kill are the same state, and all three stay fail-closed.
51
+
52
+ They stay fail-closed because fail-open-on-absence is a bypass primitive, not a lenience: removing the binary is an ordinary shell call, so treating absence as allow would convert a self-inflicted denial into a bypass of the write fence, the intent ceiling, the session ritual, and the occupancy lease ([#3156](https://github.com/deftai/directive/issues/3156)). What these cases need is legibility and an out-of-band escape — this page — not a relaxed fence.
53
+
54
+ For a timeout specifically, retry the gated ritual first when machine load is the likely cause; the Cursor `tool.before` budget is sized for a gated ritual plus live readiness, so a repeated timeout is a real signal rather than noise.
@@ -0,0 +1,166 @@
1
+ # `verify:orphan-active`: verdict basis and freshness (#3767)
2
+
3
+ `verify:orphan-active` decides whether an `xbrief/active/` brief with
4
+ `plan.status == running` is really still live work. Until #3767 it answered
5
+ that question from a triage-cache hit returned **unconditionally** — no age
6
+ bound, no re-validation. A cached `open` written twelve hours earlier beat
7
+ reality and suppressed the live read that would have corrected it, so the gate
8
+ exited 0 while scanning the very brief whose issue had already closed.
9
+
10
+ This document records what the gate now does, why, and what it still cannot
11
+ promise.
12
+
13
+ Legend (RFC2119): `!`=MUST, `~`=SHOULD, `⊗`=MUST NOT, `?`=MAY.
14
+
15
+ ---
16
+
17
+ ## Two modes, split by query shape
18
+
19
+ The gate has two callers with different obligations, so it has two mechanisms.
20
+
21
+ | | Scoped `--issue N` | Unscoped aggregate |
22
+ |---|---|---|
23
+ | Caller | after-merge DONE proof (#3429 / #3476) | `task check`, pre-commit, pre-push sweep |
24
+ | Mechanism | authoritative per-issue REST read | one complete open-issue inventory |
25
+ | N | 1 | one call regardless of brief count |
26
+ | Unknown state | **fail closed** (exit 1, `unresolved`) | **fail open** (exit 0, reported `unverified`) |
27
+ | Latency budget | 5 s | 15 s |
28
+
29
+ The unknown asymmetry is deliberate and predates this change. Scoped is a proof
30
+ obligation before a worker claims DONE about one named origin. The aggregate
31
+ sweep must not make offline work network-authorized — hard-failing every
32
+ aggregate unknown would break `--skip-gh`, offline runs, and fresh clones.
33
+
34
+ ⊗ Do not flatten the two unknown policies into one rule.
35
+
36
+ ---
37
+
38
+ ## The freshness choice
39
+
40
+ **Age bound plus re-validation, applied per mode.** Both options offered by
41
+ #3767 are used, because each mode needs a different one.
42
+
43
+ - A cache hit is honoured only when it is at most **15 minutes** old
44
+ (`ISSUE_CACHE_MAX_AGE_MS`). Older entries are not evidence.
45
+ - Scoped `--issue N` re-validates first: it takes the authoritative read and
46
+ falls back to the cache only inside the age bound, when the live read is
47
+ unavailable. Unknown after that is `unresolved`.
48
+ - The aggregate sweep resolves from the open-issue inventory when the network
49
+ is allowed, and uses an in-bound cache hit only under `--skip-gh` or when the
50
+ inventory itself is unavailable.
51
+
52
+ 15 minutes keeps a warm-cache offline run working while making an overnight
53
+ entry non-authoritative — the measured entry was roughly twelve hours old.
54
+
55
+ ---
56
+
57
+ ## The aggregate inventory
58
+
59
+ The sweep reuses `restIssueListOpenInventory` (#3752): one
60
+ `gh api --paginate --slurp repos/<owner>/<repo>/issues?state=open&per_page=100`
61
+ subprocess. It excludes pull-request rows and **fails closed** on command
62
+ failure, non-JSON output, a non-array payload, a malformed row, buffer
63
+ exhaustion, and the pagination cap.
64
+
65
+ ! The inventory MUST be complete. `probeCacheDrift`'s helper defaults to a
66
+ 1,000-item limit; reusing that capped set as a closed-state oracle would
67
+ misclassify open issue 1001+ in a larger repository.
68
+
69
+ Membership in a successful inventory means open. **Absence means "not open"**,
70
+ which is the direction that tells an operator to run `scope:complete` on what
71
+ may be live work — so absence is confirmed by one authoritative per-issue read
72
+ before the gate acts on it. That confirming result is reused for the rest of
73
+ the run when several briefs name the same issue. In the ordinary case
74
+ (everything open) that costs zero extra calls.
75
+
76
+ When the inventory is unavailable the gate reports `unverified` rather than
77
+ inferring closed. Fail-closed here means never manufacturing a false "closed",
78
+ not turning the sweep into a connectivity check.
79
+
80
+ ### Why not a live read per brief
81
+
82
+ Per-brief live reads are the mechanism #3752 removed. Measured: the inventory
83
+ is **4.2 s constant**, against **14.2 s** and **76.6 s** for sequential
84
+ per-brief reads at the WIP cap of 20 on two different hosts. Crossover is about
85
+ 5 briefs on `gh` and about 1 on `ghx`. `verify:orphan-active` sits in the
86
+ fast-preflight tier, where wall clock rather than REST quota is the constraint.
87
+
88
+ ---
89
+
90
+ ## Reported basis
91
+
92
+ Every run now says how it decided, so a verified pass is distinguishable from
93
+ an unverified one:
94
+
95
+ ```text
96
+ verify:orphan-active: no orphaned active/running xBRIEFs (scanned 3 running briefs in active/).
97
+ Basis: inventory 2, cache 1 (max age 4m).
98
+ ```
99
+
100
+ ```text
101
+ verify:orphan-active: no orphaned active/running xBRIEFs (scanned 1 running brief in active/).
102
+ Basis: unverified 1.
103
+ UNVERIFIED: state could not be established for the references below, so this run is
104
+ not evidence that they are unshipped:
105
+ - #8001 (open-issue inventory unavailable: gh api failed: ...)
106
+ ```
107
+
108
+ `EvaluateResult.basis` carries the same counts structurally
109
+ (`inventory`, `live`, `cache`, `unverified`, `maxCacheAgeMs`, `proxied`,
110
+ `elapsedMs`, `budgetMs`).
111
+
112
+ ⊗ Do not cite an exit 0 with `unverified > 0` as evidence that a tree is clean.
113
+
114
+ ---
115
+
116
+ ## The `ghx` caveat
117
+
118
+ "Live" is itself a cache. `defaultRunGh` resolves through `resolveBinary()`,
119
+ which prefers **`ghx`**, a cached read-only GET proxy whose age nothing in this
120
+ gate can inspect.
121
+
122
+ This gate therefore **pins plain `gh`** for its authoritative reads when `gh` is
123
+ on PATH. When only `ghx` is available the gate still runs, sets
124
+ `basis.proxied`, and prints:
125
+
126
+ ```text
127
+ Note: reads resolved through `ghx`, a cached GET proxy; freshness is bounded by that
128
+ proxy, which this gate cannot inspect (#3737).
129
+ ```
130
+
131
+ ⊗ Do not claim the gate detects a closed origin "regardless of cache age" while
132
+ `ghx` is in the path. Whether `resolveBinary` should prefer `ghx` at all is
133
+ [#3737](https://github.com/deftai/directive/issues/3737).
134
+
135
+ ---
136
+
137
+ ## Latency budgets
138
+
139
+ There were none before #3767, which is why "affordable" was unfalsifiable.
140
+
141
+ | Mode | Budget | Basis |
142
+ |---|---|---|
143
+ | Scoped `--issue N` | 5 s | one issue read plus at most one linked-PR read, measured ~0.75 s each |
144
+ | Unscoped aggregate | 15 s | one 4.2 s inventory plus confirming reads for apparent closes |
145
+
146
+ Exceeding a budget prints an advisory line and does **not** change the exit
147
+ code. Budget drift is a signal to re-measure the mechanism, not a new failure
148
+ mode for callers.
149
+
150
+ ---
151
+
152
+ ## Out of scope
153
+
154
+ - The offline crash when neither `gh` nor `ghx` is on PATH
155
+ ([#3774](https://github.com/deftai/directive/issues/3774)).
156
+ - Whether `resolveBinary` should prefer `ghx`
157
+ ([#3737](https://github.com/deftai/directive/issues/3737)).
158
+ - The `cache_fresh` forge-error fail-open decided on
159
+ [#3738](https://github.com/deftai/directive/issues/3738).
160
+
161
+ ## Related
162
+
163
+ - [#3429](https://github.com/deftai/directive/issues/3429) — the gate's contract
164
+ - [#3476](https://github.com/deftai/directive/issues/3476) — `verify:completed-tracked`
165
+ - [#3752](https://github.com/deftai/directive/issues/3752) — the open-inventory mechanism reused here
166
+ - [#3156](https://github.com/deftai/directive/issues/3156) — gate integrity; this was a deliberate gate-definition change
@@ -70,7 +70,7 @@ task scope:record-approved-scope -- xbrief/active/story.xbrief.json --actor scot
70
70
 
71
71
  Mint uses the shared #3110 human-presence gate (same module as `authz`):
72
72
 
73
- - Interactive TTY (stdin + stdout) and a controlling terminal (`/dev/tty` or `CONIN$`)
73
+ - Interactive TTY (stdin + stdout) and a controlling terminal (`/dev/tty` or `\\.\CONIN$`)
74
74
  - Explicit `--confirm`
75
75
  - Typed phrase `mint` on the controlling TTY
76
76
  - Agent/CI env markers (`AUTHZ_AGENT_SHELL_ENV_MARKERS`) refuse fail-closed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-content",
3
- "version": "0.106.0",
3
+ "version": "0.108.0",
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",