dev-loops 0.6.0 → 0.7.2

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.
Files changed (203) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +8 -7
  3. package/.claude/agents/fixer.md +1 -0
  4. package/.claude/agents/refiner.md +2 -2
  5. package/.claude/agents/review.md +4 -3
  6. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  7. package/.claude/commands/loop-continue.md +15 -0
  8. package/.claude/commands/loop-enqueue.md +22 -0
  9. package/.claude/commands/loop-grill.md +17 -0
  10. package/.claude/commands/{info.md → loop-info.md} +2 -2
  11. package/.claude/commands/loop-queue-status.md +24 -0
  12. package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
  13. package/.claude/commands/{start.md → loop-start.md} +1 -1
  14. package/.claude/commands/{status.md → loop-status.md} +1 -1
  15. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  16. package/.claude/hooks/_hook-decisions.mjs +138 -15
  17. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  18. package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
  19. package/.claude/skills/dev-loop/SKILL.md +16 -12
  20. package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
  21. package/.claude/skills/docs/anti-patterns.md +5 -3
  22. package/.claude/skills/docs/artifact-authority-contract.md +30 -11
  23. package/.claude/skills/docs/confirmation-rules.md +1 -0
  24. package/.claude/skills/docs/contract-style-guide.md +37 -0
  25. package/.claude/skills/docs/copilot-loop-operations.md +19 -15
  26. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  27. package/.claude/skills/docs/debt-remediation-contract.md +1 -1
  28. package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
  29. package/.claude/skills/docs/issue-intake-procedure.md +26 -19
  30. package/.claude/skills/docs/local-planning-flow.md +1 -1
  31. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  32. package/.claude/skills/docs/merge-preconditions.md +30 -12
  33. package/.claude/skills/docs/plan-file-contract.md +1 -1
  34. package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
  35. package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
  36. package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
  37. package/.claude/skills/docs/spike-mode-contract.md +14 -7
  38. package/.claude/skills/docs/stop-conditions.md +30 -15
  39. package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
  40. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  41. package/.claude/skills/docs/validation-policy.md +4 -5
  42. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  43. package/.claude/skills/local-implementation/SKILL.md +62 -145
  44. package/.claude/skills/loop-grill/SKILL.md +163 -0
  45. package/AGENTS.md +1 -0
  46. package/CHANGELOG.md +101 -0
  47. package/README.md +9 -9
  48. package/agents/dev-loop.agent.md +11 -10
  49. package/agents/developer.agent.md +1 -1
  50. package/agents/docs.agent.md +1 -1
  51. package/agents/fixer.agent.md +2 -1
  52. package/agents/quality.agent.md +1 -1
  53. package/agents/refiner.agent.md +3 -3
  54. package/agents/review.agent.md +5 -4
  55. package/cli/index.mjs +35 -42
  56. package/extension/README.md +4 -4
  57. package/extension/checks.ts +1 -5
  58. package/extension/harness-types.ts +1 -0
  59. package/extension/index.ts +6 -0
  60. package/extension/pi-extension-adapter.ts +2 -0
  61. package/extension/presentation.ts +7 -19
  62. package/package.json +12 -12
  63. package/scripts/_core-helpers.mjs +2 -0
  64. package/scripts/claude/headless-dev-loop.mjs +1 -1
  65. package/scripts/docs/validate-rule-ownership.mjs +442 -0
  66. package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
  67. package/scripts/github/_review-thread-mutations.mjs +5 -2
  68. package/scripts/github/capture-review-threads.mjs +2 -9
  69. package/scripts/github/comment-issue.mjs +2 -9
  70. package/scripts/github/create-label.mjs +133 -0
  71. package/scripts/github/create-pr.mjs +119 -10
  72. package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
  73. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  74. package/scripts/github/edit-pr.mjs +259 -0
  75. package/scripts/github/fetch-ci-logs.mjs +2 -9
  76. package/scripts/github/list-issues.mjs +2 -9
  77. package/scripts/github/list-review-threads.mjs +277 -0
  78. package/scripts/github/manage-sub-issues.mjs +46 -10
  79. package/scripts/github/offer-human-handoff.mjs +8 -5
  80. package/scripts/github/post-gate-findings.mjs +20 -5
  81. package/scripts/github/probe-ci-status.mjs +8 -2
  82. package/scripts/github/probe-copilot-review.mjs +21 -14
  83. package/scripts/github/ready-for-review.mjs +26 -8
  84. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  85. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  86. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  87. package/scripts/github/request-copilot-review.mjs +98 -26
  88. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  89. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  90. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  91. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  92. package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
  93. package/scripts/github/verify-briefing-prefixes.mjs +191 -0
  94. package/scripts/github/verify-fresh-review-context.mjs +226 -32
  95. package/scripts/github/view-pr.mjs +150 -0
  96. package/scripts/github/wait-pr-checks.mjs +171 -0
  97. package/scripts/github/write-gate-context.mjs +458 -65
  98. package/scripts/github/write-gate-findings-log.mjs +128 -2
  99. package/scripts/lib/jq-output.mjs +18 -0
  100. package/scripts/loop/_post-convergence-change.mjs +211 -0
  101. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  102. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  103. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  104. package/scripts/loop/check-retro-tooling.mjs +14 -3
  105. package/scripts/loop/checkpoint-contract.mjs +7 -4
  106. package/scripts/loop/cleanup-worktree.mjs +12 -3
  107. package/scripts/loop/conductor-monitor.mjs +12 -18
  108. package/scripts/loop/copilot-pr-handoff.mjs +162 -14
  109. package/scripts/loop/debt-remediate.mjs +24 -12
  110. package/scripts/loop/detect-change-scope.mjs +38 -9
  111. package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
  112. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  113. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  114. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  115. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  116. package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
  117. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  118. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  119. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  120. package/scripts/loop/ensure-worktree.mjs +8 -3
  121. package/scripts/loop/info.mjs +12 -4
  122. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  123. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  124. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  125. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  126. package/scripts/loop/inspect-run.mjs +8 -2
  127. package/scripts/loop/outer-loop.mjs +8 -4
  128. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  129. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  130. package/scripts/loop/pre-flight-gate.mjs +9 -9
  131. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  132. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  133. package/scripts/loop/provision-worktree.mjs +74 -3
  134. package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
  135. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  136. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  137. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  138. package/scripts/loop/run-queue.mjs +18 -9
  139. package/scripts/loop/run-refinement-audit.mjs +8 -9
  140. package/scripts/loop/run-watch-cycle.mjs +2 -9
  141. package/scripts/loop/sanctioned-commands.mjs +106 -0
  142. package/scripts/loop/steer-loop.mjs +29 -16
  143. package/scripts/loop/validate-pr-body-spec.mjs +223 -0
  144. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  145. package/scripts/pages/build-site.mjs +59 -8
  146. package/scripts/pages/build-state-atlas.mjs +443 -0
  147. package/scripts/projects/_resolve-project.mjs +1 -0
  148. package/scripts/projects/add-queue-item.mjs +60 -54
  149. package/scripts/projects/archive-done-items.mjs +49 -84
  150. package/scripts/projects/ensure-queue-board.mjs +10 -36
  151. package/scripts/projects/list-queue-items.mjs +59 -382
  152. package/scripts/projects/move-queue-item.mjs +21 -449
  153. package/scripts/projects/reconcile-queue.mjs +253 -0
  154. package/scripts/projects/reorder-queue-item.mjs +43 -68
  155. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  156. package/scripts/projects/sync-item-status.mjs +15 -10
  157. package/scripts/refine/_refine-helpers.mjs +21 -5
  158. package/scripts/refine/exit-spike.mjs +18 -8
  159. package/scripts/refine/promote-plan.mjs +22 -16
  160. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  161. package/scripts/refine/refine-plan-file.mjs +13 -4
  162. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  163. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  164. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  165. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  166. package/scripts/refine/validate-plan-file.mjs +1 -1
  167. package/scripts/refine/validate-spike-file.mjs +1 -1
  168. package/scripts/refine/verify.mjs +11 -6
  169. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  170. package/scripts/release/extract-changelog-section.mjs +16 -2
  171. package/skills/copilot-pr-followup/SKILL.md +60 -34
  172. package/skills/dev-loop/SKILL.md +11 -7
  173. package/skills/docs/acceptance-criteria-verification.md +8 -3
  174. package/skills/docs/anti-patterns.md +5 -3
  175. package/skills/docs/artifact-authority-contract.md +30 -11
  176. package/skills/docs/confirmation-rules.md +1 -0
  177. package/skills/docs/contract-style-guide.md +37 -0
  178. package/skills/docs/copilot-loop-operations.md +19 -15
  179. package/skills/docs/cross-harness-regression-contract.md +60 -0
  180. package/skills/docs/debt-remediation-contract.md +1 -1
  181. package/skills/docs/epic-tree-refinement-procedure.md +25 -22
  182. package/skills/docs/issue-intake-procedure.md +26 -19
  183. package/skills/docs/local-planning-flow.md +1 -1
  184. package/skills/docs/local-planning-worked-example.md +1 -1
  185. package/skills/docs/merge-preconditions.md +30 -12
  186. package/skills/docs/plan-file-contract.md +1 -1
  187. package/skills/docs/pr-lifecycle-contract.md +44 -35
  188. package/skills/docs/public-dev-loop-contract.md +60 -44
  189. package/skills/docs/required-rules.json +149 -0
  190. package/skills/docs/retrospective-checkpoint-contract.md +106 -79
  191. package/skills/docs/spike-mode-contract.md +14 -7
  192. package/skills/docs/stop-conditions.md +30 -15
  193. package/skills/docs/tracker-first-loop-state.md +13 -8
  194. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  195. package/skills/docs/validation-policy.md +4 -5
  196. package/skills/docs/workflow-handoff-contract.md +39 -0
  197. package/skills/local-implementation/SKILL.md +62 -145
  198. package/skills/loop-grill/SKILL.md +165 -0
  199. package/.claude/commands/continue.md +0 -15
  200. package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
  201. package/scripts/loop/conductor.mjs +0 -233
  202. package/scripts/loop/detect-stale-runner.mjs +0 -265
  203. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { appendFile, readFile } from "node:fs/promises";
