@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.
- package/Taskfile.yml +14 -1
- package/UPGRADING.md +24 -5
- package/commands.md +29 -4
- package/contracts/design-critique.md +369 -14
- package/contracts/issue-eval.md +77 -0
- package/contracts/path-write-fence.md +126 -1
- package/contracts/runtime-authority.md +2 -0
- package/contracts/scm-readiness.md +2 -2
- package/docs/delivery-attempt.md +2 -1
- package/docs/freshness-contract.md +6 -1
- package/docs/getting-started.md +10 -11
- package/docs/hook-runtime-unavailable.md +54 -0
- package/docs/orphan-active-verdict-basis.md +166 -0
- package/docs/scope-provenance.md +1 -1
- package/package.json +1 -1
- package/packs/skills/skills-pack-0.1.json +24 -10
- package/scm/github.md +65 -2
- package/skills/deft-directive-build/SKILL.md +2 -2
- package/skills/deft-directive-cost/SKILL.md +7 -11
- package/skills/deft-directive-design-critique/SKILL.md +22 -6
- package/skills/deft-directive-design-critique/references/motion-shape.md +19 -0
- package/skills/deft-directive-feedback/SKILL.md +11 -2
- package/skills/deft-directive-interview/SKILL.md +10 -10
- package/skills/deft-directive-issue-eval/SKILL.md +48 -0
- package/skills/deft-directive-release/SKILL.md +10 -6
- package/skills/deft-directive-review-cycle/SKILL.md +33 -0
- package/skills/deft-directive-setup/SKILL.md +53 -22
- package/skills/deft-directive-swarm/references/core-ops.md +4 -0
- package/skills/deft-directive-swarm/references/core-phase-1-2.md +1 -1
- package/skills/deft-directive-swarm/references/core-phase-3.md +3 -1
- package/skills/deft-directive-swarm/references/core-phase-4.md +11 -8
- package/skills/deft-directive-swarm/references/host-cursor.md +1 -0
- package/skills/deft-directive-swarm/references/host-grok-build.md +1 -0
- package/skills/deft-directive-triage/SKILL.md +3 -2
- package/tasks/engine.yml +4 -0
- package/tasks/feedback.yml +1 -1
- package/tasks/occupancy.yml +34 -1
- package/tasks/prd.yml +4 -5
- package/tasks/scm.yml +14 -2
- package/tasks/session.yml +13 -2
- package/tasks/toolchain.yml +2 -2
- package/tasks/triage-evaluate.yml +22 -0
- package/tasks/verify.yml +21 -1
- package/templates/agent-prompt-preamble.md +28 -4
- package/templates/agents-entry.md +10 -5
- 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
|
|
package/docs/delivery-attempt.md
CHANGED
|
@@ -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/<
|
|
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
|
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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**,
|
|
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 (
|
|
28
|
+
### npm (canonical)
|
|
28
29
|
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
package/docs/scope-provenance.md
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|