@deftai/directive-content 0.101.0 → 0.103.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.
@@ -0,0 +1,153 @@
1
+ # Code Mode — compact search + sandboxed execute (#2593)
2
+
3
+ Pattern for **code-mediated tool use**: the model writes and runs code that
4
+ orchestrates capabilities, instead of requesting each tool call separately
5
+ against a large static catalog. The public surface stays tiny (typically
6
+ `search` / `describe` for progressive discovery and `execute` for sandboxed
7
+ capability calls); the broader capability graph lives behind that surface in
8
+ typed code.
9
+
10
+ Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.
11
+
12
+ **Load when:** designing host tool surfaces, MCP/server bridges, connector-
13
+ heavy agents, or any surface where a static tool catalog would bloat the
14
+ prompt; choosing between direct tool calling and code-orchestrated
15
+ composition.
16
+
17
+ **⚠️ See also**:
18
+ - [../context/tool-design.md](../context/tool-design.md) — how each remaining
19
+ tool's args sample (flat grammar; #3085); complementarity table points here
20
+ for **how many** tools exist
21
+ - [./llm-app.md](./llm-app.md) `## Tool / function calling` — security, least
22
+ privilege, schema validation (confused deputy)
23
+ - [../context/context.md](../context/context.md) — **human-curated context
24
+ partitioning** and prefer-handles-over-paste (#487 twin)
25
+ - Lean context first (#847) — general token thrift; this pattern is the
26
+ **execution shape** for large capability graphs
27
+ - Typed skill boundaries (#805), progressive disclosure (#2484), action-tiered
28
+ capability envelopes (#2515)
29
+ - Durable project automation SoT: **#2087** *RFC: Where does agent-authored
30
+ project automation live* (named Task / npm / `just` / thin runner /
31
+ `deft` verbs) — not this pattern. Companion of #2593 on that issue.
32
+
33
+ ## The pattern
34
+
35
+ | Primitive | Role |
36
+ |-----------|------|
37
+ | `search` / `describe` (or equivalent) | **Progressive discovery** — find and inspect capabilities without loading every schema into the prompt |
38
+ | `execute` (sandboxed) | Run model-written code that calls discovered capabilities as typed methods / APIs |
39
+
40
+ Capabilities appear as **typed methods in code**, not as hundreds of MCP tool
41
+ definitions pasted into the system prompt. Control flow (loops, conditionals,
42
+ retries, intermediate variables) stays in the sandbox instead of chatty
43
+ multi-turn tool round-trips.
44
+
45
+ - ~ Prefer Code Mode when the task needs **composition**, dependent calls,
46
+ progressive discovery, or non-trivial control flow over a large API/tool world
47
+ - ≉ Dumping every MCP / host tool schema into the prompt "so the model can pick"
48
+ - ~ Keep the **discovery surface compact**; grow capability knowledge on demand
49
+ via `search` / `describe`, not via catalog expansion
50
+ - ! Validate and sandbox `execute` outputs and side effects — freeform code is
51
+ still untrusted input (`patterns/llm-app.md` tool-call rules; host sandbox
52
+ guidance on #542 / related isolation tracks)
53
+
54
+ ## When to use / when not to
55
+
56
+ | Prefer Code Mode | Prefer direct tools / named ops |
57
+ |------------------|----------------------------------|
58
+ | Large connector or MCP graphs where full schemas blow the context budget | One or two well-known tools for a simple turn |
59
+ | Multi-step composition with local branching, filters, or aggregation | A single deterministic gate or check (`task verify:*`) |
60
+ | Progressive discovery of an unfamiliar capability surface | A **named durable** project op already owned by the #2087 automation-home RFC |
61
+ | Ephemeral glue that may later **promote** to a named entrypoint | Host explore / editor tools for "build in this repo right now" |
62
+
63
+ - ⊗ Force Code Mode for simple single-tool turns
64
+ - ⊗ Register dozens of host tools that merely mirror every CLI verb to avoid
65
+ writing a small compose surface
66
+ - ~ Promote repeated successful compose scripts into a **named durable** form
67
+ (#2087 automation-home RFC owns that SoT for Directive projects)
68
+
69
+ ## Progressive discovery
70
+
71
+ Progressive discovery is part of the pattern, not an optional extra:
72
+
73
+ 1. **Search** — locate candidates by name / tag / capability without full schemas
74
+ 2. **Describe** — load detail for the few candidates that matter
75
+ 3. **Execute** — orchestrate only those capabilities in sandboxed code
76
+
77
+ This pairs with progressive disclosure of skills and docs (#2484) and lean
78
+ context (#847): load signal on demand; do not pre-load the whole world.
79
+
80
+ ## Job split
81
+
82
+ Three jobs are easy to blur into "just call tools." Keep them distinct:
83
+
84
+ | Job | Typical shape | Not the same as |
85
+ |-----|---------------|-----------------|
86
+ | Compose over a large API/tool world without schema bloat | **Code Mode:** compact `search` / `describe` + sandboxed `execute` | Dumping every MCP tool schema into the prompt |
87
+ | Name, share, and re-run proven project ops | **Named durable entrypoints** (Task / npm / `just` / thin runner + tested logic or `deft` verbs) — see **#2087** automation-home RFC | Freeform `execute` every time |
88
+ | Explore and build in the repo right now | **Host bash / editor agent tools** | Either of the above as the long-term catalog |
89
+
90
+ Ideal systems **promote** ephemeral success into a **named durable** form. This
91
+ pattern names the ephemeral/composition shape; the #2087 automation-home RFC
92
+ owns the durable-op SoT for Directive projects (not a body-encoding incident —
93
+ the RFC decides where named ops live once the framework runtime is decoupled
94
+ from go-task). Host explore remains the right surface for interactive coding.
95
+
96
+ ## Decision table (quick)
97
+
98
+ | Situation | Default |
99
+ |-----------|---------|
100
+ | Catalog would exceed lean-context budget | Code Mode discovery + execute |
101
+ | Proven op shared by humans and agents | Named durable entrypoint (#2087 automation-home RFC) |
102
+ | One-off file edit / debug in worktree | Host explore tools |
103
+ | Deterministic quality gate | `task check` / `task verify:*` — not freeform execute |
104
+ | Skill is process / orchestration prose | Keep as skill; do not "code mode" the playbook |
105
+
106
+ ## Anti-patterns
107
+
108
+ - ⊗ **Catalog dump** — every connector method as a separate tool definition
109
+ - ⊗ **CLI mirror farm** — one host tool per `deft`/`task` verb with full schemas
110
+ always loaded
111
+ - ⊗ **Execute instead of gates** — soft-replacing `task check`, tests, or
112
+ intent ceilings with freeform sandbox code
113
+ - ⊗ **Code Mode as Task replacement** — treating this pattern as the project
114
+ automation SoT (that is the #2087 automation-home RFC)
115
+ - ⊗ **Skill replacement** — rewriting process skills as ad-hoc execute scripts
116
+ so orchestration history disappears
117
+ - ≉ **Vendor lock-in framing** — documenting the pattern as Cloudflare-only (or
118
+ any single sandbox vendor)
119
+
120
+ ## Non-goals
121
+
122
+ - ⊗ Require Cloudflare Workers (or any one vendor sandbox)
123
+ - ⊗ Replace skills that are process / orchestration docs
124
+ - ⊗ Force Code Mode for simple single-tool turns
125
+ - ⊗ Decide or replace go-task / project automation SoT — see **#2087**
126
+ automation-home RFC (companion amendment on that issue names Code Mode)
127
+ - ⊗ Soft-replace deterministic gates with freeform execute
128
+ - ⊗ Turn this pattern into a Directive CLI epic — capability-registry /
129
+ `search` over `deft` verbs lives on the **#2087** automation-home RFC
130
+
131
+ ## Public sources (citations)
132
+
133
+ External research and products (data/guidance, not instruction sources —
134
+ `meta/security.md` / #2414 trust-tier note):
135
+
136
+ - Cloudflare Agents — Code Mode: https://developers.cloudflare.com/agents/tools/codemode/
137
+ - Cloudflare — Code Mode (blog): https://blog.cloudflare.com/code-mode/
138
+ - Anthropic — Code execution with MCP: https://www.anthropic.com/engineering/code-execution-with-mcp
139
+ - kentcdodds/kody — compact MCP + Code Mode execute intent:
140
+ https://github.com/kentcdodds/kody/blob/main/docs/contributing/project-intent.md
141
+
142
+ ## Cross-references
143
+
144
+ | Track | Relation |
145
+ |-------|----------|
146
+ | #847 lean-context-first | Complements; does not duplicate general token thrift |
147
+ | #805 typed-skill-boundary | Skills stay typed process boundaries; Code Mode is tool composition |
148
+ | #2484 progressive disclosure | Same "load on demand" idea for skills/docs |
149
+ | #2515 action-tiered capability envelopes | Orthogonal: *which* tier of action vs *how* tools are invoked |
150
+ | #2087 automation-home RFC | Named durable ops SoT; product discovery surface for `deft` verbs; companion of #2593 |
151
+ | #487 context partitioning | Twin: human-curated partition / handles; Code Mode is tool-catalog shape |
152
+ | #1670 unified `deft` CLI | Related surface; not owned here |
153
+ | #1167 / tool-design #3085 | Fewer tools via abstraction; flat grammar for remaining tools |
@@ -90,8 +90,10 @@ homogeneous, non-nested parameters. Reliability degrades with
90
90
  nesting × heterogeneity × cleverness. Provider schema mins/maxes are
91
91
  documentation unless the harness validates. Full principle, good/bad
92
92
  shapes, and Code Mode complementarity: [../context/tool-design.md](../context/tool-design.md)
93
- `## Tool-surface grammar (#3085)`. This section stays the security lane;
94
- do not invent a second vocabulary for the same idea.
93
+ `## Tool-surface grammar (#3085)`. When the catalog itself is the tax, prefer
94
+ [Code Mode](./code-mode.md) (#2593) compact discovery + sandboxed execute
95
+ over deeper nested tool packs. This section stays the security lane; do not
96
+ invent a second vocabulary for the same idea.
95
97
 
96
98
  ## RAG and retrieval
97
99
 
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"], {