3
3
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
5
+ import { parseArgs } from "node:util";
4
6
  export const INSPECT_RUN_VIEWER_RELEVANT_EXACT_PATHS = Object.freeze([
5
7
  ".github/workflows/ci.yml",
6
8
  "package.json",
7
9
  "package-lock.json",
8
- "playwright.inspect-run-viewer.config.mjs",
10
+ "playwright.config.mjs",
9
11
  "scripts/loop/_inspect-run-viewer-adapter.mjs",
10
12
  "scripts/loop/inspect-run-viewer.mjs",
11
13
  "scripts/loop/inspect-run-viewer-ci-changes.mjs",
@@ -22,9 +24,13 @@ Classify changed files to determine if inspect-run-viewer tests should run.
22
24
  Reads a newline-delimited file list and checks against known relevant paths.
23
25
  Options:
24
26
  --help, -h Show this help
27
+
28
+ ${JQ_OUTPUT_USAGE}
29
+
25
30
  Exit codes:
26
31
  0 Success
27
32
  1 Error
33
+ 2 Invalid --jq filter
28
34
  `;
29
35
  const parseError = buildParseError(USAGE);
30
36
  export function normalizeInspectRunViewerPath(filePath) {
@@ -52,21 +58,27 @@ export function classifyInspectRunViewerCiChanges(changedPaths = []) {
52
58
  }
53
59
  export async function runCli(
54
60
  argv = process.argv.slice(2),
55
- { env = process.env, stdout = process.stdout } = {},
61
+ { env = process.env, stdout = process.stdout, stderr = process.stderr } = {},
56
62
  ) {
57
- if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
63
+ const { values, positionals } = parseArgs({
64
+ args: [...argv],
65
+ options: { help: { type: "boolean", short: "h" }, ...JQ_OUTPUT_PARSE_OPTIONS },
66
+ allowPositionals: true,
67
+ strict: false,
68
+ });
69
+ if (values.help) {
58
70
  stdout.write(HELP);
59
71
  return;
60
72
  }
61
- if (argv.length !== 1) {
73
+ if (positionals.length !== 1) {
62
74
  throw parseError("inspect-run-viewer-ci-changes requires exactly one changed-files path argument");
63
75
  }
64
- const rawPaths = await readFile(argv[0], "utf8");
76
+ const rawPaths = await readFile(positionals[0], "utf8");
65
77
  const result = classifyInspectRunViewerCiChanges(rawPaths.split(/\r?\n/u));
66
78
  if (env.GITHUB_OUTPUT) {
67
79
  await appendFile(env.GITHUB_OUTPUT, `inspect_run_viewer=${result.shouldRun}\n`, "utf8");
68
80
  }
69
- stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
81
+ process.exitCode = emitResult({ ok: true, ...result }, { jq: values.jq, silent: values.silent, stdout, stderr });
70
82
  return result;
71
83
  }
72
84
  if (isDirectCliRun(import.meta.url)) {
@@ -38,6 +38,63 @@ export {
38
38
  resetMermaidBrowserScriptCache,
39
39
  restartExistingPortListener,
40
40
  };
41
+ // ponytail: lifetime timeout defaults to 8h so a forgotten direct-run viewer cannot leak indefinitely; callers pass lifetimeMs to override.
42
+ export const DEFAULT_SERVER_LIFETIME_MS = 8 * 60 * 60 * 1000;
43
+
44
+ // Wires signal handlers + a lifetime timeout so a directly-run viewer server is
45
+ // always torn down instead of leaking across sessions. Returns an idempotent
46
+ // teardown() that closes the server and detaches every handler it installed.
47
+ export function installServerTeardown(
48
+ server,
49
+ {
50
+ signals = ["SIGINT", "SIGTERM"],
51
+ lifetimeMs = DEFAULT_SERVER_LIFETIME_MS,
52
+ onTeardown = () => {},
53
+ processImpl = process,
54
+ setTimeoutImpl = setTimeout,
55
+ clearTimeoutImpl = clearTimeout,
56
+ } = {},
57
+ ) {
58
+ let closed = false;
59
+ let lifetimeTimer = null;
60
+
61
+ const teardown = (reason = "teardown") => {
62
+ if (closed) {
63
+ return;
64
+ }
65
+ closed = true;
66
+ if (lifetimeTimer !== null) {
67
+ clearTimeoutImpl(lifetimeTimer);
68
+ lifetimeTimer = null;
69
+ }
70
+ for (const signal of signals) {
71
+ processImpl.removeListener(signal, signalHandler);
72
+ }
73
+ try {
74
+ onTeardown(reason);
75
+ } finally {
76
+ server.close();
77
+ }
78
+ };
79
+
80
+ function signalHandler(signal) {
81
+ teardown(signal);
82
+ }
83
+
84
+ for (const signal of signals) {
85
+ processImpl.on(signal, signalHandler);
86
+ }
87
+
88
+ if (Number.isFinite(lifetimeMs) && lifetimeMs > 0) {
89
+ lifetimeTimer = setTimeoutImpl(() => teardown("lifetime-timeout"), lifetimeMs);
90
+ // Do not let the teardown timer itself keep the event loop alive.
91
+ if (typeof lifetimeTimer?.unref === "function") {
92
+ lifetimeTimer.unref();
93
+ }
94
+ }
95
+
96
+ return teardown;
97
+ }
41
98
  export async function runCli(
42
99
  argv = process.argv.slice(2),
43
100
  {
@@ -75,8 +132,14 @@ export async function runCli(
75
132
  }
76
133
  const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
77
134
  if (isDirectRun) {
78
- runCli().catch((error) => {
79
- process.stderr.write(`${formatCliError(error)}\n`);
80
- process.exitCode = 1;
81
- });
135
+ runCli()
136
+ .then((server) => {
137
+ if (server) {
138
+ installServerTeardown(server);
139
+ }
140
+ })
141
+ .catch((error) => {
142
+ process.stderr.write(`${formatCliError(error)}\n`);
143
+ process.exitCode = 1;
144
+ });
82
145
  }
@@ -23,6 +23,7 @@ import {
23
23
  } from "@dev-loops/core/loop/steering";
24
24
  import { validateSteeringStateTarget } from "./_steering-state-file.mjs";
25
25
  import { parseArgs } from "node:util";
26
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
26
27
  const USAGE = `Usage: inspect-run.mjs --repo <owner/name> --pr <number>
27
28
  Read-only run inspection for the Copilot PR outer-loop family.
28
29
  Produces a single JSON snapshot describing the current state of one
@@ -68,9 +69,11 @@ Error output (stderr, JSON):
68
69
  { "ok": false, "error": "...", "usage": "..." }
69
70
  Runtime failures:
70
71
  { "ok": false, "error": "..." }
72
+ ${JQ_OUTPUT_USAGE}
71
73
  Exit codes:
72
74
  0 Success
73
- 1 Argument error or unexpected runtime failure`.trim();
75
+ 1 Argument error or unexpected runtime failure
76
+ 2 Invalid --jq filter`.trim();
74
77
  const parseError = buildParseError(USAGE);
75
78
  export function parseInspectRunCliArgs(argv) {
76
79
  const options = {
@@ -90,6 +93,7 @@ export function parseInspectRunCliArgs(argv) {
90
93
  "steering-state-file": { type: "string" },
91
94
  "copilot-input": { type: "string" },
92
95
  "reviewer-input": { type: "string" },
96
+ ...JQ_OUTPUT_PARSE_OPTIONS,
93
97
  },
94
98
  allowPositionals: true,
95
99
  strict: false,
@@ -126,6 +130,7 @@ export function parseInspectRunCliArgs(argv) {
126
130
  options.reviewerInputPath = requireTokenValue(token, parseError);
127
131
  continue;
128
132
  }
133
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
129
134
  throw parseError(`Unknown argument: ${token.rawName}`);
130
135
  }
131
136
  if (!options.help) {
@@ -380,6 +385,7 @@ export async function runCli(
380
385
  argv = process.argv.slice(2),
381
386
  {
382
387
  stdout = process.stdout,
388
+ stderr = process.stderr,
383
389
  env = process.env,
384
390
  ghCommand = "gh",
385
391
  } = {},
@@ -390,7 +396,7 @@ export async function runCli(
390
396
  return;
391
397
  }
392
398
  const snapshot = await inspectRun(options, { env, ghCommand });
393
- stdout.write(`${JSON.stringify(snapshot)}\n`);
399
+ process.exitCode = emitResult(snapshot, { jq: options.jq, silent: options.silent, stdout, stderr });
394
400
  }
395
401
  const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
396
402
  if (isDirectRun) {
@@ -26,6 +26,7 @@ import {
26
26
  } from "@dev-loops/core/loop/async-start-contract";
27
27
  import { loadDevLoopConfig, resolveConductorModel, resolveAutonomyStopAt, resolveWorkflowConfig } from "@dev-loops/core/config";
28
28
  import { parseArgs } from "node:util";
29
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
29
30
  const USAGE = `Usage: outer-loop.mjs --repo <owner/name> --pr <number>
30
31
  Thin outer-loop wrapper for the Copilot PR remediation loop.
31
32
  Detects current PR state from both the Copilot inner loop and the reviewer
@@ -84,9 +85,11 @@ Error output (stderr, JSON):
84
85
  { "ok": false, "error": "..." }
85
86
  Async-start contract rejection:
86
87
  { "ok": false, "error": "...", "asyncStartContract": "rejected" }
88
+ ${JQ_OUTPUT_USAGE}
87
89
  Exit codes:
88
90
  0 Success
89
- 1 Argument error, gh/git failure, or indeterminate state`.trim();
91
+ 1 Argument error, gh/git failure, or indeterminate state
92
+ 2 Invalid --jq filter`.trim();
90
93
  const parseError = buildParseError(USAGE);
91
94
  export function parseOuterLoopCliArgs(argv) {
92
95
  const options = {
@@ -106,6 +109,7 @@ export function parseOuterLoopCliArgs(argv) {
106
109
  "checkpoint-dir": { type: "string" },
107
110
  "copilot-input": { type: "string" },
108
111
  "reviewer-input": { type: "string" },
112
+ ...JQ_OUTPUT_PARSE_OPTIONS,
109
113
  },
110
114
  allowPositionals: true,
111
115
  strict: false,
@@ -142,6 +146,7 @@ export function parseOuterLoopCliArgs(argv) {
142
146
  options.reviewerInputPath = requireTokenValue(token, parseError);
143
147
  continue;
144
148
  }
149
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
145
150
  throw parseError(`Unknown argument: ${token.rawName}`);
146
151
  }
147
152
  if (!options.help) {
@@ -426,11 +431,10 @@ export async function runCli(
426
431
  }
427
432
  const result = await runOuterLoop(options, { env, ghCommand, gitCommand });
428
433
  if (result.ok === false) {
429
- stderr.write(`${JSON.stringify(result)}\n`);
430
- process.exitCode = 1;
434
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
431
435
  return;
432
436
  }
433
- stdout.write(`${JSON.stringify(result)}\n`);
437
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
434
438
  }
435
439
  const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
436
440
  if (isDirectRun) {
@@ -5,7 +5,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
5
5
  import { parsePrNumber, requireTokenValue } from "../_cli-primitives.mjs";
6
6
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
7
  import { resolveRunId as resolveEnvRunId } from "@dev-loops/core/loop/run-context";
8
- import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
8
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
9
9
  import {
10
10
  assertRunnerOwnership,
11
11
  claimRunnerOwnership,
@@ -88,14 +88,7 @@ function parseCliArgs(argv) {
88
88
  options.requireExisting = true;
89
89
  continue;
90
90
  }
91
- if (token.name === "jq") {
92
- options.jq = requireTokenValue(token, parseError);
93
- continue;
94
- }
95
- if (token.name === "silent") {
96
- options.silent = true;
97
- continue;
98
- }
91
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
99
92
  throw parseError(`Unknown argument: ${token.rawName}`);
100
93
  }
101
94
  const validCommands = new Set(["status", "claim", "takeover", "assert", "release"]);
@@ -5,11 +5,14 @@ import { parseArgs } from "node:util";
5
5
  import {
6
6
  isUnderWorktreePath, parseMainWorktreePath, isMainCheckout,
7
7
  } from "@dev-loops/core/loop/worktree-guard";
8
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
8
9
 
9
10
  const USAGE = `Usage:
10
11
  pre-commit-branch-guard.mjs --expected-branch <name> [--require-worktree] [--block-main-checkout]
11
12
 
12
- Verify the current git branch identity and/or worktree isolation before local commit steps.`;
13
+ Verify the current git branch identity and/or worktree isolation before local commit steps.
14
+
15
+ ${JQ_OUTPUT_USAGE}`;
13
16
 
14
17
  const parseError = buildParseError(USAGE);
15
18
 
@@ -22,6 +25,7 @@ export function parseBranchGuardCliArgs(argv) {
22
25
  "expected-branch": { type: "string" },
23
26
  "require-worktree": { type: "boolean" },
24
27
  "block-main-checkout": { type: "boolean" },
28
+ ...JQ_OUTPUT_PARSE_OPTIONS,
25
29
  },
26
30
  allowPositionals: true,
27
31
  strict: false,
@@ -38,6 +42,7 @@ export function parseBranchGuardCliArgs(argv) {
38
42
  if (token.name === "expected-branch") { options.expectedBranch = requireTokenValue(token, parseError, { flagPattern: /^-/u }); continue; }
39
43
  if (token.name === "require-worktree") { options.requireWorktree = true; continue; }
40
44
  if (token.name === "block-main-checkout") { options.blockMainCheckout = true; continue; }
45
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
41
46
  throw parseError(`Unknown argument: ${token.rawName}`);
42
47
  }
43
48
  if (options.expectedBranch === undefined) { throw parseError("--expected-branch <name> is required"); }
@@ -48,12 +53,15 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
48
53
  const options = parseBranchGuardCliArgs(argv);
49
54
  if (options.help) { stdout.write(`${USAGE}\n`); return { ok: true, help: true }; }
50
55
 
56
+ const emit = (payload, target) => {
57
+ process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout: target, stderr });
58
+ return payload;
59
+ };
60
+
51
61
  const { stdout: branchOutput } = await runCommand(gitCommand, ["branch", "--show-current"], { cwd, env });
52
62
  const currentBranch = branchOutput.trim();
53
63
  if (currentBranch !== options.expectedBranch) {
54
- const payload = { ok: false, error: "branch_mismatch", current: currentBranch, expected: options.expectedBranch };
55
- stderr.write(`${JSON.stringify(payload)}\n`);
56
- return payload;
64
+ return emit({ ok: false, error: "branch_mismatch", current: currentBranch, expected: options.expectedBranch }, stderr);
57
65
  }
58
66
 
59
67
  let worktreeOk = null, mainCheckoutBlocked = null;
@@ -64,22 +72,20 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
64
72
  }
65
73
  if (options.requireWorktree) {
66
74
  worktreeOk = isUnderWorktreePath(cwd);
67
- if (!worktreeOk) { stderr.write(JSON.stringify({ ok: false, error: "not_in_worktree", cwd, requiredPrefix: "tmp/worktrees/" }) + "\n"); return { ok: false, error: "not_in_worktree" }; }
75
+ if (!worktreeOk) { return emit({ ok: false, error: "not_in_worktree", cwd, requiredPrefix: "tmp/worktrees/" }, stderr); }
68
76
  }
69
77
  if (options.blockMainCheckout) {
70
78
  const isMain = isMainCheckout(cwd, mainWorktreePath);
71
79
  mainCheckoutBlocked = !(isMain && !isUnderWorktreePath(cwd));
72
- if (!mainCheckoutBlocked) { stderr.write(JSON.stringify({ ok: false, error: "main_checkout_blocked", cwd, mainWorktree: mainWorktreePath }) + "\n"); return { ok: false, error: "main_checkout_blocked" }; }
80
+ if (!mainCheckoutBlocked) { return emit({ ok: false, error: "main_checkout_blocked", cwd, mainWorktree: mainWorktreePath }, stderr); }
73
81
  }
74
82
  if (!options.requireWorktree) worktreeOk = null;
75
83
  if (!options.blockMainCheckout) mainCheckoutBlocked = null;
76
84
  }
77
85
 
78
- const payload = { ok: true, branch: currentBranch, matched: true, worktreeOk, mainCheckoutBlocked };
79
- stdout.write(`${JSON.stringify(payload)}\n`);
80
- return payload;
86
+ return emit({ ok: true, branch: currentBranch, matched: true, worktreeOk, mainCheckoutBlocked }, stdout);
81
87
  }
