dev-loops 0.1.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/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { buildParseError, formatCliError, parseJsonText } from "../_core-helpers.mjs";
|
|
7
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
8
|
+
import {
|
|
9
|
+
buildCheckpointFilePath,
|
|
10
|
+
buildDefaultCheckpointDir,
|
|
11
|
+
} from "./_checkpoint-paths.mjs";
|
|
12
|
+
import { readExistingCheckpoint } from "./_checkpoint-io.mjs";
|
|
13
|
+
import { loadCopilotEvidence, loadReviewerEvidence } from "./_loop-evidence.mjs";
|
|
14
|
+
import {
|
|
15
|
+
ENTRYPOINT,
|
|
16
|
+
evaluateConductorRouting,
|
|
17
|
+
LOOP_FAMILY,
|
|
18
|
+
ROUTING_OUTCOME,
|
|
19
|
+
SOURCE_MODE,
|
|
20
|
+
} from "@dev-loops/core/loop/conductor-routing";
|
|
21
|
+
import {
|
|
22
|
+
ASYNC_START_STATUS,
|
|
23
|
+
buildAsyncStartRejection,
|
|
24
|
+
validateAsyncStartContext,
|
|
25
|
+
} from "@dev-loops/core/loop/async-start-contract";
|
|
26
|
+
import { loadDevLoopConfig, resolveConductorModel, resolveAutonomyStopAt, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
27
|
+
const USAGE = `Usage: outer-loop.mjs --repo <owner/name> --pr <number>
|
|
28
|
+
Thin outer-loop wrapper for the Copilot PR remediation loop.
|
|
29
|
+
Detects current PR state from both the Copilot inner loop and the reviewer
|
|
30
|
+
inner loop, decides the outer-loop action, and persists a minimal checkpoint.
|
|
31
|
+
Required:
|
|
32
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
33
|
+
--pr <number> Pull request number
|
|
34
|
+
--checkpoint-dir <dir> Directory for checkpoint artifact
|
|
35
|
+
(default: tmp/copilot-loop/<owner>/<repo>/pr-<n>/)
|
|
36
|
+
--copilot-input <path> Path to a pre-built copilot snapshot JSON
|
|
37
|
+
(skips live copilot detection; for testing)
|
|
38
|
+
--reviewer-input <path> Path to a pre-built reviewer snapshot JSON
|
|
39
|
+
(skips live reviewer detection; for testing)
|
|
40
|
+
Output (stdout, JSON):
|
|
41
|
+
{ "ok": true, "outerAction": "...", "copilotState": "...",
|
|
42
|
+
"reviewerState": "...", "reviewerScope": { "mode": "...",
|
|
43
|
+
"reviewerLogin": "..."|null }, "reason"?: "...",
|
|
44
|
+
"conductorRouting": { "routingOutcome": "...", "outerAction": "...",
|
|
45
|
+
"stopReason": null|"...", "handoffEnvelope": { ... } },
|
|
46
|
+
"checkpoint": { "pr": N, "repo": "...", "outerAction": "...",
|
|
47
|
+
"copilotState": "...", "reviewerState": "...",
|
|
48
|
+
"reviewerScope": "...", "reviewerLogin": "..."|null,
|
|
49
|
+
"reason": null|"...", "timestamp": "...", "waitCycles": N,
|
|
50
|
+
"headSha": "..."|null },
|
|
51
|
+
"conductorModel": "..."|null }
|
|
52
|
+
Outer actions:
|
|
53
|
+
continue_wait Durable outer-loop wait state; re-run after bounded wait
|
|
54
|
+
reenter_copilot_loop Copilot inner loop needs action
|
|
55
|
+
reenter_reviewer_loop Reviewer inner loop needs action
|
|
56
|
+
stop Terminal, blocked, or reconcile-needed; do not proceed
|
|
57
|
+
done PR is merged or closed; loop complete
|
|
58
|
+
Stop reasons:
|
|
59
|
+
pr_not_ready PR does not exist
|
|
60
|
+
copilot_blocked Copilot loop is blocked
|
|
61
|
+
reviewer_blocked Reviewer loop is blocked
|
|
62
|
+
review_unavailable Copilot review is unavailable
|
|
63
|
+
unsafe_local_branch_mismatch_requires_reconcile
|
|
64
|
+
Next step needs PR-local work but local
|
|
65
|
+
branch does not match PR head branch
|
|
66
|
+
unsafe_local_head_mismatch_requires_reconcile
|
|
67
|
+
Next step needs PR-local work but local
|
|
68
|
+
HEAD does not match PR head commit
|
|
69
|
+
unknown_state Unrecognized combined state
|
|
70
|
+
Async-start contract:
|
|
71
|
+
This loop must run within a visible Pi-managed async context when
|
|
72
|
+
workflow.asyncStartMode is set to required (default). It fails closed unless
|
|
73
|
+
PI_SUBAGENT_RUN_ID is set, to prevent hidden detached-process fallback
|
|
74
|
+
(nohup, disowned shell jobs, etc.). Snapshot/test input mode
|
|
75
|
+
(both --copilot-input and --reviewer-input) is exempt. Any relaxed
|
|
76
|
+
async-start posture is maintainer-controlled repository policy, not an
|
|
77
|
+
agent-tunable runtime path.
|
|
78
|
+
Error output (stderr, JSON):
|
|
79
|
+
Argument/usage errors:
|
|
80
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
81
|
+
gh/git/runtime failures:
|
|
82
|
+
{ "ok": false, "error": "..." }
|
|
83
|
+
Async-start contract rejection:
|
|
84
|
+
{ "ok": false, "error": "...", "asyncStartContract": "rejected" }
|
|
85
|
+
Exit codes:
|
|
86
|
+
0 Success
|
|
87
|
+
1 Argument error, gh/git failure, or indeterminate state`.trim();
|
|
88
|
+
const parseError = buildParseError(USAGE);
|
|
89
|
+
export function parseOuterLoopCliArgs(argv) {
|
|
90
|
+
const args = [...argv];
|
|
91
|
+
const options = {
|
|
92
|
+
help: false,
|
|
93
|
+
repo: undefined,
|
|
94
|
+
pr: undefined,
|
|
95
|
+
checkpointDir: undefined,
|
|
96
|
+
copilotInputPath: undefined,
|
|
97
|
+
reviewerInputPath: undefined,
|
|
98
|
+
};
|
|
99
|
+
while (args.length > 0) {
|
|
100
|
+
const token = args.shift();
|
|
101
|
+
if (token === "--help" || token === "-h") {
|
|
102
|
+
options.help = true;
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
if (token === "--repo") {
|
|
106
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (token === "--pr") {
|
|
110
|
+
options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (token === "--checkpoint-dir") {
|
|
114
|
+
options.checkpointDir = requireOptionValue(args, "--checkpoint-dir", parseError);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (token === "--copilot-input") {
|
|
118
|
+
options.copilotInputPath = requireOptionValue(args, "--copilot-input", parseError);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token === "--reviewer-input") {
|
|
122
|
+
options.reviewerInputPath = requireOptionValue(args, "--reviewer-input", parseError);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
126
|
+
}
|
|
127
|
+
if (!options.help) {
|
|
128
|
+
if (options.repo === undefined || options.pr === undefined) {
|
|
129
|
+
throw parseError("outer-loop requires both --repo <owner/name> and --pr <number>");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
parseRepoSlug(options.repo);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return options;
|
|
138
|
+
}
|
|
139
|
+
async function checkGitStatus({ env = process.env, gitCommand = "git" } = {}) {
|
|
140
|
+
const [statusResult, headRefResult, headShaResult] = await Promise.all([
|
|
141
|
+
runChild(gitCommand, ["status", "--porcelain"], env),
|
|
142
|
+
runChild(gitCommand, ["rev-parse", "--abbrev-ref", "HEAD"], env),
|
|
143
|
+
runChild(gitCommand, ["rev-parse", "HEAD"], env),
|
|
144
|
+
]);
|
|
145
|
+
const isDirty = statusResult.code === 0
|
|
146
|
+
? statusResult.stdout.trim().length > 0
|
|
147
|
+
: false;
|
|
148
|
+
const headRef = headRefResult.code === 0
|
|
149
|
+
? headRefResult.stdout.trim()
|
|
150
|
+
: "";
|
|
151
|
+
const isDetached = headRef === "HEAD";
|
|
152
|
+
const branchName = !isDetached && headRef.length > 0 ? headRef : null;
|
|
153
|
+
const headSha = headShaResult.code === 0
|
|
154
|
+
? headShaResult.stdout.trim() || null
|
|
155
|
+
: null;
|
|
156
|
+
return { isDirty, isDetached, branchName, headSha };
|
|
157
|
+
}
|
|
158
|
+
async function fetchPrHeadIdentity({ repo, pr }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
159
|
+
const result = await runChild(
|
|
160
|
+
ghCommand,
|
|
161
|
+
["pr", "view", String(pr), "--repo", repo, "--json", "headRefName,headRefOid"],
|
|
162
|
+
env,
|
|
163
|
+
);
|
|
164
|
+
if (result.code !== 0) {
|
|
165
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
166
|
+
throw new Error(`Failed to read PR head identity: ${detail}`);
|
|
167
|
+
}
|
|
168
|
+
const payload = parseJsonText(result.stdout);
|
|
169
|
+
const branchName = typeof payload.headRefName === "string" && payload.headRefName.trim().length > 0
|
|
170
|
+
? payload.headRefName.trim()
|
|
171
|
+
: null;
|
|
172
|
+
const headSha = typeof payload.headRefOid === "string" && payload.headRefOid.trim().length > 0
|
|
173
|
+
? payload.headRefOid.trim()
|
|
174
|
+
: null;
|
|
175
|
+
return { branchName, headSha };
|
|
176
|
+
}
|
|
177
|
+
function requiresPrLocalIdentityGate(outerAction) {
|
|
178
|
+
return outerAction === "reenter_copilot_loop" || outerAction === "reenter_reviewer_loop";
|
|
179
|
+
}
|
|
180
|
+
function evaluatePrLocalIdentity({
|
|
181
|
+
localBranch,
|
|
182
|
+
localHeadSha,
|
|
183
|
+
prBranch,
|
|
184
|
+
prHeadSha,
|
|
185
|
+
}) {
|
|
186
|
+
const branchMatches = typeof prBranch === "string" && prBranch.length > 0
|
|
187
|
+
? localBranch === prBranch
|
|
188
|
+
: null;
|
|
189
|
+
const headMatches = typeof prHeadSha === "string" && prHeadSha.length > 0
|
|
190
|
+
? localHeadSha === prHeadSha
|
|
191
|
+
: null;
|
|
192
|
+
const mismatchReason = branchMatches === false
|
|
193
|
+
? "unsafe_local_branch_mismatch_requires_reconcile"
|
|
194
|
+
: (headMatches === false ? "unsafe_local_head_mismatch_requires_reconcile" : null);
|
|
195
|
+
return {
|
|
196
|
+
localBranch,
|
|
197
|
+
localHeadSha,
|
|
198
|
+
prBranch,
|
|
199
|
+
prHeadSha,
|
|
200
|
+
branchMatches,
|
|
201
|
+
headMatches,
|
|
202
|
+
mismatchReason,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function buildPrLocalIdentityStopRouting({
|
|
206
|
+
repo,
|
|
207
|
+
pr,
|
|
208
|
+
branchIdentity,
|
|
209
|
+
}) {
|
|
210
|
+
const targetIdentity = { repo, pr };
|
|
211
|
+
const reason = branchIdentity.mismatchReason === "unsafe_local_branch_mismatch_requires_reconcile"
|
|
212
|
+
? `Local branch '${branchIdentity.localBranch ?? "(unknown)"}' does not match PR head branch '${branchIdentity.prBranch ?? "(unknown)"}'; reconcile local branch/worktree before PR-local follow-up.`
|
|
213
|
+
: `Local HEAD '${branchIdentity.localHeadSha ?? "(unknown)"}' does not match PR head '${branchIdentity.prHeadSha ?? "(unknown)"}'; reconcile local branch/worktree before PR-local follow-up.`;
|
|
214
|
+
return {
|
|
215
|
+
routingOutcome: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
216
|
+
outerAction: "stop",
|
|
217
|
+
stopReason: branchIdentity.mismatchReason,
|
|
218
|
+
handoffEnvelope: {
|
|
219
|
+
targetIdentity,
|
|
220
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
221
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
222
|
+
reason,
|
|
223
|
+
requiredArgs: { repo, pr },
|
|
224
|
+
requiresLocalIsolation: false,
|
|
225
|
+
confidence: SOURCE_MODE.LOCAL,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function enrichHandoffWithPrHeadIdentity(routing, { branchName, headSha }) {
|
|
230
|
+
if (!routing || typeof routing !== "object" || !routing.handoffEnvelope || typeof routing.handoffEnvelope !== "object") {
|
|
231
|
+
return routing;
|
|
232
|
+
}
|
|
233
|
+
const requiredArgs = {
|
|
234
|
+
...(routing.handoffEnvelope.requiredArgs && typeof routing.handoffEnvelope.requiredArgs === "object"
|
|
235
|
+
? routing.handoffEnvelope.requiredArgs
|
|
236
|
+
: {}),
|
|
237
|
+
...(typeof branchName === "string" && branchName.length > 0 ? { headRefName: branchName } : {}),
|
|
238
|
+
...(typeof headSha === "string" && headSha.length > 0 ? { headRefOid: headSha } : {}),
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
...routing,
|
|
242
|
+
handoffEnvelope: {
|
|
243
|
+
...routing.handoffEnvelope,
|
|
244
|
+
requiredArgs,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function writeCheckpoint(checkpointDir, checkpoint) {
|
|
249
|
+
await mkdir(checkpointDir, { recursive: true });
|
|
250
|
+
const filePath = buildCheckpointFilePath(checkpointDir);
|
|
251
|
+
await writeFile(filePath, `${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
|
|
252
|
+
}
|
|
253
|
+
function shouldCarryForwardWaitCycles(previousCheckpoint, { repo, pr, headSha, outerAction }) {
|
|
254
|
+
return previousCheckpoint !== null
|
|
255
|
+
&& previousCheckpoint.outerAction === "continue_wait"
|
|
256
|
+
&& outerAction === "continue_wait"
|
|
257
|
+
&& previousCheckpoint.repo === repo
|
|
258
|
+
&& previousCheckpoint.pr === pr
|
|
259
|
+
&& typeof previousCheckpoint.headSha === "string"
|
|
260
|
+
&& previousCheckpoint.headSha.length > 0
|
|
261
|
+
&& typeof headSha === "string"
|
|
262
|
+
&& headSha.length > 0
|
|
263
|
+
&& previousCheckpoint.headSha === headSha;
|
|
264
|
+
}
|
|
265
|
+
export function decideOuterAction({ copilotState, reviewerState, gitStatus }) {
|
|
266
|
+
const routing = evaluateConductorRouting({
|
|
267
|
+
target: { repo: "routing/sentinel", pr: 1 },
|
|
268
|
+
copilotState,
|
|
269
|
+
reviewerState,
|
|
270
|
+
requiresLocalIsolation: gitStatus.isDirty || gitStatus.isDetached,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
outerAction: routing.outerAction,
|
|
274
|
+
...(routing.stopReason !== null ? { reason: routing.stopReason } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export async function runOuterLoop(options, { env = process.env, ghCommand = "gh", gitCommand = "git" } = {}) {
|
|
278
|
+
const { repo, pr, copilotInputPath, reviewerInputPath } = options;
|
|
279
|
+
const normalizedRepo = repo.trim().toLowerCase();
|
|
280
|
+
const checkpointDir = options.checkpointDir ?? buildDefaultCheckpointDir(normalizedRepo, pr);
|
|
281
|
+
const isSnapshotMode = copilotInputPath !== undefined && reviewerInputPath !== undefined;
|
|
282
|
+
let devLoopConfig = null;
|
|
283
|
+
if (!isSnapshotMode) {
|
|
284
|
+
const loaded = await loadDevLoopConfig();
|
|
285
|
+
if (loaded.errors.length === 0) {
|
|
286
|
+
devLoopConfig = loaded.config;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const asyncStartMode = devLoopConfig === null
|
|
290
|
+
? "required"
|
|
291
|
+
: resolveWorkflowConfig(devLoopConfig, "asyncStartMode");
|
|
292
|
+
const asyncStartValidation = validateAsyncStartContext({ env, isSnapshotMode, asyncStartMode });
|
|
293
|
+
if (asyncStartValidation.status === ASYNC_START_STATUS.REJECTED) {
|
|
294
|
+
return buildAsyncStartRejection(asyncStartValidation);
|
|
295
|
+
}
|
|
296
|
+
const { snapshot: copilotSnapshot, interpretation: copilotInterpretation } = await loadCopilotEvidence(
|
|
297
|
+
{ repo: normalizedRepo, pr, copilotInputPath },
|
|
298
|
+
{ env, ghCommand },
|
|
299
|
+
);
|
|
300
|
+
const { snapshot: reviewerSnapshot, interpretation: reviewerInterpretation } = await loadReviewerEvidence(
|
|
301
|
+
{ repo: normalizedRepo, pr, reviewerInputPath },
|
|
302
|
+
{ env, ghCommand },
|
|
303
|
+
);
|
|
304
|
+
const currentHeadSha = typeof reviewerSnapshot?.prHeadSha === "string" && reviewerSnapshot.prHeadSha.length > 0
|
|
305
|
+
? reviewerSnapshot.prHeadSha
|
|
306
|
+
: null;
|
|
307
|
+
const gitStatus = await checkGitStatus({ env, gitCommand });
|
|
308
|
+
const sourceMode = (copilotInputPath !== undefined && reviewerInputPath !== undefined)
|
|
309
|
+
? "snapshot"
|
|
310
|
+
: "local";
|
|
311
|
+
let conductorRouting = evaluateConductorRouting({
|
|
312
|
+
target: { repo: normalizedRepo, pr },
|
|
313
|
+
copilotState: copilotInterpretation.state,
|
|
314
|
+
reviewerState: reviewerInterpretation.state,
|
|
315
|
+
sourceMode,
|
|
316
|
+
requiresLocalIsolation: gitStatus.isDirty || gitStatus.isDetached,
|
|
317
|
+
});
|
|
318
|
+
let outerAction = conductorRouting.outerAction;
|
|
319
|
+
let outerReason = conductorRouting.stopReason;
|
|
320
|
+
let branchIdentity = null;
|
|
321
|
+
if (outerReason === null && sourceMode === "local" && requiresPrLocalIdentityGate(outerAction)) {
|
|
322
|
+
const prHeadIdentity = await fetchPrHeadIdentity({ repo: normalizedRepo, pr }, { env, ghCommand });
|
|
323
|
+
conductorRouting = enrichHandoffWithPrHeadIdentity(conductorRouting, prHeadIdentity);
|
|
324
|
+
branchIdentity = evaluatePrLocalIdentity({
|
|
325
|
+
localBranch: gitStatus.branchName,
|
|
326
|
+
localHeadSha: gitStatus.headSha,
|
|
327
|
+
prBranch: prHeadIdentity.branchName,
|
|
328
|
+
prHeadSha: prHeadIdentity.headSha ?? currentHeadSha,
|
|
329
|
+
});
|
|
330
|
+
const isolationManagedHandoff = conductorRouting.handoffEnvelope?.requiresLocalIsolation === true;
|
|
331
|
+
if (branchIdentity.mismatchReason !== null && !isolationManagedHandoff) {
|
|
332
|
+
outerAction = "stop";
|
|
333
|
+
outerReason = branchIdentity.mismatchReason;
|
|
334
|
+
conductorRouting = buildPrLocalIdentityStopRouting({
|
|
335
|
+
repo: normalizedRepo,
|
|
336
|
+
pr,
|
|
337
|
+
branchIdentity,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const { checkpoint: prevCheckpoint } = await readExistingCheckpoint(normalizedRepo, pr, {
|
|
342
|
+
checkpointDir: options.checkpointDir,
|
|
343
|
+
});
|
|
344
|
+
const prevWaitCycles = typeof prevCheckpoint?.waitCycles === "number" ? prevCheckpoint.waitCycles : 0;
|
|
345
|
+
const waitCycles = shouldCarryForwardWaitCycles(prevCheckpoint, {
|
|
346
|
+
repo: normalizedRepo,
|
|
347
|
+
pr,
|
|
348
|
+
headSha: currentHeadSha,
|
|
349
|
+
outerAction,
|
|
350
|
+
})
|
|
351
|
+
? prevWaitCycles + 1
|
|
352
|
+
: (outerAction === "continue_wait" ? 1 : 0);
|
|
353
|
+
const checkpoint = {
|
|
354
|
+
pr,
|
|
355
|
+
repo: normalizedRepo,
|
|
356
|
+
outerAction,
|
|
357
|
+
copilotState: copilotInterpretation.state,
|
|
358
|
+
reviewerState: reviewerInterpretation.state,
|
|
359
|
+
reviewerScope: reviewerSnapshot.reviewerScope,
|
|
360
|
+
reviewerLogin: reviewerSnapshot.reviewerLogin,
|
|
361
|
+
reason: outerReason ?? null,
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
363
|
+
waitCycles,
|
|
364
|
+
headSha: currentHeadSha,
|
|
365
|
+
};
|
|
366
|
+
await writeCheckpoint(checkpointDir, checkpoint);
|
|
367
|
+
let conductorModel = null;
|
|
368
|
+
let autonomyStopAt = null;
|
|
369
|
+
if (devLoopConfig !== null) {
|
|
370
|
+
conductorModel = resolveConductorModel(devLoopConfig);
|
|
371
|
+
autonomyStopAt = resolveAutonomyStopAt(devLoopConfig);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
outerAction,
|
|
376
|
+
copilotState: copilotInterpretation.state,
|
|
377
|
+
reviewerState: reviewerInterpretation.state,
|
|
378
|
+
reviewerScope: {
|
|
379
|
+
mode: reviewerSnapshot.reviewerScope,
|
|
380
|
+
reviewerLogin: reviewerSnapshot.reviewerLogin,
|
|
381
|
+
},
|
|
382
|
+
...(outerReason !== null && outerReason !== undefined ? { reason: outerReason } : {}),
|
|
383
|
+
...(branchIdentity !== null ? { branchIdentity } : {}),
|
|
384
|
+
conductorRouting,
|
|
385
|
+
checkpoint,
|
|
386
|
+
conductorModel,
|
|
387
|
+
autonomyStopAt,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
export async function runCli(
|
|
391
|
+
argv = process.argv.slice(2),
|
|
392
|
+
{
|
|
393
|
+
stdout = process.stdout,
|
|
394
|
+
stderr = process.stderr,
|
|
395
|
+
env = process.env,
|
|
396
|
+
ghCommand = "gh",
|
|
397
|
+
gitCommand = "git",
|
|
398
|
+
} = {},
|
|
399
|
+
) {
|
|
400
|
+
const options = parseOuterLoopCliArgs(argv);
|
|
401
|
+
if (options.help) {
|
|
402
|
+
stdout.write(`${USAGE}\n`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const result = await runOuterLoop(options, { env, ghCommand, gitCommand });
|
|
406
|
+
if (result.ok === false) {
|
|
407
|
+
stderr.write(`${JSON.stringify(result)}\n`);
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
412
|
+
}
|
|
413
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
414
|
+
if (isDirectRun) {
|
|
415
|
+
runCli().catch((error) => {
|
|
416
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
4
|
+
import { parsePrNumber, requireOptionValue } from "../_cli-primitives.mjs";
|
|
5
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
|
+
import {
|
|
7
|
+
assertRunnerOwnership,
|
|
8
|
+
claimRunnerOwnership,
|
|
9
|
+
loadRunnerCoordinationState,
|
|
10
|
+
releaseRunnerOwnership,
|
|
11
|
+
} from "./_pr-runner-coordination.mjs";
|
|
12
|
+
const USAGE = `Usage:
|
|
13
|
+
pr-runner-coordination.mjs status --repo <owner/name> --pr <number>
|
|
14
|
+
pr-runner-coordination.mjs claim --repo <owner/name> --pr <number> [--run-id <id>]
|
|
15
|
+
pr-runner-coordination.mjs takeover --repo <owner/name> --pr <number> [--run-id <id>]
|
|
16
|
+
pr-runner-coordination.mjs assert --repo <owner/name> --pr <number> [--run-id <id>] [--require-existing]
|
|
17
|
+
pr-runner-coordination.mjs release --repo <owner/name> --pr <number> [--run-id <id>]
|
|
18
|
+
Durable one-runner-per-PR coordination helper.
|
|
19
|
+
If --run-id is omitted for claim/assert/release/takeover, PI_SUBAGENT_RUN_ID is used.
|
|
20
|
+
Output:
|
|
21
|
+
stdout: { "ok": true, ... }
|
|
22
|
+
stderr: { "ok": false, "error": "...", ... }
|
|
23
|
+
Exit codes:
|
|
24
|
+
0 Success / clean stop-compatible result
|
|
25
|
+
1 Argument error or coordination conflict`.trim();
|
|
26
|
+
const parseError = buildParseError(USAGE);
|
|
27
|
+
function parseCliArgs(argv) {
|
|
28
|
+
const args = [...argv];
|
|
29
|
+
const options = {
|
|
30
|
+
help: false,
|
|
31
|
+
command: null,
|
|
32
|
+
repo: undefined,
|
|
33
|
+
pr: undefined,
|
|
34
|
+
runId: undefined,
|
|
35
|
+
requireExisting: false,
|
|
36
|
+
};
|
|
37
|
+
const command = args.shift();
|
|
38
|
+
if (command === undefined || command === "--help" || command === "-h") {
|
|
39
|
+
options.help = true;
|
|
40
|
+
return options;
|
|
41
|
+
}
|
|
42
|
+
options.command = command;
|
|
43
|
+
while (args.length > 0) {
|
|
44
|
+
const token = args.shift();
|
|
45
|
+
if (token === "--help" || token === "-h") {
|
|
46
|
+
options.help = true;
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
if (token === "--repo") {
|
|
50
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (token === "--pr") {
|
|
54
|
+
options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (token === "--run-id") {
|
|
58
|
+
options.runId = requireOptionValue(args, "--run-id", parseError).trim();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (token === "--require-existing") {
|
|
62
|
+
options.requireExisting = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
66
|
+
}
|
|
67
|
+
const validCommands = new Set(["status", "claim", "takeover", "assert", "release"]);
|
|
68
|
+
if (!validCommands.has(options.command)) {
|
|
69
|
+
throw parseError(`Unknown subcommand: ${options.command}`);
|
|
70
|
+
}
|
|
71
|
+
if (options.repo === undefined || options.pr === undefined) {
|
|
72
|
+
throw parseError("pr-runner-coordination requires both --repo <owner/name> and --pr <number>");
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
parseRepoSlug(options.repo);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
|
81
|
+
function resolveRunId(explicitRunId, env) {
|
|
82
|
+
return typeof explicitRunId === "string" && explicitRunId.trim().length > 0
|
|
83
|
+
? explicitRunId.trim()
|
|
84
|
+
: (typeof env?.PI_SUBAGENT_RUN_ID === "string" && env.PI_SUBAGENT_RUN_ID.trim().length > 0
|
|
85
|
+
? env.PI_SUBAGENT_RUN_ID.trim()
|
|
86
|
+
: null);
|
|
87
|
+
}
|
|
88
|
+
export async function runPrRunnerCoordination(options, { env = process.env, cwd = process.cwd() } = {}) {
|
|
89
|
+
if (options.command === "status") {
|
|
90
|
+
const { filePath, state } = await loadRunnerCoordinationState({ repo: options.repo, pr: options.pr, cwd });
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
command: "status",
|
|
94
|
+
repo: options.repo.trim().toLowerCase(),
|
|
95
|
+
pr: options.pr,
|
|
96
|
+
filePath,
|
|
97
|
+
state,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const runId = resolveRunId(options.runId, env);
|
|
101
|
+
if (options.command === "claim") {
|
|
102
|
+
return claimRunnerOwnership({ repo: options.repo, pr: options.pr, runId, mode: "claim", cwd });
|
|
103
|
+
}
|
|
104
|
+
if (options.command === "takeover") {
|
|
105
|
+
return claimRunnerOwnership({ repo: options.repo, pr: options.pr, runId, mode: "takeover", cwd });
|
|
106
|
+
}
|
|
107
|
+
if (options.command === "assert") {
|
|
108
|
+
return assertRunnerOwnership({
|
|
109
|
+
repo: options.repo,
|
|
110
|
+
pr: options.pr,
|
|
111
|
+
runId,
|
|
112
|
+
requireExisting: options.requireExisting,
|
|
113
|
+
cwd,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (options.command === "release") {
|
|
117
|
+
return releaseRunnerOwnership({ repo: options.repo, pr: options.pr, runId, cwd });
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Unhandled runner coordination command: ${options.command}`);
|
|
120
|
+
}
|
|
121
|
+
async function main() {
|
|
122
|
+
try {
|
|
123
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
124
|
+
if (options.help) {
|
|
125
|
+
console.log(USAGE);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const result = await runPrRunnerCoordination(options, { env: process.env });
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
console.error(JSON.stringify(result));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(JSON.stringify(result));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const payload = formatCliError(error, { usage: USAGE });
|
|
137
|
+
console.error(JSON.stringify(payload));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
142
|
+
await main();
|
|
143
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
3
|
+
import { requireOptionValue, runCommand } from "../_cli-primitives.mjs";
|
|
4
|
+
import {
|
|
5
|
+
isUnderWorktreePath, parseMainWorktreePath, isMainCheckout,
|
|
6
|
+
} from "@dev-loops/core/loop/worktree-guard";
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage:
|
|
9
|
+
pre-commit-branch-guard.mjs --expected-branch <name> [--require-worktree] [--block-main-checkout]
|
|
10
|
+
|
|
11
|
+
Verify the current git branch identity and/or worktree isolation before local commit steps.`;
|
|
12
|
+
|
|
13
|
+
const parseError = buildParseError(USAGE);
|
|
14
|
+
|
|
15
|
+
export function parseBranchGuardCliArgs(argv) {
|
|
16
|
+
const args = [...argv];
|
|
17
|
+
const options = { help: false, expectedBranch: undefined, requireWorktree: false, blockMainCheckout: false };
|
|
18
|
+
while (args.length > 0) {
|
|
19
|
+
const token = args.shift();
|
|
20
|
+
if (token === "--help" || token === "-h") { options.help = true; return options; }
|
|
21
|
+
if (token === "--expected-branch") { options.expectedBranch = requireOptionValue(args, "--expected-branch", parseError, { flagPattern: /^-/u }); continue; }
|
|
22
|
+
if (token === "--require-worktree") { options.requireWorktree = true; continue; }
|
|
23
|
+
if (token === "--block-main-checkout") { options.blockMainCheckout = true; continue; }
|
|
24
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
25
|
+
}
|
|
26
|
+
if (options.expectedBranch === undefined) { throw parseError("--expected-branch <name> is required"); }
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), env = process.env, gitCommand = "git" } = {}) {
|
|
31
|
+
const options = parseBranchGuardCliArgs(argv);
|
|
32
|
+
if (options.help) { stdout.write(`${USAGE}\n`); return { ok: true, help: true }; }
|
|
33
|
+
|
|
34
|
+
const { stdout: branchOutput } = await runCommand(gitCommand, ["branch", "--show-current"], { cwd, env });
|
|
35
|
+
const currentBranch = branchOutput.trim();
|
|
36
|
+
if (currentBranch !== options.expectedBranch) {
|
|
37
|
+
const payload = { ok: false, error: "branch_mismatch", current: currentBranch, expected: options.expectedBranch };
|
|
38
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
39
|
+
return payload;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let worktreeOk = null, mainCheckoutBlocked = null;
|
|
43
|
+
if (options.requireWorktree || options.blockMainCheckout) {
|
|
44
|
+
let mainWorktreePath = null;
|
|
45
|
+
if (options.blockMainCheckout) {
|
|
46
|
+
try { const { stdout: wtOutput } = await runCommand(gitCommand, ["worktree", "list"], { cwd, env }); mainWorktreePath = parseMainWorktreePath(wtOutput); } catch {}
|
|
47
|
+
}
|
|
48
|
+
if (options.requireWorktree) {
|
|
49
|
+
worktreeOk = isUnderWorktreePath(cwd);
|
|
50
|
+
if (!worktreeOk) { stderr.write(JSON.stringify({ ok: false, error: "not_in_worktree", cwd, requiredPrefix: "tmp/worktrees/" }) + "\n"); return { ok: false, error: "not_in_worktree" }; }
|
|
51
|
+
}
|
|
52
|
+
if (options.blockMainCheckout) {
|
|
53
|
+
const isMain = isMainCheckout(cwd, mainWorktreePath);
|
|
54
|
+
mainCheckoutBlocked = !(isMain && !isUnderWorktreePath(cwd));
|
|
55
|
+
if (!mainCheckoutBlocked) { stderr.write(JSON.stringify({ ok: false, error: "main_checkout_blocked", cwd, mainWorktree: mainWorktreePath }) + "\n"); return { ok: false, error: "main_checkout_blocked" }; }
|
|
56
|
+
}
|
|
57
|
+
if (!options.requireWorktree) worktreeOk = null;
|
|
58
|
+
if (!options.blockMainCheckout) mainCheckoutBlocked = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const payload = { ok: true, branch: currentBranch, matched: true, worktreeOk, mainCheckoutBlocked };
|
|
62
|
+
stdout.write(`${JSON.stringify(payload)}\n`);
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
67
|
+
runCli().then((result) => { if (result?.ok === false) { process.exitCode = 1; } }).catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
|
|
68
|
+
}
|