@deftai/directive-content 0.100.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.
@@ -111,7 +111,7 @@
111
111
  "rapid"
112
112
  ],
113
113
  "path": "strategies/rapid.md",
114
- "body": "# Rapid Strategy\n\nQuick prototyping workflow -- v0.20 date-prefixed story vBRIEF output with minimal gates and fast iteration.\n\n**Ceremony dial (#3214):** when `plan.policy.ceremonyDial` (or session inputs) selects depth `rapid` — default for S-task × frontier model on a project-shaped session — session ritual / gate depth follows this light path. See `task policy:show --field=ceremonyDial`. Non-project sessions select `minimal` and compose the #3014 minimal AGENTS profile research pointer instead.\n\n**v0.20 note (s5-migrate-speckit-rapid-enterprise / #1166):** Rapid now emits only the canonical v0.20 shape (date-prefixed story vBRIEFs in proposed/, full PROJECT-DEFINITION.vbrief.json via task project:render, seeded lifecycle folders, no legacy specification.vbrief.json). See the dedicated ## v0.20 Output Shape section and the canonical contract `strategies/v0-20-contract.md` (s1-contract of #1166).\n\nLegend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.\n\n**⚠️ See also**: [strategies/interview.md](./interview.md) | [strategies/yolo.md](./yolo.md) | [strategies/README.md](./README.md) | [strategies/v0-20-contract.md](./v0-20-contract.md) | [artifact-guards.md](./artifact-guards.md)\n\n> Ship a throwaway prototype fast. Skip the PRD, skip approval gates, produce a\n> date-prefixed vBRIEF in proposed/ + PROJECT-DEFINITION and start building. Suited for spikes, proof-of-concepts, and\n> disposable experiments where learning speed matters more than long-term quality.\n\n---\n\n## When to Use\n\n- ~ Throwaway prototypes, spikes, and proof-of-concept experiments\n- ~ Validating a technical approach before committing to a full spec cycle\n- ~ Solo explorations where the cost of rework is low\n- ? Time-boxed experiments (e.g. \"spend 2 hours proving this works\")\n- ⊗ Production features, shared libraries, or anything with downstream consumers\n\n---\n\n## Workflow\n\n### Step 1: State the Goal\n\n! Describe the prototype goal in one sentence: what are you trying to learn or prove?\n\n- ! Record the goal in a new date-prefixed story vBRIEF written to `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` (e.g. as the plan title or a brief narrative in the v0.6 schema). Use today's date for the prefix and a descriptive slug.\n- ~ Include a time-box if applicable (e.g. \"4-hour spike\")\n- ⊗ Skip this step -- even throwaway work needs a clear objective\n\n### Step 2: Minimal Interview\n\n! Ask only the questions needed to unblock implementation. Skip sizing gate, skip PRD.\n\n- ! Identify: target platform, primary language, key dependency or API\n- ~ 3-5 questions maximum -- bias toward defaults and moving fast\n- ⊗ Run the full interview questionnaire -- that defeats the purpose of rapid\n\n### Step 3: Generate date-prefixed vBRIEF in proposed/ (Forced-Light Path)\n\n! Before writing output artifacts, follow the guards in [artifact-guards.md](./artifact-guards.md) (Preparatory Guard for proposed/ scope items; Spec-Generating Guard for PROJECT-DEFINITION).\n\n! Write a slim story scope vBRIEF to `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` -- no PRD, no approval gate. Decompose the rapid plan into one or more focused, buildable story vBRIEFs (v0.6 schema) rather than a monolithic legacy spec.\n\n- ! Use the Light path from [interview.md](./interview.md) unconditionally\n- ! Mark `plan.status` as `draft` (not `approved`) to signal prototype quality\n- ! After the proposed/ vBRIEF(s) are written, invoke `task project:render` (from repo root) to generate/refresh the complete `vbrief/PROJECT-DEFINITION.vbrief.json`\n- ~ Keep tasks coarse-grained -- 3-5 tasks is typical for a spike\n- ⊗ Generate a PRD or require approval -- rapid skips both\n- ⊗ Hand-author `SPECIFICATION.md` directly -- it is a rendered derivative only (see v0.20 Output Shape)\n\n! After emitting the proposed/ story vBRIEF(s), surface the GitHub-issue tracking hint from [emit-hints.md](./emit-hints.md) — name all three patterns (none / `--umbrella` / `--per-vbrief`).\n\n### Step 4: Build\n\n! Implement against the spec. Quality gates are relaxed but not absent.\n\n- ! Tests are still required, but coverage gate is relaxed to ≥50%\n- ~ Favour working code over clean code -- refactor later if the prototype graduates\n- ! `task check` must still pass (lint + fmt + tests)\n- ⊗ Skip `task check` entirely -- even prototypes must compile and pass basic checks\n\n### Step 5: Evaluate\n\n! At the end of the time-box (or when the prototype is done), decide next steps.\n\n- ! Record findings: what worked, what didn't, what surprised you\n- ~ Options: discard, iterate, or graduate to a full spec cycle via [interview.md](./interview.md)\n- ! If graduating: start a fresh interview -- do not carry forward the rapid spec as-is\n\n---\n\n## Output Artifacts\n\n- `vbrief/proposed/YYYY-MM-DD-*.vbrief.json` (one or more) -- lightweight story scope vBRIEF(s) with `draft` status (primary artifact; date-prefixed per v0.20 contract)\n- `vbrief/PROJECT-DEFINITION.vbrief.json` -- complete project gestalt + items registry (via `task project:render` invoked by Rapid)\n- `vbrief/{proposed,pending,active,completed,cancelled}/` -- all five lifecycle folders seeded\n- (optional derivative) `SPECIFICATION.md` -- read-only human-readable export (includes deprecated-redirect sentinel; via `task spec:render` if invoked)\n- Prototype code (may be discarded)\n- Findings summary (inline in the proposed/ vBRIEF or as a separate note)\n\n---\n\n## Fits into Chaining Gate\n\nRapid is a **spec-generating** strategy. Selecting it at the chaining gate produces date-prefixed story vBRIEF(s) in `vbrief/proposed/` (and refreshes `vbrief/PROJECT-DEFINITION.vbrief.json` via `task project:render`) and moves directly to implementation. There is no chaining back to preparatory strategies. See the v0.20 Output Shape section below for the exact contract.\n\n---\n\n## Literal acceptance-command verification (#3267)\n\nRapid lightens cold ceremony only. When the task statement names exact shell acceptance commands:\n\n- ! Capture them at intake as executable AC (`plan.metadata.literal_acceptance_commands`) — do not paraphrase.\n- ! Before done, run `task verify:literal-ac -- <active-story-path>` (verbatim flags/cwd). Fail closed on non-zero.\n- ! This check survives ceremony dial rapid/minimal (#3214) — it is the positive verification content of the light path.\n- ⊗ Treat self-chosen approximate checks as sufficient when the statement named exact commands.\n\n## Anti-Patterns\n\n- ⊗ Using rapid for production features -- rapid output is explicitly throwaway\n- ⊗ Graduating a rapid prototype without a fresh spec cycle -- the shortcuts compound\n- ⊗ Running the full interview or PRD path -- that's interview strategy, not rapid\n- ⊗ Skipping `task check` -- even prototypes must pass basic quality checks\n- ⊗ Omitting the goal statement -- undirected spikes waste time\n- ⊗ Emitting the legacy singular specification artifact or bare-named vBRIEFs -- violates the v0.20 contract (see strategies/v0-20-contract.md)\n\n---\n\n## v0.20 Output Shape (s5-migrate-speckit-rapid-enterprise / #1166)\n\nThis strategy has been migrated to the full v0.20 output shape so rapid-generated projects are accepted by the build skill Pre-Cutover Detection Guard with zero errors on first attempt (resolves the rapid row from the #1166 inconsistency table and the s5 story acceptance criteria).\n\n- ! Seed the five lifecycle folders under `vbrief/` if any are missing: `proposed/`, `pending/`, `active/`, `completed/`, `cancelled/`.\n- ! Emit story scope items exclusively as date-prefixed scope vBRIEFs: `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` (use the run's creation date for the prefix; choose descriptive slugs). Decompose the rapid plan into one or more focused, buildable story vBRIEFs (v0.6 schema) rather than a monolithic legacy spec.\n- ! After the proposed/ stories are written, invoke `task project:render` (run from the repo root) to generate/refresh the complete `vbrief/PROJECT-DEFINITION.vbrief.json` (items registry is derived from the lifecycle folders).\n- ⊗ Never emit `vbrief/specification.vbrief.json` (or any legacy dual-write).\n- ~ `SPECIFICATION.md` at the project root, if produced at all, must be only a read-only derivative (e.g. via `task spec:render` after the vbriefs exist) that includes the v0.20 deprecated-redirect sentinel. The source of truth is the vbrief/ lifecycle stories + PROJECT-DEFINITION.\n- ! Before writing any proposed/ stories or PROJECT-DEFINITION, follow the guards in [artifact-guards.md](./artifact-guards.md) (Preparatory Guard for scope items in proposed/; Spec-Generating Guard for PROJECT-DEFINITION).\n- ! Final output tree must pass the deterministic v0.20 strategy output validation gate (s2-deterministic-gate) and the build Pre-Cutover Detection Guard with zero warnings/errors. See full acceptance in the s5 vBRIEF and the 1166 decomposition.\n- ! Cite the canonical contract `strategies/v0-20-contract.md` (s1-contract) for the exact shape and the per-strategy table row.\n\n---\n\n## Artifacts Summary (v0.20)\n\n**Rapid (Forced-Light path):**\n\n| Artifact | Purpose | Created By |\n|----------|---------|------------|\n| `vbrief/proposed/YYYY-MM-DD-*.vbrief.json` (one or more) | Focused story scope items (date-prefixed per vbrief convention and v0.20 contract) | Rapid |\n| `vbrief/PROJECT-DEFINITION.vbrief.json` | Project identity gestalt + complete scope items registry | `task project:render` (invoked by Rapid) |\n| `vbrief/{proposed,pending,active,completed,cancelled}/` | All five lifecycle folders seeded | Rapid |\n| (optional derivative) `SPECIFICATION.md` | Human-readable plan (includes deprecated-redirect sentinel) | `task spec:render` (if invoked) |\n\n**Pre-v0.20 / legacy artifacts that MUST NOT be produced by this strategy:**\n\n- `vbrief/specification.vbrief.json`\n- Primary handoff `SPECIFICATION.md` at project root (without sentinel)\n- Bare-named vBRIEFs in proposed/\n\nSee the full table and rules in `strategies/v0-20-contract.md` (rapid row: Must Create Lifecycle Folders: Yes; Must Write PROJECT-DEFINITION: Yes; Scope vBRIEFs Location: proposed/YYYY-MM-DD-*.vbrief.json only; specification.vbrief.json: Never; SPECIFICATION.md / PROJECT.md: Omit or deprecation redirect only).\n\n---\n\n## Invoking This Strategy\n\n```\n /deft:run:rapid [project name]\n```\n\nOr explicitly:\n"
114
+ "body": "# Rapid Strategy\n\nQuick prototyping workflow -- v0.20 date-prefixed story vBRIEF output with minimal gates and fast iteration.\n\n**Ceremony dial (#3214):** when `plan.policy.ceremonyDial` (or session inputs) selects depth `rapid` — default for S-task × frontier model on a project-shaped session — session ritual / gate depth follows this light path. See `task policy:show --field=ceremonyDial`. Non-project sessions select `minimal` and compose the #3014 minimal AGENTS profile research pointer instead.\n\n**v0.20 note (s5-migrate-speckit-rapid-enterprise / #1166):** Rapid now emits only the canonical v0.20 shape (date-prefixed story vBRIEFs in proposed/, full PROJECT-DEFINITION.vbrief.json via task project:render, seeded lifecycle folders, no legacy specification.vbrief.json). See the dedicated ## v0.20 Output Shape section and the canonical contract `strategies/v0-20-contract.md` (s1-contract of #1166).\n\nLegend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.\n\n**⚠️ See also**: [strategies/interview.md](./interview.md) | [strategies/yolo.md](./yolo.md) | [strategies/README.md](./README.md) | [strategies/v0-20-contract.md](./v0-20-contract.md) | [artifact-guards.md](./artifact-guards.md)\n\n> Ship a throwaway prototype fast. Skip the PRD, skip approval gates, produce a\n> date-prefixed vBRIEF in proposed/ + PROJECT-DEFINITION and start building. Suited for spikes, proof-of-concepts, and\n> disposable experiments where learning speed matters more than long-term quality.\n\n---\n\n## When to Use\n\n- ~ Throwaway prototypes, spikes, and proof-of-concept experiments\n- ~ Validating a technical approach before committing to a full spec cycle\n- ~ Solo explorations where the cost of rework is low\n- ? Time-boxed experiments (e.g. \"spend 2 hours proving this works\")\n- ⊗ Production features, shared libraries, or anything with downstream consumers\n\n---\n\n## Workflow\n\n### Step 1: State the Goal\n\n! Describe the prototype goal in one sentence: what are you trying to learn or prove?\n\n- ! Record the goal in a new date-prefixed story vBRIEF written to `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` (e.g. as the plan title or a brief narrative in the v0.6 schema). Use today's date for the prefix and a descriptive slug.\n- ~ Include a time-box if applicable (e.g. \"4-hour spike\")\n- ⊗ Skip this step -- even throwaway work needs a clear objective\n\n### Step 2: Minimal Interview\n\n! Ask only the questions needed to unblock implementation. Skip sizing gate, skip PRD.\n\n- ! Identify: target platform, primary language, key dependency or API\n- ~ 3-5 questions maximum -- bias toward defaults and moving fast\n- ⊗ Run the full interview questionnaire -- that defeats the purpose of rapid\n\n### Step 3: Generate date-prefixed vBRIEF in proposed/ (Forced-Light Path)\n\n! Before writing output artifacts, follow the guards in [artifact-guards.md](./artifact-guards.md) (Preparatory Guard for proposed/ scope items; Spec-Generating Guard for PROJECT-DEFINITION).\n\n! Write a slim story scope vBRIEF to `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` -- no PRD, no approval gate. Decompose the rapid plan into one or more focused, buildable story vBRIEFs (v0.6 schema) rather than a monolithic legacy spec.\n\n- ! Use the Light path from [interview.md](./interview.md) unconditionally\n- ! Mark `plan.status` as `draft` (not `approved`) to signal prototype quality\n- ! After the proposed/ vBRIEF(s) are written, invoke `task project:render` (from repo root) to generate/refresh the complete `vbrief/PROJECT-DEFINITION.vbrief.json`\n- ~ Keep tasks coarse-grained -- 3-5 tasks is typical for a spike\n- ⊗ Generate a PRD or require approval -- rapid skips both\n- ⊗ Hand-author `SPECIFICATION.md` directly -- it is a rendered derivative only (see v0.20 Output Shape)\n\n! After emitting the proposed/ story vBRIEF(s), surface the GitHub-issue tracking hint from [emit-hints.md](./emit-hints.md) — name all three patterns (none / `--umbrella` / `--per-vbrief`).\n\n### Step 4: Build\n\n! Implement against the spec. Quality gates are relaxed but not absent.\n\n- ! Tests are still required, but coverage gate is relaxed to ≥50%\n- ~ Favour working code over clean code -- refactor later if the prototype graduates\n- ! `task check` must still pass (lint + fmt + tests)\n- ⊗ Skip `task check` entirely -- even prototypes must compile and pass basic checks\n\n### Step 5: Evaluate\n\n! At the end of the time-box (or when the prototype is done), decide next steps.\n\n- ! Record findings: what worked, what didn't, what surprised you\n- ~ Options: discard, iterate, or graduate to a full spec cycle via [interview.md](./interview.md)\n- ! If graduating: start a fresh interview -- do not carry forward the rapid spec as-is\n\n---\n\n## Output Artifacts\n\n- `vbrief/proposed/YYYY-MM-DD-*.vbrief.json` (one or more) -- lightweight story scope vBRIEF(s) with `draft` status (primary artifact; date-prefixed per v0.20 contract)\n- `vbrief/PROJECT-DEFINITION.vbrief.json` -- complete project gestalt + items registry (via `task project:render` invoked by Rapid)\n- `vbrief/{proposed,pending,active,completed,cancelled}/` -- all five lifecycle folders seeded\n- (optional derivative) `SPECIFICATION.md` -- read-only human-readable export (includes deprecated-redirect sentinel; via `task spec:render` if invoked)\n- Prototype code (may be discarded)\n- Findings summary (inline in the proposed/ vBRIEF or as a separate note)\n\n---\n\n## Fits into Chaining Gate\n\nRapid is a **spec-generating** strategy. Selecting it at the chaining gate produces date-prefixed story vBRIEF(s) in `vbrief/proposed/` (and refreshes `vbrief/PROJECT-DEFINITION.vbrief.json` via `task project:render`) and moves directly to implementation. There is no chaining back to preparatory strategies. See the v0.20 Output Shape section below for the exact contract.\n\n---\n\n## Product-first done-gate / literal AC (#3284 / #3267)\n\nRapid lightens cold ceremony only. The **positive verification content** of the light path is stated acceptance commands — not framework hygiene.\n\n- ! Capture stated shell commands at intake into `plan.acceptance.commands` (and #3267 `literal_acceptance_commands`) — do not paraphrase. Empty requires `none_stated: true` with a ladder rung (`derived` or `project_floor`).\n- ! Before done, run `task verify:ac -- <active-story-path>` (verbatim flags/cwd). Fail closed on non-zero. (`verify:literal-ac` remains the #3267 mechanism alias.)\n- ! Ceremony dial rapid/minimal = **AC-only** verification for `task check` composition (#3284): hygiene degrades away; AC never degrades when commands exist.\n- ⊗ Treat self-chosen approximate checks as sufficient when the statement named exact commands.\n- ⊗ Skip AC because ceremony is rapid — rapid's job is to keep exactly this check.\n\n## Anti-Patterns\n\n- ⊗ Using rapid for production features -- rapid output is explicitly throwaway\n- ⊗ Graduating a rapid prototype without a fresh spec cycle -- the shortcuts compound\n- ⊗ Running the full interview or PRD path -- that's interview strategy, not rapid\n- ⊗ Skipping `task check` -- even prototypes must pass basic quality checks\n- ⊗ Omitting the goal statement -- undirected spikes waste time\n- ⊗ Emitting the legacy singular specification artifact or bare-named vBRIEFs -- violates the v0.20 contract (see strategies/v0-20-contract.md)\n\n---\n\n## v0.20 Output Shape (s5-migrate-speckit-rapid-enterprise / #1166)\n\nThis strategy has been migrated to the full v0.20 output shape so rapid-generated projects are accepted by the build skill Pre-Cutover Detection Guard with zero errors on first attempt (resolves the rapid row from the #1166 inconsistency table and the s5 story acceptance criteria).\n\n- ! Seed the five lifecycle folders under `vbrief/` if any are missing: `proposed/`, `pending/`, `active/`, `completed/`, `cancelled/`.\n- ! Emit story scope items exclusively as date-prefixed scope vBRIEFs: `vbrief/proposed/YYYY-MM-DD-<kebab-slug>.vbrief.json` (use the run's creation date for the prefix; choose descriptive slugs). Decompose the rapid plan into one or more focused, buildable story vBRIEFs (v0.6 schema) rather than a monolithic legacy spec.\n- ! After the proposed/ stories are written, invoke `task project:render` (run from the repo root) to generate/refresh the complete `vbrief/PROJECT-DEFINITION.vbrief.json` (items registry is derived from the lifecycle folders).\n- ⊗ Never emit `vbrief/specification.vbrief.json` (or any legacy dual-write).\n- ~ `SPECIFICATION.md` at the project root, if produced at all, must be only a read-only derivative (e.g. via `task spec:render` after the vbriefs exist) that includes the v0.20 deprecated-redirect sentinel. The source of truth is the vbrief/ lifecycle stories + PROJECT-DEFINITION.\n- ! Before writing any proposed/ stories or PROJECT-DEFINITION, follow the guards in [artifact-guards.md](./artifact-guards.md) (Preparatory Guard for scope items in proposed/; Spec-Generating Guard for PROJECT-DEFINITION).\n- ! Final output tree must pass the deterministic v0.20 strategy output validation gate (s2-deterministic-gate) and the build Pre-Cutover Detection Guard with zero warnings/errors. See full acceptance in the s5 vBRIEF and the 1166 decomposition.\n- ! Cite the canonical contract `strategies/v0-20-contract.md` (s1-contract) for the exact shape and the per-strategy table row.\n\n---\n\n## Artifacts Summary (v0.20)\n\n**Rapid (Forced-Light path):**\n\n| Artifact | Purpose | Created By |\n|----------|---------|------------|\n| `vbrief/proposed/YYYY-MM-DD-*.vbrief.json` (one or more) | Focused story scope items (date-prefixed per vbrief convention and v0.20 contract) | Rapid |\n| `vbrief/PROJECT-DEFINITION.vbrief.json` | Project identity gestalt + complete scope items registry | `task project:render` (invoked by Rapid) |\n| `vbrief/{proposed,pending,active,completed,cancelled}/` | All five lifecycle folders seeded | Rapid |\n| (optional derivative) `SPECIFICATION.md` | Human-readable plan (includes deprecated-redirect sentinel) | `task spec:render` (if invoked) |\n\n**Pre-v0.20 / legacy artifacts that MUST NOT be produced by this strategy:**\n\n- `vbrief/specification.vbrief.json`\n- Primary handoff `SPECIFICATION.md` at project root (without sentinel)\n- Bare-named vBRIEFs in proposed/\n\nSee the full table and rules in `strategies/v0-20-contract.md` (rapid row: Must Create Lifecycle Folders: Yes; Must Write PROJECT-DEFINITION: Yes; Scope vBRIEFs Location: proposed/YYYY-MM-DD-*.vbrief.json only; specification.vbrief.json: Never; SPECIFICATION.md / PROJECT.md: Omit or deprecation redirect only).\n\n---\n\n## Invoking This Strategy\n\n```\n /deft:run:rapid [project name]\n```\n\nOr explicitly:\n"
115
115
  },