82
88
 
83
89
  if (isDirectCliRun(import.meta.url)) {
84
- runCli().then((result) => { if (result?.ok === false) { process.exitCode = 1; } }).catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
90
+ runCli().catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
85
91
  }
@@ -12,6 +12,7 @@ import {
12
12
  isListedWorktree,
13
13
  detectSubagentAvailability,
14
14
  } from "@dev-loops/core/loop/worktree-guard";
15
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
15
16
  const DEVLOOPS_PREFLIGHT_BYPASS_VAR = "DEVLOOPS_PREFLIGHT_BYPASS";
16
17
  const USAGE = `Usage:
17
18
  pre-flight-gate.mjs [--expected-branch <name>] [--check-subagents]
@@ -28,7 +29,9 @@ Violation output (stderr, JSON, exit 1):
28
29
  { "ok": false, "error": "<error_code>", "checks": { ... },
29
30
  "guidance": "<actionable instruction for the agent>" }
30
31
  Bypass:
31
- DEVLOOPS_PREFLIGHT_BYPASS=1 Skip all checks (for development/testing only).`.trim();
32
+ DEVLOOPS_PREFLIGHT_BYPASS=1 Skip all checks (for development/testing only).
33
+
34
+ ${JQ_OUTPUT_USAGE}`.trim();
32
35
  const parseError = buildParseError(USAGE);
33
36
  export function parsePreFlightGateCliArgs(argv) {
34
37
  const options = {
@@ -42,6 +45,7 @@ export function parsePreFlightGateCliArgs(argv) {
42
45
  help: { type: "boolean", short: "h" },
43
46
  "expected-branch": { type: "string" },
44
47
  "check-subagents": { type: "boolean" },
48
+ ...JQ_OUTPUT_PARSE_OPTIONS,
45
49
  },
46
50
  allowPositionals: true,
47
51
  strict: false,
@@ -66,6 +70,7 @@ export function parsePreFlightGateCliArgs(argv) {
66
70
  options.checkSubagents = true;
67
71
  continue;
68
72
  }
73
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
69
74
  throw parseError(`Unknown argument: ${token.rawName}`);
70
75
  }
71
76
  return options;
@@ -189,7 +194,7 @@ export async function runCli(
189
194
  checks: { worktree: true, branch: "skipped", subagents: "skipped" },
190
195
  summary: "pre-flight gate bypassed via DEVLOOPS_PREFLIGHT_BYPASS=1",
191
196
  };
192
- stdout.write(`${JSON.stringify(payload)}\n`);
197
+ process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout, stderr });
193
198
  return payload;
194
199
  }
195
200
  const checks = { worktree: false, branch: "skipped", subagents: "skipped" };
@@ -230,7 +235,7 @@ export async function runCli(
230
235
  guidance: errors.map((e) => e.guidance).join("\n\n"),
231
236
  errors,
232
237
  };
233
- stderr.write(`${JSON.stringify(payload)}\n`);
238
+ process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
234
239
  return payload;
235
240
  }
236
241
  const payload = {
@@ -238,16 +243,11 @@ export async function runCli(
238
243
  checks,
239
244
  summary: "all checks passed",
240
245
  };
241
- stdout.write(`${JSON.stringify(payload)}\n`);
246
+ process.exitCode = emitResult(payload, { jq: options.jq, silent: options.silent, stdout, stderr });
242
247
  return payload;
243
248
  }
244
249
  if (isDirectCliRun(import.meta.url)) {
245
250
  runCli()
246
- .then((result) => {
247
- if (result?.ok === false) {
248
- process.exitCode = 1;
249
- }
250
- })
251
251
  .catch((error) => {
252
252
  process.stderr.write(`${formatCliError(error)}\n`);
253
253
  process.exitCode = 1;
@@ -10,6 +10,7 @@ import {
10
10
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
11
11
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
12
12
  import { parseArgs } from "node:util";
13
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
13
14
 
14
15
  const USAGE = `Usage:
