@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.
- package/Taskfile.yml +7 -0
- package/commands.md +16 -2
- package/docs/product-signal.md +6 -2
- package/docs/task-cache.md +37 -0
- package/package.json +1 -1
- package/packs/skills/skills-pack-0.1.json +4 -4
- package/skills/deft-directive-build/SKILL.md +30 -8
- package/skills/deft-directive-pre-pr/SKILL.md +13 -3
- package/skills/deft-directive-product-signal/SKILL.md +3 -1
- package/skills/deft-directive-swarm/SKILL.md +20 -7
- package/tasks/engine-pm-run.cjs +269 -0
- package/tasks/engine-pm-run.test.cjs +201 -0
- package/tasks/engine.yml +2 -135
|
@@ -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
|
-
###
|
|
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 `
|
|
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 #
|
|
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`
|
|
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
|
-
- !
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
+
};
|