@deftai/directive-content 0.101.0 → 0.102.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/scm/github.md CHANGED
@@ -393,9 +393,9 @@ Following a v1.0.0 release, commits:
393
393
 
394
394
  **Framework CI runners (#2672)**:
395
395
  - ! `deftai/directive` required CI prefers Blacksmith (`blacksmith-4vcpu-ubuntu-2404`) for TypeScript and Go cost
396
- - ! Capacity watchdog (~20 minute budget): if a Blacksmith primary job stays `queued` with `runner_name` null and no `started_at`, cancel that queued attempt (concurrency cancel-in-progress) and run the same suite on `ubuntu-latest`
397
- - ! Branch-protection required check names (`TypeScript (build + lint + test)`, `Go (test + build)`) live **only** on the aggregator jobs — never on primary/failover lane names
398
- - ⊗ Fail over `in_progress` jobs (execution hangs) — those stay timeout + fix (#2652); capacity failover is queue-stall only
396
+ - ! Capacity watchdog (~20 minute budget): if a required Blacksmith job (TypeScript, Go, or merge-gate) stays unclaimed (`runner_name` null `started_at` alone is not a claim), cancel that unclaimed attempt and run the same suite on `ubuntu-latest` (#3340)
397
+ - ! Branch-protection required check names (`TypeScript (build + lint + test)`, `Go (test + build)`, `Merge gate (task check)`) live **only** on the aggregator jobs — never on primary/failover lane names
398
+ - ⊗ Fail over a job that has `runner_name` (claimed execution hang) — those stay timeout + fix (#2652); capacity failover is unclaimed-queue stall only (#3340)
399
399
  - ! Consumer scaffolds and `npm-publish.yml` stay on GitHub-hosted `ubuntu-latest` (Blacksmith is opt-in for consumer orgs; npm `--provenance` requires GH-hosted)
400
400
  - ! Agents seeing `runner_capacity_stall` / `RUNNER_CAPACITY_STALL` MUST wait for auto-failover — ⊗ `--skip-ci` as a capacity remedy
401
401
 
@@ -366,6 +366,15 @@ task verify:ac -- <active-story-path>
366
366
  - ⊗ Skip this gate because ceremony dial is rapid/minimal — rapid's positive content is exactly this check (#3284).
367
367
  - ⊗ Leave `plan.acceptance.commands` empty without `none_stated: true` — absence must be an explicit decision.
368
368
 
369
+ ## Product-oracle gate integrity (#3322 / #3156)
370
+
371
+ A red product verification may be resolved only by a product change or an independently re-derived oracle (both sides rebuilt from scratch, different method). In-place repair of the failing comparison then pass is not a pass — it is an unresolved discrepancy.
372
+
373
+ - ! When a product oracle is red, resolve it by changing the product or by independently re-deriving the oracle, and record `independent_rederivation` on the run-summary `verification` event.
374
+ - ! Emit a run-summary verification event `{check_id, method_fingerprint, outcome}` for each product-oracle attempt when `DEFT_RUN_SUMMARY_PATH` is set. `fail` then a different `method_fingerprint` then `pass` on one check id is machine-flagged.
375
+ - ! `task verify:ac` treats comparison-method mutation as unresolved (exit non-zero) unless independent re-derivation is recorded. Lead the done report with any unresolved discrepancy (#1006).
376
+ - ⊗ Self-adjudicate a red product oracle by editing the comparison (reference file, diff invocation, one-sided regenerate) and shipping the new pass as success.
377
+
369
378
  ## Operator-log hygiene (lazy-load, #1940)
370
379
 
371
380
  When the story touches **operator-facing** services (dashboards, multi-process
@@ -510,3 +519,4 @@ Docs: `docs/decision-log.md` · `xbrief/decisions/README.md`.
510
519
  - ⊗ Silently skip deepening for budget without a fail-loud summary note (#3266 / #1006)
511
520
  - ⊗ Chase post-bank out-of-scope findings when surplus budget is insufficient — report, do not thrash the banked pass (#3285)
512
521
  - ⊗ Skip finalize-on-green after first stated AC pass under a hard budget (#3285)
522
+ - ⊗ Clear a red product oracle by editing the comparison method then treating the new pass as a pass — record independent re-derivation or fix the product (#3322 / #3156)
@@ -84,16 +84,18 @@ Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.
84
84
  3. ! Emit an ordered **shortlist** (deep dive / promote candidates) and a **park list** with park reasons.
85
85
  4. ! Separate **interrupt / non-portfolio** rows (escalate, hold, already dispositioned) so they are not ranked against park list.
86
86
  5. ⊗ Claim "#X supersedes #Y" from titles alone — see Phase 4 epistemic gates.
87
+ 6. ! **List-before-re-recommend** (#3315): for each cited `#N` before re-recommending an overlap, run `task decision:list -- --issue N --json` and match `relatedIssues` from the JSON. Park / do not re-shortlist as novel overlap only when a dispose decision's `relatedIssues` covers this overlap's members (not some other relationship that merely mentions N), unless `revisitTrigger` applies. Advisory diligence — not a `task check` gate. ⊗ Auto-close issues or treat the brief as the dispose record.
88
+ 7. Parent/child, leaf/tracker, and sibling-validator pairs are **not** duplicates — #3066/#3082, #95/#96, #513/#514.
87
89
 
88
90
  ### Phase 4 — Epistemic gates (MUST)
89
91
 
90
92
  Before citing any `#N` in the brief:
91
93
 
92
94
  1. ! **Verify existence + state** for every cited issue via live `gh api repos/OWNER/NAME/issues/N` (REST) **or** a proven-fresh cache entry whose state matches the claim.
93
- 2. ! **Read the body** of every issue used for shortlist, park-as-superseded, pack membership, or "decided" claims.
95
+ 2. ! **Read the body** of every issue used for shortlist, park-as-superseded, duplicate/consolidate classification, pack membership, or "decided" claims.
94
96
  3. ! When claiming decided / superseded / closed-by-comment, also **read comments** (REST `issues/N/comments`).
95
97
  4. ! State **open/closed accurately**; never invent issue numbers.
96
- 5. ⊗ **Title-only supersession** or ownership claims.
98
+ 5. ⊗ **Title-only supersession**, duplicate, or ownership claims.
97
99
  6. ⊗ Cite PRs as issues without filtering `pull_request` on mixed issue lists.
98
100
 
99
101
  ### Phase 5 — Emit priority brief
@@ -117,9 +119,10 @@ Before citing any `#N` in the brief:
117
119
  ### Phase 6 — Dispose checklist (hand off)
118
120
 
119
121
  1. ! Present the dispose checklist to the operator (accept/edit shortlist + park; record dispose; optional plan-sequence for P1).
120
- 2. ! Point dispose targets: `task decision:write` (#1396 structured decision log under `xbrief/decisions/`; optional interim issue comment only if the write surface is unavailable), and/or `task plan-sequence:set`.
122
+ 2. ! Point dispose targets: `task decision:write` (#1396 / [`docs/decision-log.md`](../../docs/decision-log.md)): every overlap-cluster member in `relatedIssues`; include `revisitTrigger`; free-text MAY name relationship. Optional `task plan-sequence:set`. Interim issue comment only if the write surface is unavailable.
121
123
  3. ⊗ Auto-promote shortlist into plan-sequence without explicit operator dispose.
122
124
  4. ⊗ Exit treating the brief alone as durable prioritization memory (#2741 class).
125
+ ~ Dedicated duplicate-clusters ledger is deferred; earn it only when a pass re-litigates a cluster despite a dispose decision listing the members (#3310). Boundaries: #886, #1178, #786, #3198/#3201, #1396.
123
126
 
124
127
  ## Anti-Patterns
125
128
 
@@ -130,6 +133,7 @@ Before citing any `#N` in the brief:
130
133
  - ⊗ Full open-backlog unattended ranking without an explicit slice
131
134
  - ⊗ Replacing `triage:queue` for buildable work selection
132
135
  - ⊗ Treating the brief as the decision record without dispose
136
+ - ⊗ Re-shortlist a disposed overlap without `task decision:list -- --issue N` (#3315)
133
137
 
134
138
  ## EXIT
135
139
 
@@ -141,4 +145,5 @@ Before citing any `#N` in the brief:
141
145
 
142
146
  - #3198 process + dogfood · #3201 this skill · #3200 / patterns pilot brief
143
147
  - #1396 decision log · #3179 propose-not-apply · #1423 / #3197 classify filter only
148
+ - #3315 overlap dispose · #3310 ledger archive · #886 / #1178 / #786 boundaries
144
149
  - Siblings: `deft-directive-triage`, `deft-directive-refinement` — not #1419/#1511 post-promotion
@@ -181,6 +181,15 @@ When session effort-budget is hard-capped (`DEFT_MAX_TURNS` / `DEFT_MAX_BUDGET`
181
181
  - ! Full `task check` already runs `verify:ac` first; still run it explicitly before push when AC is stated so failures are visible without the full hygiene suite.
182
182
  - ⊗ Skip AC run on rapid/minimal ceremony dial — rapid = AC-only; AC never degrades when commands exist (#3284 / #3156).
183
183
 
184
+ ## Product-oracle gate integrity (#3322 / #3156)
185
+
186
+ A red product verification may be resolved only by a product change or an independently re-derived oracle (both sides rebuilt from scratch, different method). In-place repair of the failing comparison then pass is not a pass — it is an unresolved discrepancy.
187
+
188
+ - ! When a product oracle is red, resolve it by changing the product or by independently re-deriving the oracle, and record `independent_rederivation` on the run-summary `verification` event.
189
+ - ! Emit a run-summary verification event `{check_id, method_fingerprint, outcome}` for each product-oracle attempt when `DEFT_RUN_SUMMARY_PATH` is set. `fail` then a different `method_fingerprint` then `pass` on one check id is machine-flagged.
190
+ - ! `task verify:ac` treats comparison-method mutation as unresolved (exit non-zero) unless independent re-derivation is recorded. Lead the done report with any unresolved discrepancy (#1006).
191
+ - ⊗ Self-adjudicate a red product oracle by editing the comparison (reference file, diff invocation, one-sided regenerate) and shipping the new pass as success.
192
+
184
193
  ## Probe-then-fill remote claims (#3120)
185
194
 
186
195
  ! Before filling any **remote** handoff field (PR URL, PR number, commit/HEAD SHA, CI green/success, review score) or claiming `status: pass` / ship/gate done, MUST **probe then fill**:
@@ -228,5 +237,6 @@ Docs: `docs/decision-log.md`.
228
237
  - ⊗ Invent remote PR/SHA/CI/review claims in handoff evidence without same-turn probe binding — invented-done (#3120)
229
238
  - ⊗ Fill remote ship/gate fields from memory when only local work completed; legal partial omits PR fields (#3120)
230
239
  - ⊗ Clear a failing gate by editing the gate (definition, verifier, reward, required check, coverage floor, policy, eval fixture) instead of the work under test — gate integrity (#3156); see [docs/gate-integrity.md](../../docs/gate-integrity.md)
240
+ - ⊗ Clear a red product oracle by editing the comparison method then treating the new pass as a pass — record independent re-derivation or fix the product (#3322 / #3156)
231
241
  - ⊗ Under a hard turn/cost budget, gold-plate pre-PR polish past the stated bar until the budget expires (#3266)
232
242
  - ⊗ Exit pre-PR after skipping deepen-for-budget without naming the skip in the summary (#3266 / #1006)
@@ -144,7 +144,7 @@ See [`docs/RELEASING.md`](../../../docs/RELEASING.md) § Fixable check failure d
144
144
  2. If **no open coverage-debt issue exists** → auto-hatch files `#N` (or operator files manually) with title prefix `coverage-debt:` and body containing both markers. The open `#N` remains WIP until coverage is restored and the issue is closed.
145
145
  3. If an **open coverage-debt issue from a prior hatch still exists** → ⊗ soft-pass again; restore real coverage (all four metrics ≥ 85%) and close the debt issue before the cut proceeds.
146
146
 
147
- **Framework-release-first (#3187):** auto-hatch applies to framework `task release` Step 5 (e.g. deftai/directive). Consumer expansion of auto-hatch is blocked on project `plan.policy.coverageDebt` (#3189) refuse when `status=unset` or `mode=off`. Do not auto-file debt on the framework repo from consumer trees; consumer ledger is always the consumer repo.
147
+ **Framework-release-first (#3187):** auto-hatch applies to framework `task release` Step 5 (e.g. deftai/directive). Consumer expansion of auto-hatch via `plan.policy.coverageDebt` is reserved; consumer expansion is not implemented (#3314). Refuse when unset or off. Live hatch is `--allow-coverage-debt=#N` (#2866). Do not auto-file debt on the framework repo from consumer trees; consumer ledger is always the consumer repo.
148
148
 
149
149
  ⊗ Auto-pass on a near-miss band without `#N` (#2573).
150
150
  ⊗ Silent soft-pass with no tracked issue.
@@ -11,6 +11,18 @@
11
11
  */
12
12
 
13
13
  const { spawnSync } = require("node:child_process");
14
+ const fs = require("node:fs");
15
+ const path = require("node:path");
16
+
17
+ /** Consumer-deposit identity (#3324). Present on @deftai/directive-content. */
18
+ const CONSUMER_DEPOSIT_FIELD = "deftConsumerDeposit";
19
+ /** Optional marker file at DEFT_ROOT (installer / test fixture). */
20
+ const CONSUMER_DEPOSIT_MARKER_FILE = ".deft-consumer-deposit";
21
+ /** Published content-package name also identifies a deposit. */
22
+ const CONTENT_PACKAGE_NAME = "@deftai/directive-content";
23
+ /** Single remediation when a deposit has no global CLI (#3324 / #3265). */
24
+ const DEPOSIT_REMEDIATION = "npm i -g @deftai/directive";
25
+ const SOURCE_BUILD_REMEDIATION = "task build";
14
26
 
15
27
  /**
16
28
  * cmd.exe command separators / metacharacters. Free-text DEFT_ENGINE_CMD_JSON
@@ -73,6 +85,142 @@ function shellSplit(input) {
73
85
  return out;
74
86
  }
75
87
 
88
+ /**
89
+ * True when DEFT_ROOT is a consumer deposit, not a framework source checkout.
90
+ * Honors an explicit package.json field, the content-package name, or a
91
+ * marker file. #3324: deposits must never take the self-build path.
92
+ * @param {string} root
93
+ * @param {{ fs?: typeof fs, path?: typeof path }} [opts]
94
+ */
95
+ /**
96
+ * Consumer Taskfile includes live at `.deft/core/tasks/`, so DEFT_ROOT is
97
+ * `.deft/core`. Framework source uses repo-root `tasks/`. Go source-tarball
98
+ * deposits keep the unmarked monorepo package.json at that core root (#3324
99
+ * Greptile P1) — the path itself is the deposit identity.
100
+ * @param {string} root
101
+ */
102
+ function isVendoredCoreRoot(root, opts = {}) {
103
+ const io = opts.fs || fs;
104
+ const pathMod = opts.path || path;
105
+ const norm = String(root).replace(/\\/g, "/").replace(/\/+$/, "");
106
+ if (!/(^|\/)\.deft\/core$/.test(norm)) {
107
+ return false;
108
+ }
109
+ // Installer tarballs exclude .git (#1425). A genuine source checkout mounted
110
+ // at this path still has .git and must keep the self-build path.
111
+ try {
112
+ if (io.existsSync(pathMod.join(root, ".git"))) {
113
+ return false;
114
+ }
115
+ } catch {
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ function hasConsumerDepositMarker(root, opts = {}) {
122
+ const io = opts.fs || fs;
123
+ const pathMod = opts.path || path;
124
+ if (!root) {
125
+ return false;
126
+ }
127
+ if (isVendoredCoreRoot(root, opts)) {
128
+ return true;
129
+ }
130
+ const markerPath = pathMod.join(root, CONSUMER_DEPOSIT_MARKER_FILE);
131
+ try {
132
+ if (io.existsSync(markerPath)) {
133
+ return true;
134
+ }
135
+ } catch {
136
+ // ignore unreadable marker path
137
+ }
138
+ const pkgPath = pathMod.join(root, "package.json");
139
+ try {
140
+ const pkg = JSON.parse(io.readFileSync(pkgPath, "utf8"));
141
+ if (pkg && pkg[CONSUMER_DEPOSIT_FIELD] === true) {
142
+ return true;
143
+ }
144
+ if (pkg && pkg.name === CONTENT_PACKAGE_NAME) {
145
+ return true;
146
+ }
147
+ } catch {
148
+ return false;
149
+ }
150
+ return false;
151
+ }
152
+
153
+ /**
154
+ * Buildable-source means "may run pnpm/corepack/`task build`". A deposit
155
+ * marker forces false even when packages/cli + scripts.build are present.
156
+ * @param {string} root
157
+ * @param {{ fs?: typeof fs, path?: typeof path }} [opts]
158
+ */
159
+ function isBuildableSource(root, opts = {}) {
160
+ if (hasConsumerDepositMarker(root, opts)) {
161
+ return false;
162
+ }
163
+ const io = opts.fs || fs;
164
+ const pathMod = opts.path || path;
165
+ if (!root) {
166
+ return false;
167
+ }
168
+ const cliPkg = pathMod.join(root, "packages", "cli", "package.json");
169
+ const rootPkg = pathMod.join(root, "package.json");
170
+ try {
171
+ if (!io.existsSync(cliPkg) || !io.existsSync(rootPkg)) {
172
+ return false;
173
+ }
174
+ const pkg = JSON.parse(io.readFileSync(rootPkg, "utf8"));
175
+ return Boolean(pkg && pkg.scripts && pkg.scripts.build);
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Dispatch decision after local dist / buildable / runtime-whitelist probes.
183
+ * Deposit marker is applied via isBuildableSource(false); this keeps the
184
+ * runtime-verb whitelist for true source checkouts only (#3324 / #2409).
185
+ *
186
+ * @param {{
187
+ * hasBin: boolean,
188
+ * isBuildableSource: boolean,
189
+ * isRuntimeVerb: boolean,
190
+ * hasGlobalCli: boolean,
191
+ * }} input
192
+ * @returns {{
193
+ * action: "vendored" | "global" | "fail-closed",
194
+ * remediations?: string[],
195
+ * exitCode?: number,
196
+ * }}
197
+ */
198
+ function resolveInvokeDispatch(input) {
199
+ if (input.hasBin) {
200
+ return { action: "vendored" };
201
+ }
202
+ if (input.isBuildableSource) {
203
+ if (input.isRuntimeVerb && input.hasGlobalCli) {
204
+ return { action: "global" };
205
+ }
206
+ return {
207
+ action: "fail-closed",
208
+ remediations: input.isRuntimeVerb
209
+ ? [DEPOSIT_REMEDIATION, SOURCE_BUILD_REMEDIATION]
210
+ : [SOURCE_BUILD_REMEDIATION],
211
+ exitCode: 2,
212
+ };
213
+ }
214
+ if (input.hasGlobalCli) {
215
+ return { action: "global" };
216
+ }
217
+ return {
218
+ action: "fail-closed",
219
+ remediations: [DEPOSIT_REMEDIATION],
220
+ exitCode: 2,
221
+ };
222
+ }
223
+
76
224
  function main() {
77
225
  const mode = process.argv[2];
78
226
  const target = process.argv[3];
@@ -171,7 +319,28 @@ function buildSpawnPlan(mode, target, argv, opts = {}) {
171
319
  }
172
320
 
173
321
  if (require.main === module) {
322
+ const mode = process.argv[2];
323
+ if (mode === "deposit-marker") {
324
+ process.exit(hasConsumerDepositMarker(process.argv[3] || "") ? 0 : 1);
325
+ }
326
+ if (mode === "is-buildable-source") {
327
+ process.exit(isBuildableSource(process.argv[3] || "") ? 0 : 1);
328
+ }
174
329
  main();
175
330
  }
176
331
 
177
- module.exports = { shellSplit, quoteWin32Arg, buildSpawnPlan, WIN32_CMD_METACHAR_RE };
332
+ module.exports = {
333
+ shellSplit,
334
+ quoteWin32Arg,
335
+ buildSpawnPlan,
336
+ WIN32_CMD_METACHAR_RE,
337
+ hasConsumerDepositMarker,
338
+ isVendoredCoreRoot,
339
+ isBuildableSource,
340
+ resolveInvokeDispatch,
341
+ CONSUMER_DEPOSIT_FIELD,
342
+ CONSUMER_DEPOSIT_MARKER_FILE,
343
+ CONTENT_PACKAGE_NAME,
344
+ DEPOSIT_REMEDIATION,
345
+ SOURCE_BUILD_REMEDIATION,
346
+ };
@@ -2,12 +2,23 @@
2
2
  "use strict";
3
3
 
4
4
  const assert = require("node:assert/strict");
5
- const { describe, it } = require("node:test");
5
+ const { spawnSync } = require("node:child_process");
6
+ const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+ const { describe, it, after } = require("node:test");
6
10
  const {
7
11
  shellSplit,
8
12
  quoteWin32Arg,
9
13
  buildSpawnPlan,
10
14
  WIN32_CMD_METACHAR_RE,
15
+ hasConsumerDepositMarker,
16
+ isVendoredCoreRoot,
17
+ isBuildableSource,
18
+ resolveInvokeDispatch,
19
+ CONSUMER_DEPOSIT_MARKER_FILE,
20
+ CONTENT_PACKAGE_NAME,
21
+ DEPOSIT_REMEDIATION,
11
22
  } = require("./engine-invoke.cjs");
12
23
 
13
24
  const WIN32 = { platform: "win32", nodePath: "/node" };
@@ -203,7 +214,141 @@ describe("buildSpawnPlan — CodeQL absolute-path isolation (#3175 / alert #74)"
203
214
  assert.equal(plan.args[3].includes("node.exe"), false);
204
215
  assert.deepEqual(splitCmdTokens(plan.args[3]), ["deft", "release", "--summary", "ok"]);
205
216
  });
217
+ });
218
+
219
+ describe("consumer-deposit marker (#3324)", () => {
220
+ const created = [];
221
+
222
+ after(() => {
223
+ for (const dir of created.splice(0)) {
224
+ rmSync(dir, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ function tempRoot() {
229
+ const dir = mkdtempSync(join(tmpdir(), "3324-deposit-"));
230
+ created.push(dir);
231
+ return dir;
232
+ }
233
+
234
+ function writePkg(dir, pkg) {
235
+ mkdirSync(dir, { recursive: true });
236
+ writeFileSync(join(dir, "package.json"), JSON.stringify(pkg), "utf8");
237
+ }
238
+
239
+ function writeBuildableTree(dir, pkgExtra = {}) {
240
+ writePkg(dir, { name: "looks-like-source", scripts: { build: "tsc -b" }, ...pkgExtra });
241
+ mkdirSync(join(dir, "packages", "cli"), { recursive: true });
242
+ writePkg(join(dir, "packages", "cli"), { name: "@deftai/directive-cli" });
243
+ }
244
+
245
+ it("detects the package.json deftConsumerDeposit field", () => {
246
+ const root = tempRoot();
247
+ writePkg(root, { name: "app", deftConsumerDeposit: true, scripts: { build: "tsc" } });
248
+ assert.equal(hasConsumerDepositMarker(root), true);
249
+ });
250
+
251
+ it("detects @deftai/directive-content as a deposit", () => {
252
+ const root = tempRoot();
253
+ writePkg(root, { name: CONTENT_PACKAGE_NAME, scripts: { build: "tsc" } });
254
+ assert.equal(hasConsumerDepositMarker(root), true);
255
+ });
256
+
257
+ it("detects the .deft-consumer-deposit marker file", () => {
258
+ const root = tempRoot();
259
+ writePkg(root, { name: "app", scripts: { build: "tsc" } });
260
+ writeFileSync(join(root, CONSUMER_DEPOSIT_MARKER_FILE), "1\n", "utf8");
261
+ assert.equal(hasConsumerDepositMarker(root), true);
262
+ });
263
+
264
+ it("does not mark a framework source checkout", () => {
265
+ const root = tempRoot();
266
+ writeBuildableTree(root);
267
+ assert.equal(hasConsumerDepositMarker(root), false);
268
+ assert.equal(isBuildableSource(root), true);
269
+ });
270
+
271
+ it("treats a .deft/core DEFT_ROOT as a deposit (Go source-tarball layout)", () => {
272
+ const root = join(tempRoot(), ".deft", "core");
273
+ writeBuildableTree(root);
274
+ assert.equal(isVendoredCoreRoot(root), true);
275
+ assert.equal(hasConsumerDepositMarker(root), true);
276
+ assert.equal(isBuildableSource(root), false);
277
+ });
206
278
 
279
+ it("does not treat a source checkout mounted at .deft/core as a deposit", () => {
280
+ const root = join(tempRoot(), ".deft", "core");
281
+ writeBuildableTree(root);
282
+ mkdirSync(join(root, ".git"));
283
+ assert.equal(isVendoredCoreRoot(root), false);
284
+ assert.equal(isBuildableSource(root), true);
285
+ });
286
+
287
+ it("never treats a marked deposit as buildable source", () => {
288
+ const root = tempRoot();
289
+ writeBuildableTree(root, { deftConsumerDeposit: true });
290
+ assert.equal(hasConsumerDepositMarker(root), true);
291
+ assert.equal(isBuildableSource(root), false);
292
+ });
293
+
294
+ it("routes every deposit verb via global CLI (not the self-build path)", () => {
295
+ const plan = resolveInvokeDispatch({
296
+ hasBin: false,
297
+ isBuildableSource: false,
298
+ isRuntimeVerb: false,
299
+ hasGlobalCli: true,
300
+ });
301
+ assert.deepEqual(plan, { action: "global" });
302
+ });
303
+
304
+ it("fails closed with the one remediation when a deposit has no global CLI", () => {
305
+ const plan = resolveInvokeDispatch({
306
+ hasBin: false,
307
+ isBuildableSource: false,
308
+ isRuntimeVerb: false,
309
+ hasGlobalCli: false,
310
+ });
311
+ assert.equal(plan.action, "fail-closed");
312
+ assert.equal(plan.exitCode, 2);
313
+ assert.deepEqual(plan.remediations, [DEPOSIT_REMEDIATION]);
314
+ assert.equal(DEPOSIT_REMEDIATION, "npm i -g @deftai/directive");
315
+ });
316
+
317
+ it("keeps the runtime-verb whitelist for true source checkouts", () => {
318
+ const runtime = resolveInvokeDispatch({
319
+ hasBin: false,
320
+ isBuildableSource: true,
321
+ isRuntimeVerb: true,
322
+ hasGlobalCli: true,
323
+ });
324
+ assert.deepEqual(runtime, { action: "global" });
325
+
326
+ const check = resolveInvokeDispatch({
327
+ hasBin: false,
328
+ isBuildableSource: true,
329
+ isRuntimeVerb: false,
330
+ hasGlobalCli: true,
331
+ });
332
+ assert.equal(check.action, "fail-closed");
333
+ assert.deepEqual(check.remediations, ["task build"]);
334
+ });
335
+
336
+ it("CLI probe exits 0 for a marked deposit and 1 for is-buildable-source", () => {
337
+ const root = tempRoot();
338
+ writeBuildableTree(root, { deftConsumerDeposit: true });
339
+ const script = join(__dirname, "engine-invoke.cjs");
340
+ const marker = spawnSync(process.execPath, [script, "deposit-marker", root], {
341
+ encoding: "utf8",
342
+ });
343
+ assert.equal(marker.status, 0, marker.stderr);
344
+ const buildable = spawnSync(process.execPath, [script, "is-buildable-source", root], {
345
+ encoding: "utf8",
346
+ });
347
+ assert.equal(buildable.status, 1, buildable.stderr);
348
+ });
349
+ });
350
+
351
+ describe("buildSpawnPlan — CodeQL absolute-path isolation (#3175 / alert #74)", () => {
207
352
  it("uses nodePath only as the non-shell vendored command (never cmd.exe)", () => {
208
353
  const nodePath = String.raw`C:\Program Files\nodejs\node.exe`;
209
354
  const plan = buildSpawnPlan("vendored", String.raw`C:\repo\packages\cli\dist\bin.js`, ["session:start"], {
package/tasks/engine.yml CHANGED
@@ -5,6 +5,8 @@ version: '3'
5
5
  # Framework-source checkouts invoke the vendored `packages/cli/dist/bin.js`.
6
6
  # npm consumer deposits copy @deftai/directive-content only — no bundled CLI —
7
7
  # so tasks fall back to the globally installed `deft` / `directive` command.
8
+ # A deposit marker (package.json deftConsumerDeposit / .deft-consumer-deposit /
9
+ # @deftai/directive-content name) forces that fallback for every verb (#3324).
8
10
 
9
11
  vars:
10
12
  DEFT_ROOT: '{{joinPath .TASKFILE_DIR ".."}}'
@@ -24,7 +26,7 @@ tasks:
24
26
 
25
27
  _ts-build:
26
28
  internal: true
27
- desc: "Build CLI dist from the framework source checkout; no-op on npm consumer deposits that ship no packages/ source (#2022 Phase 3 / #2126)."
29
+ desc: "Build CLI dist from the framework source checkout; no-op on consumer deposits (#2022 / #2126 / #3324)."
28
30
  # Run from the operator project root (USER_WORKING_DIR), NOT an absolute
29
31
  # DEFT_ROOT: an absolute `dir:` doubles onto the invocation cwd on Windows
30
32
  # when the Taskfile is invoked via `task -t <absolute-path>` (observed as
@@ -45,9 +47,9 @@ tasks:
45
47
  cmds:
46
48
  - |
47
49
  set -eu
48
- if [ -f "{{.DEFT_ROOT}}/packages/cli/package.json" ] \
49
- && [ -f "{{.DEFT_ROOT}}/package.json" ] \
50
- && node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));process.exit(j.scripts&&j.scripts.build?0:1)" "{{.DEFT_ROOT}}/package.json"; then
50
+ # #3324: consumer-deposit marker forces a no-op even when stray
51
+ # packages/cli + scripts.build make the tree look buildable.
52
+ if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" is-buildable-source "{{.DEFT_ROOT}}"; then
51
53
  if node "{{.TASKFILE_DIR}}/ts-build-fresh.cjs" "{{.DEFT_ROOT}}"; then
52
54
  exit 0
53
55
  fi
@@ -56,7 +58,7 @@ tasks:
56
58
 
57
59
  invoke:
58
60
  internal: true
59
- desc: "Run a deft-ts verb from vendored bin.js (source checkout) or global deft (npm consumer deposit / runtime fallback #2409). Runtime/session verbs must not trigger engine:_ts-build / pnpm (#2181)."
61
+ desc: "Run a deft-ts verb from vendored bin.js (source checkout) or global deft (deposit / runtime fallback #2409 / #3324). Runtime/session verbs must not trigger engine:_ts-build / pnpm (#2181)."
60
62
  # Run from the operator project root so deft verbs resolve USER_WORKING_DIR
61
63
  # correctly; without this, included engine.yml defaults cwd to tasks/ (#2022).
62
64
  dir: '{{.USER_WORKING_DIR}}'
@@ -70,10 +72,10 @@ tasks:
70
72
  set -eu
71
73
  bin="{{.DEFT_ROOT}}/packages/cli/dist/bin.js"
72
74
  root_pkg="{{.DEFT_ROOT}}/package.json"
75
+ # #3324: deposit marker (package.json field / file / content-package
76
+ # name) forces is_buildable_source=0 so every verb uses global CLI.
73
77
  is_buildable_source=0
74
- if [ -f "{{.DEFT_ROOT}}/packages/cli/package.json" ] \
75
- && [ -f "$root_pkg" ] \
76
- && node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));process.exit(j.scripts&&j.scripts.build?0:1)" "$root_pkg"; then
78
+ if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" is-buildable-source "{{.DEFT_ROOT}}"; then
77
79
  is_buildable_source=1
78
80
  fi
79
81
  if [ -f "$bin" ]; then
@@ -530,6 +530,11 @@ The parent monitor watches the heartbeat file directly (three-state exit 0 ok /
530
530
  - Monitors run `task verify:subagent-alive -- --require-agent <agent-id> [--scratch-dir <worktree>/.deft-scratch/subagent-status]` each poll iteration.
531
531
  - Workers run `task agent:monitor` (raw sweep) or the gate verb above; both wrap `subagent-monitor` (#1365).
532
532
 
533
+ ## 10.55 Rule Authority and Thin Fail-Closed (#3313)
534
+
535
+ Principle: `main.md` `## Rule Authority [AXIOM]`.
536
+ Principle: `main.md` `## Thin Fail-Closed Design (#3265)`.
537
+
533
538
  ## 10.6 Dual stop for multi-iteration worker loops (#2442)
534
539
 
535
540
  Multi-iteration implement, pre-PR, repair, and monitor loops require **two** stops: **success** (goal / AC / checker met) and **failure or budget** (max iterations, no-progress, or time/token budget). Single-turn tasks are exempt. Principle and defaults: `main.md` `## Dual Stop Rule (#2442)`; build skill dual-stop table; swarm Phase 4 / core-ops.
@@ -53,7 +53,6 @@ Legacy `vbrief/` read-accepted; `deft migrate:xbrief` for `xbrief/` (v0.6→v0.8
53
53
  ## Content packs
54
54
 
55
55
  ! Before improvising: `deft packs:slice --list-packs`, then `deft packs:slice <pack> --list` / `deft packs:slice <pack> <slice>` — `commands.md` (§ packs); never enumerate names here.
56
-
57
56
  ## Codebase MAP Projection (#1595 / #1498)
58
57
 
59
58
  ! `plan.architecture.codeStructure` is durable SoT; `.planning/codebase/MAP.md` is generated — `deft codebase:map` / `deft verify:codebase-map-fresh` (`commands.md`). ⊗ Do not hand-edit MAP, block on stale/absent MAP, or elevate projection above xBRIEF (#1595 / #1498).
@@ -61,13 +60,16 @@ Legacy `vbrief/` read-accepted; `deft migrate:xbrief` for `xbrief/` (v0.6→v0.8
61
60
  ## Skills
62
61
 
63
62
  ! **Skills Index** (Level-0) in `.deft/core/REFERENCES.md` — scan before improvising; read `SKILL.md` only on index match. `welcome` / `onboard triage` → `deft triage:welcome --onboard` (N3 / #1143); lessons → packs:slice.
64
-
65
63
  ## Skill pin policy (#2508)
66
64
 
67
65
  ! Process-critical skills with false-negative risk MUST be named in AGENTS.md (always-pin tier) — tier definitions: `.deft/core/docs/skill-pin-policy.md` (#2508).
68
66
  ! **Default always-pins:** `deft-directive-build`, `deft-directive-pre-pr`, `deft-directive-review-cycle`, `deft-directive-swarm` — read each `SKILL.md` when that work type starts.
69
67
  ⊗ Pin entire language packs, deployment docs, or framework bulk into AGENTS.md — pins are for false-negative-sensitive process gates only (#2508).
70
68
  ! **Dual stop (#2442):** multi-iteration work MUST have success + failure/budget stop (max iters / no-progress / budget); single-turn exempt; halt with operator-visible report; ⊗ thrash. Defaults: build, swarm, review-cycle skills. See main.md Dual Stop Rule. Ledger #3143 (`packages/core/src/delivery-attempt/`).
69
+ ## Rule Authority [AXIOM]
70
+ ! Prefer `task deft:*` over AGENTS.md prose. See main.md.
71
+ ## Thin Fail-Closed Design (#3265)
72
+ ! One fail-closed `task deft:*` check + one remediation. See main.md.
71
73
 
72
74
  ## Through-merge worker dispatch (#3032)
73
75
 
@@ -93,7 +95,6 @@ Legacy `vbrief/` read-accepted; `deft migrate:xbrief` for `xbrief/` (v0.6→v0.8
93
95
 
94
96
  ! Mid-scope gates: **split-dispatch** when `agent_id` is terminal; retain-capable hosts (continue-by-agent-id / message-later / steer-mid-flight) MAY re-message the live child. Retention = orchestration only — not constitution self-edit (#3164). Depth: preamble §10; `deft-directive-swarm`. Topology: #3155 nuclear-family. ⊗ Invent retain on one-shot hosts.
95
97
 
96
-
97
98
  ## Review-surface precedence (#2308)
98
99
 
99
100
  ! Route PR shepherding / review work through `deft-directive-review-cycle` — `.deft/core/.agents/skills/deft-directive-review-cycle/SKILL.md`; host `babysit` / `bugbot` / `security-review` advisory-only (#2308 / #2261).
@@ -104,7 +105,6 @@ Legacy `vbrief/` read-accepted; `deft migrate:xbrief` for `xbrief/` (v0.6→v0.8
104
105
 
105
106
  ## Structured decision log (#1396 / #3211)
106
107
  ! Significant choices → `deft decision:write`; re-load → `deft decision:list` / `xbrief/decisions/`; depth `.deft/core/docs/decision-log.md` (not triage/ADRs/lessons).
107
-
108
108
  ## Eval and framework health (#1703)
109
109
 
110
110
  ! `deft eval:health` when orienting or after gate/policy changes (Tier 0; 4-hour debounce). Release: `deft eval:run` / `deft eval:report`; skill routing: `deft eval:triggers` (#1586 / #1703).