15
16
  pre-pr-ready-gate.mjs --repo <owner/name> --pr <number>
@@ -33,7 +34,8 @@ Output (stdout, JSON on success):
33
34
  }
34
35
 
35
36
  Error output (stderr, JSON):
36
- { "ok": false, "error": "<reason>" }`.trim();
37
+ { "ok": false, "error": "<reason>" }
38
+ ${JQ_OUTPUT_USAGE}`.trim();
37
39
 
38
40
  const parseError = buildParseError(USAGE);
39
41
  const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state } } }`;
@@ -46,6 +48,7 @@ export function parsePrePrReadyGateCliArgs(argv) {
46
48
  help: { type: "boolean", short: "h" },
47
49
  repo: { type: "string" },
48
50
  pr: { type: "string" },
51
+ ...JQ_OUTPUT_PARSE_OPTIONS,
49
52
  },
50
53
  allowPositionals: true,
51
54
  strict: false,
@@ -61,6 +64,7 @@ export function parsePrePrReadyGateCliArgs(argv) {
61
64
  if (token.name === "help") { options.help = true; return options; }
62
65
  if (token.name === "repo") { options.repo = requireTokenValue(token, parseError).trim(); continue; }
63
66
  if (token.name === "pr") { options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError); continue; }
67
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
64
68
  throw parseError(`Unknown argument: ${token.rawName}`);
65
69
  }
66
70
  if (options.repo === undefined || options.pr === undefined) {
@@ -183,11 +187,11 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
183
187
  }
184
188
  const result = await prePrReadyGate(options, runtime);
185
189
  if (!result.ok) {
186
- process.stderr.write(`${JSON.stringify(result)}\n`);
187
- process.exitCode = 1;
190
+ // Failure prints to stderr (existing contract); jq/silent apply there too.
191
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout: process.stderr, stderr: process.stderr });
188
192
  return result;
189
193
  }
190
- process.stdout.write(`${JSON.stringify(result)}\n`);
194
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout: process.stdout, stderr: process.stderr });
191
195
  return result;
192
196
  }
193
197
 
@@ -2,8 +2,9 @@
2
2
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
3
3
  import { requireTokenValue, runCommand } from "../_cli-primitives.mjs";
4
4
  import { parseArgs } from "node:util";
5
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
5
6
 
6
- const USAGE = `Usage: pre-write-remote-freshness-guard.mjs --branch <name>\nRefresh remote branch state before starting local file writes.`;
7
+ const USAGE = `Usage: pre-write-remote-freshness-guard.mjs --branch <name>\nRefresh remote branch state before starting local file writes.\n\n${JQ_OUTPUT_USAGE}`;
7
8
  const parseError = buildParseError(USAGE);
8
9
 
9
10
  export function parseRemoteFreshnessGuardCliArgs(argv) {
@@ -13,6 +14,7 @@ export function parseRemoteFreshnessGuardCliArgs(argv) {
13
14
  options: {
14
15
  help: { type: "boolean", short: "h" },
15
16
  branch: { type: "string" },
17
+ ...JQ_OUTPUT_PARSE_OPTIONS,
16
18
  },
17
19
  allowPositionals: true,
18
20
  strict: false,
@@ -27,6 +29,7 @@ export function parseRemoteFreshnessGuardCliArgs(argv) {
27
29
  }
28
30
  if (token.name === "help") { options.help = true; return options; }
29
31
  if (token.name === "branch") { options.branch = requireTokenValue(token, parseError, { flagPattern: /^-/u }); continue; }
32
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
30
33
  throw parseError(`Unknown argument: ${token.rawName}`);
31
34
  }
32
35
  if (options.branch === undefined) throw parseError("--branch <name> is required");
@@ -39,10 +42,16 @@ export async function runCli(argv = process.argv.slice(2), { stdout = process.st
39
42
  await runCommand(gitCommand, ["fetch", "origin", options.branch], { cwd, env });
40
43
  const { stdout: logOutput } = await runCommand(gitCommand, ["log", `HEAD..origin/${options.branch}`, "--oneline"], { cwd, env });
41
44
  const newCommits = logOutput.split(/\r?\n/u).map(l => l.trim()).filter(l => l.length > 0);
42
- if (newCommits.length === 0) { const p = { ok: true, status: "up_to_date" }; stdout.write(`${JSON.stringify(p)}\n`); return p; }
43
- const p = { ok: false, error: "remote_ahead", newCommits }; stderr.write(`${JSON.stringify(p)}\n`); return p;
45
+ if (newCommits.length === 0) {
46
+ const p = { ok: true, status: "up_to_date" };
47
+ process.exitCode = emitResult(p, { jq: options.jq, silent: options.silent, stdout, stderr });
48
+ return p;
49
+ }
50
+ const p = { ok: false, error: "remote_ahead", newCommits };
51
+ process.exitCode = emitResult(p, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
52
+ return p;
44
53
  }
45
54
 
46
55
  if (isDirectCliRun(import.meta.url)) {
47
- runCli().then(r => { if (r?.ok === false) process.exitCode = 1; }).catch(e => { process.stderr.write(`${formatCliError(e)}\n`); process.exitCode = 1; });
56
+ runCli().catch(e => { process.stderr.write(`${formatCliError(e)}\n`); process.exitCode = 1; });
48
57
  }
@@ -12,6 +12,15 @@
12
12
  * - Idempotent: skips a dest that is already correct.
13
13
  * - Does NOT run npm install (deps belong to `npm ci`-in-worktree).
14
14
  *
15
+ * Also (unconditionally, independent of .devloops): links the workspace
16
+ * package node_modules/@dev-loops/core -> ../../packages/core (relative,
17
+ * pointing at the WORKTREE's own packages/core). Without npm install/ci in a
18
+ * fresh worktree there is no node_modules at all, so scripts/**'s
19
+ * `@dev-loops/core` imports resolve UP-TREE to the nearest ancestor
20
+ * node_modules (the main checkout's) — silently testing main's core instead
21
+ * of the branch's (#1144). Stale/broken links are replaced; a real file/dir
22
+ * already occupying the slot is left alone (dest-conflict, never clobbered).
23
+ *
15
24
  * Prints a JSON summary of actions to stdout. Never throws on a per-entry
16
25
  * problem; exits 0 unless its own arguments are invalid.
17
26
  */
@@ -21,6 +30,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
21
30
  import { requireTokenValue } from "../_cli-primitives.mjs";
22
31
  import { parseArgs } from "node:util";
23
32
  import { loadDevLoopConfig, resolveWorktreeConfig } from "@dev-loops/core/config";
33
+ import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
24
34
 
25
35
  const USAGE = `Usage:
