@deftai/directive-content 0.82.0 → 0.84.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.
@@ -209,14 +209,27 @@ All xBRIEFs (including those read from `xbrief/active/` and any new xBRIEFs this
209
209
  - ! If any required tool is missing, stop and report — do not proceed to Step 3
210
210
  - ⊗ Assume tools are available because the spec references them
211
211
 
212
- ### Step 3: Build Phase by Phase
212
+ ### Gate throughput iteration fast lane vs merge chokepoint (#1704)
213
+
214
+ > **Invariant:** every change MUST pass the full gate at least once before merge. Iteration MAY use a cheaper proxy; the merge chokepoint MUST NOT be skipped.
215
+
216
+ - ! **Iteration lane (agents + humans):** during implementation commits, use affected/static gates — targeted tests on changed paths (`vitest run --coverage <paths>` or project equivalent), static `verify:*` gates relevant to touched files, and `task coverage:hotspots` / `task verify:forward-coverage` — NOT full `task check` on every commit.
217
+ - ! **Merge chokepoint:** run full `task check` (or `task check:merge` in the framework source repo) once before push/PR and again when CI merge gate runs. Pre-PR skill exit and review-cycle fix batches still require a green full gate.
218
+ - ! **Escape-rate safety (#1703 Tier-1):** before tightening fast-lane defaults fleet-wide, consult `#1703` measurement — `task eval:health` (Tier 0) and Tier-1 session telemetry (`helped/crud-metrics.jsonl` via instrumented CRUD / workflow metrics). Do NOT invent a separate fast-lane escape-rate surface (#1704 LockedDecisions).
219
+ - ~ **In-engine incrementality (#1713):** content-hash task cache and runner-delegated affected selection are sibling work — not required for this policy face.
220
+ - ⊗ Run full `task check` on every iteration commit when a cheaper proxy suffices — reserve the full gate for PR/merge (#1704).
221
+ - ⊗ Skip the merge chokepoint because the iteration lane passed — the fast lane is convenience only.
222
+
223
+ **Cost model (swarm-heavy path):** moves from roughly `O(commits × full-gate)` toward `O(merges × full-gate) + O(iterations × cheap-proxy)` when workers iterate with affected/static gates and run full `task check` only at PR/merge.
224
+
225
+ ## Step 3: Build Phase by Phase
213
226
 
214
227
  For each phase:
215
228
 
216
229
  1. ! **Scaffold** — file structure, dependencies, config
217
230
  2. ! **Test first** — write tests before implementation (TDD)
218
231
  3. ! **Implement** — make tests pass, following deft coding standards
219
- 4. ! **Verify** — run `task check`, fix any issues
232
+ 4. ! **Verify (iteration lane)** — run affected/static gates per `#1704` fast lane above; fix failures before checkpoint commits
220
233
  5. ! **Origin sync** — when this phase materially changed an origin-linked scope xBRIEF (`plan.references` includes `x-xbrief/github-issue`), run `task issue:sync-from-xbrief -- <path>` (or `--dry-run` to preview) so the linked GitHub issue receives a sync comment; if skipped, document why in the PR or session notes (#2540)
221
234
  6. ! **Checkpoint** — tell user what's done, what's next
222
235
 
@@ -224,15 +237,24 @@ For each phase:
224
237
 
225
238
  ### Step 4: Quality Gates
226
239
 
227
- After EVERY phase:
240
+ After EVERY phase (iteration lane — #1704):
241
+
242
+ ```bash
243
+ vitest run --coverage <changed-paths> # or project test runner on touched modules
244
+ task coverage:hotspots # branch headroom before merge
245
+ task verify:forward-coverage # new-source coverage (#1310)
246
+ ```
247
+
248
+ Before PR / phase handoff (merge chokepoint):
228
249
 
229
250
  ```bash
230
- task check # Format, lint, type check, test, coverage
251
+ task check # Full gate — format, lint, typecheck, tests, coverage, verify:*
231
252
  task test:coverage # >=85% or PROJECT-DEFINITION.xbrief.json override
232
253
  ```
233
254
 
234
- - ! Phase is NOT done until `task check` passes
255
+ - ! Phase checkpoint commits MAY use the iteration lane; phase is NOT done for PR handoff until full `task check` passes at the merge chokepoint
235
256
  - ⊗ Skip quality gates or claim they passed without running
257
+ - ⊗ Treat iteration-lane green as merge-ready without full `task check`
236
258
 
237
259
  ## Coding Standards (Summary)
238
260
 
@@ -244,7 +266,7 @@ Read full files when you need detail:
244
266
  - ~ Naming: hyphens for filenames unless language idiom dictates otherwise
245
267
  - ! Contracts first: define interfaces/types before implementation
246
268
  - ! Secrets: in `secrets/` dir with `.example` templates; ⊗ secrets in code
247
- - ! Commits: Conventional Commits format; ! run `task check` before every commit
269
+ - ! Commits: Conventional Commits format; ! use iteration fast lane before checkpoint commits; ! run full `task check` at PR/merge chokepoint only (#1704)
248
270
 
249
271
  See `deft/coding/coding.md` and `deft/coding/testing.md` for full rules.
250
272
 
@@ -266,7 +288,7 @@ See `deft/coding/coding.md` and `deft/coding/testing.md` for full rules.
266
288
 
267
289
  - ! Default to one story per branch/PR. Batching multiple stories in one branch requires explicit operator approval and a short rationale.
268
290
  - ! Create a checkpoint commit after each completed story before beginning another story.
269
- - ! Run `task check` before committing
291
+ - ! Use iteration fast lane before checkpoint commits; run full `task check` at PR/merge chokepoint (#1704)
270
292
  - ⊗ Claim checks passed without running them
271
293
 
272
294
  ```
@@ -296,7 +318,7 @@ feat(phase-2): add REST API endpoints with integration tests
296
318
  - ⊗ Implement things not in scope xBRIEF without asking
297
319
  - ⊗ Read every deft file upfront
298
320
  - ⊗ Move to next phase before current passes checks
299
- - ⊗ Make commits without running `task check`
321
+ - ⊗ Make commits without running iteration-lane validation; ⊗ skip full `task check` at PR/merge chokepoint (#1704)
300
322
  - ⊗ Proceed without USER.md -- always run the USER.md Gate first
301
323
  - ⊗ Spawn an implementation agent or invoke a code-writing tool against a xBRIEF that has not passed `task xbrief:preflight` (which wraps `scripts/preflight_implementation.py`) -- always run the Step 0 Implementation Preflight (#810) first; satisfy via `task xbrief:activate <path>`
302
324
  - ⊗ Proceed without `COST-ESTIMATE.md` and a recorded build / rescope / no-build / skip(+reason) decision -- always run the Cost Phase Gate (#739) first
@@ -60,10 +60,20 @@ task verify:plan-sequence -- --target-kind <entry-kind> --target <entry-id>
60
60
  - ! After the PR's review cycle completes successfully, run `task plan-sequence:advance` so "next" resolves to the following entry (or exhausted).
61
61
  - ⊗ Treat skill-chaining or "what's next?" as permission to open a PR outside the current sequence entry.
62
62
 
63
+ ## Gate throughput — iteration fast lane vs merge chokepoint (#1704)
64
+
65
+ > **Invariant:** every change MUST pass the full gate at least once before merge. Pre-PR is the merge chokepoint — NOT every iteration commit.
66
+
67
+ - ! **Iteration lane (Phases 2–3 loop):** use affected/static gates on changed paths — `vitest run --coverage <paths>`, relevant `verify:*` on touched files, `task coverage:hotspots` — instead of full `task check` on every RWLDL iteration.
68
+ - ! **Merge chokepoint (Phase 3 Lint exit + final confirm):** run full `task check` once before push/PR; Phase 3c targeted coverage precedes but does not replace the full gate.
69
+ - ! **Escape-rate safety (#1703 Tier-1):** before recommending fleet-wide fast-lane tightening, cite `#1703` Tier-1 telemetry (`helped/crud-metrics.jsonl`) and `task eval:health` — do NOT invent a separate escape-rate surface.
70
+ - ~ **In-engine incrementality (#1713):** content-hash cache + runner-delegated affected selection are sibling work (#1713).
71
+ - ⊗ Treat Phase 3c targeted coverage alone as PR-ready without full `task check` at the merge chokepoint.
72
+
63
73
  ## When to Use
64
74
 
65
75
  - ! Before pushing a branch for PR creation
66
- - ! After completing implementation but before the final `task check` gate
76
+ - ! After completing implementation but before the final merge-chokepoint `task check` (#1704)
67
77
  - ~ After addressing bot reviewer findings (run one RWLDL pass before pushing the fix batch)
68
78
  - ? During mid-implementation checkpoints on large changes
69
79
 
@@ -95,9 +105,9 @@ Each iteration proceeds through all phases in order. Do NOT skip phases or reord
95
105
 
96
106
  ### Phase 3 -- Lint
97
107
 
98
- ! Run `task check` and fix any failures.
108
+ ! Run the merge-chokepoint gate and fix any failures (#1704).
99
109
 
100
- - ! Run `task check` (fmt + lint + typecheck + tests + coverage)
110
+ - ! Run full `task check` (fmt + lint + typecheck + tests + coverage + verify:*) — the merge chokepoint, not every RWLDL iteration
101
111
  - ! Fix all failures before proceeding to Phase 3b
102
112
  - ~ If a lint fix requires changing a file, that counts as a change for the Loop phase
103
113
 
@@ -27,6 +27,8 @@ Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.
27
27
 
28
28
  - ! `plan.policy.productSignal.enabled` MUST be true (`task policy:show --field=productSignal`)
29
29
  - ! Install-level consent MUST be recorded before outbound submit (`task product-signal:status`)
30
+ - ! Consent schema v2 binds to the configured sinkRepo; legacy v1 consent authorizes only `deftai/product-signal` (#2767)
31
+ - ! Re-consent after changing `plan.policy.productSignal.sinkRepo`; mismatch soft-skips as sink-unconsented
30
32
  - ⊗ Prompt or submit when `enabled` is false
31
33
  - ⊗ Block story work on sink errors — fail open (#2693 D16/D18)
32
34
 
@@ -50,7 +52,7 @@ When enabled, interactive, and consent missing/revoked, ask:
50
52
  >
51
53
  > Reply **yes** to consent, or **no** to decline.
52
54
 
53
- - **yes** → `task product-signal:consent -- --grant`; continue
55
+ - **yes** → `task product-signal:consent -- --grant` (pass `--project-root` when using a custom sinkRepo); continue
54
56
  - **no** → soft-skip; do not badger every turn
55
57
 
56
58
  ## Phase 3 — Interview (≤3 answers + optional NPS)
@@ -325,6 +325,18 @@ git worktree add <path> -b <branch-name> <configured-base-branch>
325
325
 
326
326
  ~ Also prepare plain-text prompt versions for pasting into Warp agent chat or other terminal interfaces.
327
327
 
328
+ ## Gate throughput — iteration fast lane vs merge chokepoint (#1704)
329
+
330
+ > **Invariant:** every change MUST pass the full gate at least once before merge. Swarm workers and human operators share the same commands.
331
+
332
+ - ! **Iteration lane:** during implement/fix loops, run affected/static gates (targeted tests on changed paths, relevant static `verify:*` gates, `task coverage:hotspots`) — NOT full `task check` on every commit.
333
+ - ! **Merge chokepoint:** run full `task check` once before push/PR open; CI enforces the same monolith at merge (#1704). Monitor checkpoints treat "Validating" as iteration-lane OR full gate; push requires full gate green at least once on the branch.
334
+ - ! **Escape-rate safety (#1703 Tier-1):** consult `task eval:health` and Tier-1 session telemetry (`helped/crud-metrics.jsonl`) before fleet-wide fast-lane tightening — do NOT invent a separate escape-rate surface.
335
+ - ~ **In-engine incrementality (#1713):** content-hash cache + runner-delegated affected plumbing is sibling work; this skill owns process policy only.
336
+ - ⊗ Require full `task check` on every swarm iteration commit when affected/static proxies suffice (#1704).
337
+
338
+ **Swarm cost model:** cohort workers move from `O(commits × full-gate)` toward `O(merges × full-gate) + O(iterations × cheap-proxy)` when they iterate with the fast lane and reserve full `task check` for PR/merge.
339
+
328
340
  ## Phase 3 — Launch
329
341
 
330
342
  ### Step 0: Populate the allocation-context consent token (#1378)
@@ -552,7 +564,7 @@ Track each agent through these stages:
552
564
 
553
565
  1. **Reading** — agent is loading AGENTS.md, xBRIEF files, project files (no file changes yet)
554
566
  2. **Implementing** — working tree shows modified files
555
- 3. **Validating** — agent running `task check`
567
+ 3. **Validating** — agent running iteration-lane gates or full `task check` (full gate required before push/PR — #1704)
556
568
  4. **Committed** — new commit(s) in `git log`
557
569
  5. **Pushed** — branch exists on `origin`
558
570
  6. **PR Created** — PR visible via `gh pr list --head <branch>`
@@ -613,7 +625,7 @@ For each agent's PR:
613
625
  All PRs meet ALL of:
614
626
  - Greptile confidence > 3
615
627
  - No P0 or P1 issues remain (P2 issues are non-blocking style suggestions)
616
- - `task check` passed (or equivalent validation completed)
628
+ - `task check` passed at merge chokepoint before push (or equivalent full-gate validation — #1704)
617
629
  - CHANGELOG entries present under `[Unreleased]`
618
630
 
619
631
  ! **Mandatory cohort verifier (#1364):** After every poller (Phase 6 review-cycle sub-agent) reports back, the monitor MUST run `task swarm:verify-review-clean -- <pr-numbers...>` and confirm exit 0 BEFORE evaluating the rest of the Exit Condition or surfacing the Phase 5 -> 6 gate. The verifier re-uses the Greptile rolling-summary parser from `task pr:merge-ready` so the per-PR merge gate and the cohort gate stay in lockstep (a parser fix lands in both surfaces at once). Exit codes: 0 (cohort CLEAN -- all PRs simultaneously have SHA match + confidence > 3 + zero P0/P1 + not errored on current HEAD); 1 (one or more PRs unclean with per-PR diagnostics -- re-dispatch the poller for the unclean PR or address findings, then re-run the verifier); 2 (config error -- empty cohort, malformed xBRIEF glob, gh missing). The verifier is the structural answer to the #1166 swarm execution recurrence where multiple pollers exited with `clean_gate_holdout=confidence` (confidence == 3) and the monitor still raised the Phase 5 -> 6 gate because the trigger keyed on "all pollers have reported back" rather than "every PR in the cohort is objectively CLEAN".
@@ -641,7 +653,7 @@ All PRs meet ALL of:
641
653
  2. ! **Merge-readiness checklist:** Before any `gh pr merge` call, the monitor MUST emit a structured checklist confirming each PR is merge-ready. For each PR, verify and explicitly confirm:
642
654
  - Greptile confidence score > 3
643
655
  - No P0 or P1 issues remaining
644
- - `task check` passed on the branch
656
+ - full `task check` passed on the branch before push (#1704 merge chokepoint)
645
657
  - CHANGELOG.md entry present under `[Unreleased]`
646
658
  - Explicit user approval received for this merge cascade
647
659
 
@@ -918,7 +930,8 @@ When a monitor session crashes or a new session must take over an in-progress sw
918
930
  ```
919
931
  TASK: You must complete N [type] fixes on this branch ([branch-name]) in the deft directive repo.
920
932
  This is a git worktree. Do NOT just read files and stop — you must implement all changes,
921
- run task check, commit, push, create a PR, and run the review cycle.
933
+ run iteration-lane validation during implement/fix loops, full task check before push,
934
+ commit, push, create a PR, and run the review cycle.
922
935
  DO NOT STOP until all steps are complete.
923
936
 
924
937
  STEP 1 — Read directives: Read AGENTS.md, vbrief/vbrief.md, and the assigned xBRIEF(s) from xbrief/active/.
@@ -932,7 +945,7 @@ Task B (xBRIEF: [filename], issue #[N]): [one-paragraph description with specifi
932
945
 
933
946
  [...repeat for each task...]
934
947
 
935
- STEP 3 — Validate: Run task check. Fix any failures.
948
+ STEP 3 — Validate: Use iteration fast lane during commits (affected/static gates). Run full task check once before push/PR (#1704). Fix any failures.
936
949
 
937
950
  STEP 4 — Commit: Add CHANGELOG.md entries under [Unreleased].
938
951
  Commit with message: [type]([scope]): [description] — with bullet-point body.
@@ -948,7 +961,7 @@ CONSTRAINTS:
948
961
  - Do not touch [list files other agents are working on]
949
962
  - New source files (scripts/, src/, cmd/, *.py, *.go) must have corresponding test files in the same PR
950
963
  - Use conventional commits: type(scope): description
951
- - Run task check before every commit
964
+ - Iteration commits: affected/static fast lane only; full task check required before push (#1704)
952
965
  - Never force-push
953
966
  ```
954
967
 
@@ -963,7 +976,7 @@ CONSTRAINTS:
963
976
 
964
977
  ## Push Autonomy
965
978
 
966
- ! Swarm agents operating under this skill with a monitor agent may push, create PRs, and run review cycles autonomously after passing `task check`. The global "never push/commit without explicit user instruction" convention does not apply to swarm agents executing the full STEP 1-6 prompt workflow -- the skill's quality gates (`task check`, Greptile review cycle) replace the interactive confirmation gate.
979
+ ! Swarm agents operating under this skill with a monitor agent may push, create PRs, and run review cycles autonomously after passing full `task check` at the merge chokepoint (#1704). The global "never push/commit without explicit user instruction" convention does not apply to swarm agents executing the full STEP 1-6 prompt workflow -- the skill's quality gates (merge chokepoint `task check`, Greptile review cycle) replace the interactive confirmation gate.
967
980
 
968
981
  ## Anti-Patterns
969
982
 
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Run package.json scripts via pnpm / Corepack without shell-interpolating
6
+ * repository-controlled packageManager pins (#2765 / #2761).
7
+ *
8
+ * Consumed by tasks/engine.yml `:engine:pm-run` and `:engine:_ts-build`.
9
+ * Lives under tasks/ so @deftai/directive-content prepack ships it beside
10
+ * tasks/engine.yml (#2022 Phase 3).
11
+ */
12
+
13
+ const { execFileSync } = require("node:child_process");
14
+ const fs = require("node:fs");
15
+ const path = require("node:path");
16
+
17
+ const WIN32_ALLOWLIST = new Set(["pnpm", "corepack", "npm"]);
18
+ const SHELL_METACHAR_RE = /[;&|`$<>()\\'"!#\n\r\t]/;
19
+ const SEMVER_RE =
20
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
21
+ const SCRIPT_NAME_RE = /^[A-Za-z0-9:_-]+$/;
22
+
23
+ /** @param {string | null | undefined} version */
24
+ function isValidSemVer(version) {
25
+ return typeof version === "string" && SEMVER_RE.test(version);
26
+ }
27
+
28
+ /**
29
+ * @param {unknown} raw
30
+ * @returns {{ ok: true, semver: string | null, pin: string | null } | { ok: false, reason: string }}
31
+ */
32
+ function parsePnpmPin(raw) {
33
+ if (raw === undefined || raw === null || raw === "") {
34
+ return { ok: true, semver: null, pin: null };
35
+ }
36
+ const str = String(raw);
37
+ if (str !== str.trim()) {
38
+ return { ok: false, reason: "packageManager has leading/trailing whitespace" };
39
+ }
40
+ if (/\s/.test(str)) {
41
+ return { ok: false, reason: "packageManager contains whitespace" };
42
+ }
43
+ if (SHELL_METACHAR_RE.test(str)) {
44
+ return { ok: false, reason: "packageManager contains shell metacharacters" };
45
+ }
46
+ if (!str.startsWith("pnpm@")) {
47
+ return { ok: false, reason: "packageManager must be pnpm@<semver> when set" };
48
+ }
49
+ const semver = str.slice("pnpm@".length);
50
+ if (!semver || !isValidSemVer(semver)) {
51
+ return { ok: false, reason: "packageManager semver is invalid" };
52
+ }
53
+ if (str !== `pnpm@${semver}`) {
54
+ return { ok: false, reason: "packageManager pin malformed" };
55
+ }
56
+ return { ok: true, semver, pin: str };
57
+ }
58
+
59
+ /**
60
+ * @param {unknown} name
61
+ * @param {Record<string, unknown> | undefined} scripts
62
+ */
63
+ function validateScriptName(name, scripts) {
64
+ if (typeof name !== "string" || !name || !SCRIPT_NAME_RE.test(name)) {
65
+ return false;
66
+ }
67
+ return !!scripts && Object.prototype.hasOwnProperty.call(scripts, name);
68
+ }
69
+
70
+ /** @param {string} arg */
71
+ function quoteWin32Arg(arg) {
72
+ const s = String(arg);
73
+ if (!/[\s"]/.test(s)) {
74
+ return s;
75
+ }
76
+ return `"${s.replace(/"/g, '""')}"`;
77
+ }
78
+
79
+ /**
80
+ * @param {typeof execFileSync} execFn
81
+ * @param {string} name
82
+ */
83
+ function hasCmd(execFn, name) {
84
+ const spawnOpts = (/** @type {Record<string, unknown>} */ extra) => ({
85
+ stdio: "ignore",
86
+ windowsHide: true,
87
+ ...extra,
88
+ });
89
+ try {
90
+ execFn(name, ["--version"], spawnOpts({}));
91
+ return true;
92
+ } catch {
93
+ try {
94
+ execFn(name, ["--version"], spawnOpts({ shell: true }));
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {typeof execFileSync} execFn
104
+ * @param {string} cmd
105
+ * @param {string[]} args
106
+ * @param {{ cwd: string, spawn?: Record<string, unknown> }} opts
107
+ */
108
+ function executeAllowlisted(execFn, cmd, args, opts) {
109
+ if (!WIN32_ALLOWLIST.has(cmd)) {
110
+ throw new Error(`deft: allowlisted shim required, got ${JSON.stringify(cmd)}`);
111
+ }
112
+ const spawnOverride = opts.spawn && typeof opts.spawn === "object" ? opts.spawn : {};
113
+ const base = {
114
+ cwd: opts.cwd,
115
+ stdio: "inherit",
116
+ windowsHide: true,
117
+ shell: false,
118
+ ...spawnOverride,
119
+ shell: false,
120
+ };
121
+ if (process.platform === "win32") {
122
+ const commandLine = [quoteWin32Arg(cmd), ...args.map(quoteWin32Arg)].join(" ");
123
+ execFn("cmd.exe", ["/d", "/s", "/c", commandLine], base);
124
+ return;
125
+ }
126
+ execFn(cmd, args, base);
127
+ }
128
+
129
+ /**
130
+ * @param {{ hasPnpm: boolean, hasCorepack: boolean, semver: string | null, script: string }} input
131
+ */
132
+ function buildDispatchSteps(input) {
133
+ const steps = [];
134
+ if (input.hasPnpm) {
135
+ steps.push({ cmd: "pnpm", args: ["run", input.script] });
136
+ }
137
+ if (input.hasCorepack && input.semver) {
138
+ steps.push({ cmd: "corepack", args: [`pnpm@${input.semver}`, "run", input.script] });
139
+ }
140
+ if (input.hasCorepack) {
141
+ steps.push({ cmd: "corepack", args: ["pnpm", "run", input.script] });
142
+ }
143
+ return steps;
144
+ }
145
+
146
+ /** @param {string} root */
147
+ function markWarm(root) {
148
+ try {
149
+ const dist = path.join(root, "packages", "cli", "dist");
150
+ fs.mkdirSync(dist, { recursive: true });
151
+ fs.writeFileSync(path.join(dist, ".deft-ts-build-stamp"), new Date().toISOString());
152
+ } catch {
153
+ // best-effort warm marker
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {string} root
159
+ * @param {string} script
160
+ * @param {{ execFileSync?: typeof execFileSync, markWarm?: boolean }} [deps]
161
+ */
162
+ function runPackageScript(root, script, deps = {}) {
163
+ const execFn = deps.execFileSync || execFileSync;
164
+ const pkgPath = path.join(root, "package.json");
165
+ if (!fs.existsSync(pkgPath)) {
166
+ console.error(`deft: package.json missing at ${root}`);
167
+ return 2;
168
+ }
169
+ let pkg;
170
+ try {
171
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
172
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
173
+ console.error(`deft: package.json at ${root} is not a JSON object`);
174
+ return 2;
175
+ }
176
+ pkg = parsed;
177
+ } catch {
178
+ console.error(`deft: package.json at ${root} is not valid JSON`);
179
+ return 2;
180
+ }
181
+ if (!validateScriptName(script, pkg.scripts)) {
182
+ console.error(`deft: package.json has no script ${JSON.stringify(script)}`);
183
+ return 2;
184
+ }
185
+
186
+ const pinResult = parsePnpmPin(pkg.packageManager);
187
+ if (!pinResult.ok) {
188
+ console.error(`deft: invalid packageManager pin — ${pinResult.reason}`);
189
+ return 3;
190
+ }
191
+
192
+ const envPm = String(process.env.DEFT_PACKAGE_MANAGER || "")
193
+ .trim()
194
+ .toLowerCase();
195
+ if (envPm === "npm") {
196
+ try {
197
+ executeAllowlisted(execFn, "npm", ["run", script], { cwd: root });
198
+ if (deps.markWarm) {
199
+ markWarm(root);
200
+ }
201
+ return 0;
202
+ } catch {
203
+ console.error(`deft: npm run ${JSON.stringify(script)} failed`);
204
+ return 1;
205
+ }
206
+ }
207
+
208
+ const hasPnpm = hasCmd(execFn, "pnpm");
209
+ const hasCorepack = hasCmd(execFn, "corepack");
210
+ const steps = buildDispatchSteps({
211
+ hasPnpm,
212
+ hasCorepack,
213
+ semver: pinResult.semver,
214
+ script,
215
+ });
216
+
217
+ for (const step of steps) {
218
+ try {
219
+ executeAllowlisted(execFn, step.cmd, step.args, { cwd: root });
220
+ if (deps.markWarm) {
221
+ markWarm(root);
222
+ }
223
+ return 0;
224
+ } catch {
225
+ // fall through to Corepack / next resolver
226
+ }
227
+ }
228
+
229
+ console.error(`deft: neither pnpm nor corepack is available to run ${JSON.stringify(script)}.`);
230
+ if (pinResult.pin) {
231
+ console.error(
232
+ ` Enable Corepack for the pinned manager: corepack enable && corepack prepare ${pinResult.pin} --activate`,
233
+ );
234
+ } else {
235
+ console.error(" Install pnpm or enable Corepack (see package.json#packageManager).");
236
+ }
237
+ console.error(" Or set DEFT_PACKAGE_MANAGER=npm for an explicit npm build path.");
238
+ return 127;
239
+ }
240
+
241
+ function main() {
242
+ const root = process.argv[2];
243
+ const script = process.argv[3];
244
+ const markWarmFlag = process.argv.includes("--mark-warm");
245
+ if (!root || !script) {
246
+ console.error("deft: engine-pm-run usage: engine-pm-run.cjs <DEFT_ROOT> <script> [--mark-warm]");
247
+ process.exit(2);
248
+ }
249
+ const code = runPackageScript(root, script, { markWarm: markWarmFlag });
250
+ process.exit(code);
251
+ }
252
+
253
+ if (require.main === module) {
254
+ main();
255
+ }
256
+
257
+ module.exports = {
258
+ SHELL_METACHAR_RE,
259
+ WIN32_ALLOWLIST,
260
+ buildDispatchSteps,
261
+ executeAllowlisted,
262
+ hasCmd,
263
+ isValidSemVer,
264
+ markWarm,
265
+ parsePnpmPin,
266
+ quoteWin32Arg,
267
+ runPackageScript,
268
+ validateScriptName,
269
+ };