116
116
  {
117
117
  "id": "research",
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
 
@@ -298,6 +298,22 @@ When a hard turn or cost budget is detectable (session:start `effort_budget` / e
298
298
  Core helper: `packages/core/src/session/effort-budget.ts` (`detectHardEffortBudget`, `recommendVerificationDepth`). Composes #2442, #1581, #3214, #1006.
299
299
  **Enforcement note:** skill defaults are behavioral. Durable delivery/acceptance circuit-breaker: **#3143** `packages/core/src/delivery-attempt/` (`evaluatePreDispatch`, `.deft/delivery-attempts/`). Docs: `docs/delivery-attempt.md`. Route delivery/acceptance automatic retries through that gate; do not invent a parallel ledger in this skill.
300
300
 
301
+ ### AC-pass banking checkpoint - finalize on green (#3285)
302
+
303
+ Sharpens #3266: the **first** moment stated/official acceptance criteria pass is a **banking checkpoint**, not a license to keep spending the turn budget on self-imposed depth.
304
+
305
+ - ! When stated acceptance criteria first pass (`task verify:ac` / product-first done-gate #3284 / official checker), the **next** action is **FINALIZE**: checkpoint-commit the green state and record the bank (durable under `.deft/ac-pass-banks/`; optional run-summary line when `DEFT_RUN_SUMMARY_PATH` is set).
306
+ - ! **Deepening after the bank requires surplus budget.** Self-imposed extra verification, refactors, or polish are permitted only when remaining budget meets `plan.policy.acPassBanking.surplusThreshold` (default **0.2** = 20% of max turns/cost still remaining) **and** the absolute reserve from #3266. Env override: `DEFT_AC_PASS_SURPLUS_THRESHOLD`.
307
+ - ! Deepening, when allowed, happens **on top of** the committed checkpoint so a failed experiment can revert to banked green.
308
+ - ! **Post-bank discoveries are reported, not chased** when surplus is insufficient: file a note/issue in the deliverable for out-of-scope defects unless they **regress stated AC** (then fix-regression). Finding beyond the bar is a win; thrashing a dying budget into a zero is the failure mode this rule closes.
309
+ - ! When surplus is insufficient, ship the banked state and fail-loud (`deepening_skipped=true` + surplus reason) via `evaluateAcPassBanking` / `formatDeepeningSkippedNote` semantics.
310
+ - ~ When no hard budget is detected, dual-stop still applies; bank-on-first-AC-pass remains good discipline but is not a hard surplus gate.
311
+ - ⊗ Convert a banked official pass into a scored failure by chasing post-bank polish until the turn budget dies (#3285).
312
+ - ⊗ Start post-bank deepening without a finalize checkpoint when a hard budget is active (#3285).
313
+ - ⊗ Chase out-of-scope post-bank findings when surplus is below threshold (#3285).
314
+
315
+ Core helpers: `packages/core/src/session/ac-pass-banking.ts` (`evaluateAcPassBanking`, `bankAcPass`, `decidePostBankFinding`, `simulateSurplusInsufficientRun`); policy: `packages/core/src/policy/ac-pass-banking.ts` (`plan.policy.acPassBanking`). Composes #3266, #3284, #3282 (optional bank-event JSONL), #1006.
316
+
301
317
  ## Step 3: Build Phase by Phase
302
318
 
303
319
  For each phase:
@@ -335,19 +351,29 @@ task test:coverage # >=85% or PROJECT-DEFINITION.xbrief.json override
335
351
  - ⊗ Re-run full install/session ceremony after offline seed when ritual is already complete (#3010) — use `session:ready` for recovery only.
336
352
 
337
353
 
338
- ## Literal acceptance-command verification (#3267)
354
+ ## Product-first done-gate (#3284) / literal AC (#3267)
339
355
 
340
- At intake, capture the task statement's **exact** acceptance commands (when present) as executable AC. Before declaring done, run them **verbatim** — same paths, same flags, same working directory. Self-chosen verification is supplementary, never a substitute. Extends #973 machine-verifiable-spec. Required on ceremony dial rapid/minimal (#3214) verification depth is constant (#3156).
356
+ At intake, capture the task statement's **exact** acceptance commands as executable AC (`plan.acceptance.commands` + #3267 `literal_acceptance_commands`). Empty is allowed only with `none_stated: true` (ladder: stated → derived → project_floor). Before declaring done, run them **verbatim** — same paths, same flags, same working directory. Self-chosen verification is supplementary, never a substitute. Extends #973. `task check` runs `verify:ac` **first** (fail-fast); hygiene is second and may become advisory under pressure. Rapid ceremony = **AC-only**.
341
357
 
342
- - ! When reading the active scope xBRIEF / issue body at story start, capture stated shell acceptance commands into `plan.metadata.literal_acceptance_commands` (issue:ingest does this automatically). Do not paraphrase into a different command.
358
+ - ! When reading the active scope xBRIEF / issue body at story start, capture stated shell acceptance commands into `plan.acceptance.commands` (issue:ingest stamps this + the #3267 ledger automatically). Do not paraphrase.
343
359
  - ! Before claiming phase or story done (and before merge-chokepoint PR handoff), run:
344
360
  ```
345
- task verify:literal-ac -- <active-story-path>
361
+ task verify:ac -- <active-story-path>
346
362
  ```
347
- Exit 0 = pass or no commands stated; exit 1 = a stated command failed; exit 2 = config.
363
+ Exit 0 = pass or none stated with valid marker; exit 1 = a stated command failed; exit 2 = config. (`verify:literal-ac` is the #3267 mechanism alias.)
348
364
  - ! Quote the literal invocations and their outputs in the completion note when commands were stated.
349
365
  - ⊗ Substitute a self-chosen approximation (`pnpm test` when the statement said `pnpm exec vitest run packages/core/src`) for the stated command.
350
- - ⊗ Skip this gate because ceremony dial is rapid/minimal — the check survives light ceremony.
366
+ - ⊗ Skip this gate because ceremony dial is rapid/minimal — rapid's positive content is exactly this check (#3284).
367
+ - ⊗ Leave `plan.acceptance.commands` empty without `none_stated: true` — absence must be an explicit decision.
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.
351
377
 
352
378
  ## Operator-log hygiene (lazy-load, #1940)
353
379
 
@@ -491,3 +517,6 @@ Docs: `docs/decision-log.md` · `xbrief/decisions/README.md`.
491
517
  - ⊗ Silently continue after dual-stop failure halt — escalate; do not thrash (#2442)
492
518
  - ⊗ Exhaust hard turn/cost budget on self-imposed deepening after the stated acceptance bar is within reach (#3266)
493
519
  - ⊗ Silently skip deepening for budget without a fail-loud summary note (#3266 / #1006)
520
+ - ⊗ Chase post-bank out-of-scope findings when surplus budget is insufficient — report, do not thrash the banked pass (#3285)
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
@@ -174,11 +174,21 @@ When session effort-budget is hard-capped (`DEFT_MAX_TURNS` / `DEFT_MAX_BUDGET`
174
174
  - ! Dual-stop pre-PR defaults (max 3 polish / 2 no-diff) still apply; under a hard cap, prefer fewer polish iterations once AC + check are green.
175
175
  - ⊗ Start an unbounded self-imposed verification suite that exceeds stated AC while the hard budget is nearly exhausted (#3266).
176
176
 
177
- ## Literal acceptance-command verification (#3267)
177
+ ## Product-first done-gate (#3284) / literal AC (#3267)
178
178
 
179
- - ! Before Phase 3 Lint exit / push, when the active scope xBRIEF has stated acceptance commands (metadata or narratives), run `task verify:literal-ac -- <active-xbrief>` and fail closed on exit 1.
179
+ - ! Before Phase 3 Lint exit / push, when the active scope xBRIEF has acceptance commands (`plan.acceptance` or #3267 metadata/narratives), run `task verify:ac -- <active-xbrief>` and fail closed on exit 1. (`verify:literal-ac` is the mechanism alias.)
180
180
  - ! Commands must be the exact strings from the task statement — same flags and cwd. Self-chosen verification is supplementary only.
181
- - Skip literal AC run on rapid/minimal ceremony dial #3267 is required at every depth.
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
+ - ⊗ Skip AC run on rapid/minimal ceremony dial — rapid = AC-only; AC never degrades when commands exist (#3284 / #3156).
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.
182
192
 
183
193
  ## Probe-then-fill remote claims (#3120)
184
194
 
@@ -227,5 +237,6 @@ Docs: `docs/decision-log.md`.
227
237
  - ⊗ Invent remote PR/SHA/CI/review claims in handoff evidence without same-turn probe binding — invented-done (#3120)
228
238
  - ⊗ Fill remote ship/gate fields from memory when only local work completed; legal partial omits PR fields (#3120)
229
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)
230
241
  - ⊗ Under a hard turn/cost budget, gold-plate pre-PR polish past the stated bar until the budget expires (#3266)
231
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.
@@ -101,14 +101,15 @@ Rapid is a **spec-generating** strategy. Selecting it at the chaining gate produ
101
101
 
102
102
  ---
103
103
 
104
- ## Literal acceptance-command verification (#3267)
104
+ ## Product-first done-gate / literal AC (#3284 / #3267)
105
105
 
106
- Rapid lightens cold ceremony only. When the task statement names exact shell acceptance commands:
106
+ Rapid lightens cold ceremony only. The **positive verification content** of the light path is stated acceptance commands — not framework hygiene.
107
107
 
108
- - ! Capture them at intake as executable AC (`plan.metadata.literal_acceptance_commands`) — do not paraphrase.
109
- - ! Before done, run `task verify:literal-ac -- <active-story-path>` (verbatim flags/cwd). Fail closed on non-zero.
110
- - ! This check survives ceremony dial rapid/minimal (#3214) it is the positive verification content of the light path.
108
+ - ! Capture stated shell commands at intake into `plan.acceptance.commands` (and #3267 `literal_acceptance_commands`) — do not paraphrase. Empty requires `none_stated: true` with a ladder rung (`derived` or `project_floor`).
109
+ - ! Before done, run `task verify:ac -- <active-story-path>` (verbatim flags/cwd). Fail closed on non-zero. (`verify:literal-ac` remains the #3267 mechanism alias.)
110
+ - ! Ceremony dial rapid/minimal = **AC-only** verification for `task check` composition (#3284): hygiene degrades away; AC never degrades when commands exist.
111
111
  - ⊗ Treat self-chosen approximate checks as sufficient when the statement named exact commands.
112
+ - ⊗ Skip AC because ceremony is rapid — rapid's job is to keep exactly this check.
112
113
 
113
114
  ## Anti-Patterns
114
115
 
@@ -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"], {