26
36
  provision-worktree.mjs --worktree-path <p> --repo-root <p>
@@ -34,7 +44,9 @@ Optional:
34
44
  Output (stdout, JSON):
35
45
  { "ok": true, "actions": [ { "mode": "copy"|"link"|"skip"|"reject", ... } ],
36
46
  "summary": { "copied": n, "linked": n, "skipped": n, "rejected": n,
37
- "warnings": n } }`.trim();
47
+ "warnings": n } }
48
+
49
+ ${JQ_OUTPUT_USAGE}`.trim();
38
50
 
39
51
  const parseError = buildParseError(USAGE);
40
52
 
@@ -46,6 +58,7 @@ export function parseProvisionWorktreeCliArgs(argv) {
46
58
  help: { type: "boolean", short: "h" },
47
59
  "worktree-path": { type: "string" },
48
60
  "repo-root": { type: "string" },
61
+ ...JQ_OUTPUT_PARSE_OPTIONS,
49
62
  },
50
63
  allowPositionals: true,
51
64
  strict: false,
@@ -66,6 +79,7 @@ export function parseProvisionWorktreeCliArgs(argv) {
66
79
  options.repoRoot = requireTokenValue(token, parseError, { flagPattern: /^-/u });
67
80
  continue;
68
81
  }
