dev-loops 0.6.0 → 0.7.1
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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/review.md +2 -1
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
- package/.claude/skills/dev-loop/SKILL.md +15 -11
- package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
- package/.claude/skills/docs/anti-patterns.md +2 -1
- package/.claude/skills/docs/artifact-authority-contract.md +22 -4
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/issue-intake-procedure.md +1 -0
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +17 -1
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +25 -14
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +95 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +3 -2
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +1 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +1 -1
- package/agents/review.agent.md +3 -2
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +9 -10
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +14 -2
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +8 -2
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
- package/scripts/github/verify-fresh-review-context.mjs +150 -31
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/write-gate-context.mjs +248 -61
- package/scripts/github/write-gate-findings-log.mjs +75 -1
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +135 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +36 -7
- package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +104 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +207 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +441 -0
- package/scripts/projects/_resolve-project.mjs +148 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +116 -65
- package/scripts/projects/move-queue-item.mjs +28 -49
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +41 -47
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +24 -12
- package/skills/dev-loop/SKILL.md +10 -6
- package/skills/docs/acceptance-criteria-verification.md +6 -2
- package/skills/docs/anti-patterns.md +2 -1
- package/skills/docs/artifact-authority-contract.md +22 -4
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/issue-intake-procedure.md +1 -0
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +17 -1
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/public-dev-loop-contract.md +1 -1
- package/skills/docs/retrospective-checkpoint-contract.md +90 -76
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +25 -14
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- 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.
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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()
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
430
|
-
process.exitCode = 1;
|
|
434
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout: stderr, stderr });
|
|
431
435
|
return;
|
|
432
436
|
}
|
|
433
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|
|
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) {
|
|
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
|
-
|
|
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().
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>" }
|
|
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
|
-
|
|
187
|
-
process.exitCode =
|
|
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.
|
|
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) {
|
|
43
|
-
|
|
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().
|
|
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 } }
|
|
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
|
-
|
|
306
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
236
307
|
}
|
|
237
308
|
|
|
238
309
|
if (isDirectCliRun(import.meta.url)) {
|