82
+ if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
69
83
  throw parseError(`Unknown argument: ${token.rawName}`);
70
84
  }
71
85
  if (!options.worktreePath) throw parseError("Missing required --worktree-path");
@@ -156,6 +170,45 @@ async function provisionLink(src, dest, logWarn) {
156
170
  return { mode: "link", src, dest };
157
171
  }
158
172
 
173
+ /**
174
+ * Ensure node_modules/@dev-loops/core -> ../../packages/core (relative) in the
175
+ * worktree, pointing at the worktree's OWN packages/core — never the main
176
+ * checkout's (#1144). Idempotent: a correct link is a no-op; a stale/broken
177
+ * link is replaced; a real file/dir at the dest is a dest-conflict skip
178
+ * (never clobbered). node_modules is gitignored repo-wide, so this link is
179
+ * always untracked.
180
+ */
181
+ async function ensureCoreWorkspaceLink(worktreePath, logWarn) {
182
+ const corePkgDir = path.join(worktreePath, "packages", "core");
183
+ const scopeDir = path.join(worktreePath, "node_modules", "@dev-loops");
184
+ const linkPath = path.join(scopeDir, "core");
185
+
186
+ if (!(await pathExists(corePkgDir))) {
187
+ logWarn(`workspace self-link source missing, skipping: ${corePkgDir}`);
188
+ return { mode: "skip", reason: "source-missing", src: corePkgDir, dest: linkPath };
189
+ }
190
+
191
+ const relTarget = path.relative(scopeDir, corePkgDir);
192
+ const existing = await fsp.readlink(linkPath).catch(() => null);
193
+ if (existing !== null) {
194
+ // Only the exact RELATIVE target is idempotent. An absolute (or otherwise
195
+ // differently-spelled) link that happens to resolve correctly is
196
+ // normalized to the relative form — absolute links break when the
197
+ // worktree moves under tmp/.
198
+ if (existing === relTarget) {
199
+ return { mode: "skip", reason: "exists", src: corePkgDir, dest: linkPath };
200
+ }
201
+ await fsp.rm(linkPath, { force: true }); // stale/absolute/broken — replace below
202
+ } else if (await pathExists(linkPath)) {
203
+ logWarn(`workspace self-link dest conflict (not a symlink), skipping: ${linkPath}`);
204
+ return { mode: "skip", reason: "dest-conflict", src: corePkgDir, dest: linkPath };
205
+ }
206
+
207
+ await fsp.mkdir(scopeDir, { recursive: true });
208
+ await fsp.symlink(relTarget, linkPath);
209
+ return { mode: "link", src: corePkgDir, dest: linkPath };
210
+ }
211
+
159
212
  export async function provisionWorktree({ worktreePath, repoRoot }, { loadConfig = loadDevLoopConfig } = {}) {
160
213
  const root = path.resolve(repoRoot);
161
214
  const dst = path.resolve(worktreePath);
@@ -215,6 +268,24 @@ export async function provisionWorktree({ worktreePath, repoRoot }, { loadConfig
215
268
  }
216
269
  }
217
270
 
271
+ // Unconditional (not driven by .devloops, still applies under a fail-closed
272
+ // empty config) — the worktree's own package resolution is a structural
273
+ // need, not an opt-in copy/link entry.
274
+ try {
275
+ const selfLink = await ensureCoreWorkspaceLink(dst, logWarn);
276
+ actions.push({ entry: "node_modules/@dev-loops/core", ...selfLink });
277
+ } catch (err) {
278
+ const msg = (err && err.message) ? err.message : String(err);
279
+ logWarn(`workspace self-link failed, skipping: ${msg}`);
280
+ actions.push({
281
+ entry: "node_modules/@dev-loops/core",
282
+ mode: "skip",
283
+ reason: `link-failed: ${msg}`,
284
+ src: path.join(dst, "packages", "core"),
285
+ dest: path.join(dst, "node_modules", "@dev-loops", "core"),
286
+ });
287
+ }
288
+
218
289
  const summary = { copied: 0, linked: 0, skipped: 0, rejected: 0, warnings: warnings.length };
219
290
  for (const a of actions) {
220
291
  if (a.mode === "copy") summary.copied++;
@@ -225,14 +296,14 @@ export async function provisionWorktree({ worktreePath, repoRoot }, { loadConfig
225
296
  return { ok: true, actions, summary };
226
297
  }
227
298
 
228
- export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
299
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr } = {}) {
229
300
  const options = parseProvisionWorktreeCliArgs(argv);
230
301
  if (options.help) {
231
302
  stdout.write(`${USAGE}\n`);
232
303
  return;
233
304
  }
234
305
  const result = await provisionWorktree(options);
235
- stdout.write(`${JSON.stringify(result)}\n`);
306
+ process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
236
307
  }
237
308
 
238
309
  if (isDirectCliRun(import.meta.url)) {