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,533 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resolveAuthoritativeStartupResumeBundle } from "@dev-loops/core/loop/public-dev-loop-routing";
|
|
6
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
7
|
+
import { requireOptionValue, parsePositiveInteger } from "../_cli-primitives.mjs";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
isUnderWorktreePath,
|
|
11
|
+
parseMainWorktreePath,
|
|
12
|
+
isMainCheckout,
|
|
13
|
+
parseAllWorktreePaths,
|
|
14
|
+
isListedWorktree,
|
|
15
|
+
} from "@dev-loops/core/loop/worktree-guard";
|
|
16
|
+
import {
|
|
17
|
+
validateAsyncStartContext,
|
|
18
|
+
buildAsyncStartRejection,
|
|
19
|
+
ASYNC_START_STATUS,
|
|
20
|
+
} from "@dev-loops/core/loop/async-start-contract";
|
|
21
|
+
import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
22
|
+
import { isCopilotLogin } from "@dev-loops/core/github/copilot-helpers";
|
|
23
|
+
import { loadDevLoopConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
|
|
24
|
+
import { createPiAdapter } from "@dev-loops/core/harness";
|
|
25
|
+
const USAGE = `Usage:
|
|
26
|
+
resolve-dev-loop-startup.mjs --issue <number>
|
|
27
|
+
resolve-dev-loop-startup.mjs --pr <number>
|
|
28
|
+
resolve-dev-loop-startup.mjs --input <path>
|
|
29
|
+
Resolve the authoritative public dev-loop startup/resume bundle.
|
|
30
|
+
Auto-resolves state from GitHub API, git remote, and settings when
|
|
31
|
+
--issue or --pr is used. Use --input for non-standard states.
|
|
32
|
+
Required (exactly one):
|
|
33
|
+
--issue <n> Target an issue by number (auto-resolves all state)
|
|
34
|
+
--pr <n> Target a PR by number (auto-resolves all state)
|
|
35
|
+
--input <path> Path to a JSON file with canonical-state payload
|
|
36
|
+
Exit codes:
|
|
37
|
+
0 Success
|
|
38
|
+
1 Argument error, runtime failure, or async-start contract rejection`.trim();
|
|
39
|
+
const SHARED_PUBLIC_CONTRACT = "skills/docs/public-dev-loop-contract.md";
|
|
40
|
+
const SHARED_RETROSPECTIVE_CONTRACT = "skills/docs/retrospective-checkpoint-contract.md";
|
|
41
|
+
const STRATEGY_REQUIRED_READS = {
|
|
42
|
+
local_implementation: [
|
|
43
|
+
SHARED_PUBLIC_CONTRACT,
|
|
44
|
+
"skills/local-implementation/SKILL.md",
|
|
45
|
+
],
|
|
46
|
+
issue_intake: [
|
|
47
|
+
SHARED_PUBLIC_CONTRACT,
|
|
48
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
49
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
50
|
+
"skills/docs/copilot-loop-operations.md",
|
|
51
|
+
"skills/docs/issue-intake-procedure.md",
|
|
52
|
+
],
|
|
53
|
+
copilot_pr_followup: [
|
|
54
|
+
SHARED_PUBLIC_CONTRACT,
|
|
55
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
56
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
57
|
+
"skills/docs/copilot-loop-operations.md",
|
|
58
|
+
],
|
|
59
|
+
external_pr_followup: [
|
|
60
|
+
SHARED_PUBLIC_CONTRACT,
|
|
61
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
62
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
63
|
+
"skills/docs/copilot-loop-operations.md",
|
|
64
|
+
],
|
|
65
|
+
reviewer_fixer: [
|
|
66
|
+
SHARED_PUBLIC_CONTRACT,
|
|
67
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
68
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
69
|
+
"skills/docs/copilot-loop-operations.md",
|
|
70
|
+
],
|
|
71
|
+
wait_watch: [
|
|
72
|
+
SHARED_PUBLIC_CONTRACT,
|
|
73
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
74
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
75
|
+
"skills/docs/copilot-loop-operations.md",
|
|
76
|
+
],
|
|
77
|
+
final_approval: [
|
|
78
|
+
SHARED_PUBLIC_CONTRACT,
|
|
79
|
+
SHARED_RETROSPECTIVE_CONTRACT,
|
|
80
|
+
"skills/copilot-pr-followup/SKILL.md",
|
|
81
|
+
"skills/docs/copilot-loop-operations.md",
|
|
82
|
+
"skills/final-approval/SKILL.md",
|
|
83
|
+
],
|
|
84
|
+
none: [SHARED_PUBLIC_CONTRACT],
|
|
85
|
+
};
|
|
86
|
+
const STRATEGY_ASYNC_DISPATCH = {
|
|
87
|
+
local_implementation: false,
|
|
88
|
+
issue_intake: true,
|
|
89
|
+
copilot_pr_followup: true,
|
|
90
|
+
external_pr_followup: true,
|
|
91
|
+
reviewer_fixer: true,
|
|
92
|
+
wait_watch: true,
|
|
93
|
+
final_approval: false,
|
|
94
|
+
none: false,
|
|
95
|
+
};
|
|
96
|
+
const parseError = buildParseError(USAGE);
|
|
97
|
+
export function parseResolveDevLoopStartupCliArgs(argv) {
|
|
98
|
+
const args = [...argv];
|
|
99
|
+
const options = {
|
|
100
|
+
help: false,
|
|
101
|
+
inputPath: undefined,
|
|
102
|
+
issue: undefined,
|
|
103
|
+
pr: undefined,
|
|
104
|
+
};
|
|
105
|
+
while (args.length > 0) {
|
|
106
|
+
const token = args.shift();
|
|
107
|
+
if (token === "--help" || token === "-h") {
|
|
108
|
+
options.help = true;
|
|
109
|
+
return options;
|
|
110
|
+
}
|
|
111
|
+
if (token === "--input") {
|
|
112
|
+
options.inputPath = requireOptionValue(args, "--input", parseError);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (token === "--issue") {
|
|
116
|
+
options.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "--issue", parseError);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (token === "--pr") {
|
|
120
|
+
options.pr = parsePositiveInteger(requireOptionValue(args, "--pr", parseError), "--pr", parseError);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
124
|
+
}
|
|
125
|
+
const modeCount = [options.inputPath, options.issue, options.pr].filter(v => v !== undefined).length;
|
|
126
|
+
if (modeCount > 1) {
|
|
127
|
+
throw parseError("--issue, --pr, and --input are mutually exclusive; provide exactly one");
|
|
128
|
+
}
|
|
129
|
+
if (modeCount === 0) {
|
|
130
|
+
throw parseError("--input <path>, --issue <n>, or --pr <n> is required");
|
|
131
|
+
}
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
function ghJson(args, cwd) {
|
|
135
|
+
try {
|
|
136
|
+
const stdout = execFileSync("gh", args, {
|
|
137
|
+
cwd,
|
|
138
|
+
encoding: "utf8",
|
|
139
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
140
|
+
});
|
|
141
|
+
return JSON.parse(stdout);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`gh command failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function mapGhState(ghState) {
|
|
147
|
+
const s = String(ghState).toUpperCase();
|
|
148
|
+
if (s === "OPEN") return "open";
|
|
149
|
+
if (s === "CLOSED") return "closed";
|
|
150
|
+
if (s === "MERGED") return "merged";
|
|
151
|
+
throw new Error(`Unknown GitHub state: "${ghState}"`);
|
|
152
|
+
}
|
|
153
|
+
function hasAcSection(body) {
|
|
154
|
+
if (typeof body !== "string" || body.length === 0) return false;
|
|
155
|
+
return /##\s*Acceptance Criteria|##\s*AC\b|###\s*Acceptance Criteria|###\s*AC\b/i.test(body);
|
|
156
|
+
}
|
|
157
|
+
function resolveTargetPreference(cwd) {
|
|
158
|
+
const devloopsCandidates = [
|
|
159
|
+
path.join(cwd, ".devloops"),
|
|
160
|
+
path.join(cwd, ".devloops.yaml"),
|
|
161
|
+
path.join(cwd, ".devloops.yml"),
|
|
162
|
+
path.join(cwd, ".devloops.json"),
|
|
163
|
+
];
|
|
164
|
+
// Check .devloops first (bare or with extension).
|
|
165
|
+
// Bare files try YAML first, then JSON fallback (consistent with
|
|
166
|
+
// config.mjs readConfigFile behavior).
|
|
167
|
+
for (const devloopsPath of devloopsCandidates) {
|
|
168
|
+
try {
|
|
169
|
+
const raw = readFileSync(devloopsPath, "utf8");
|
|
170
|
+
let val;
|
|
171
|
+
if (devloopsPath.endsWith(".json")) {
|
|
172
|
+
val = JSON.parse(raw)?.strategy?.default;
|
|
173
|
+
} else if (devloopsPath.endsWith(".yaml") || devloopsPath.endsWith(".yml")) {
|
|
174
|
+
const m = raw.match(/strategy:\s*\n\s*default:\s*["']?([^"'\s]+)["']?/);
|
|
175
|
+
val = m ? m[1] : undefined;
|
|
176
|
+
} else {
|
|
177
|
+
// Bare file (no recognized extension) — YAML first, JSON fallback
|
|
178
|
+
const m = raw.match(/strategy:\s*\n\s*default:\s*["']?([^"'\s]+)["']?/);
|
|
179
|
+
if (m) {
|
|
180
|
+
val = m[1];
|
|
181
|
+
} else {
|
|
182
|
+
try {
|
|
183
|
+
val = JSON.parse(raw)?.strategy?.default;
|
|
184
|
+
} catch {
|
|
185
|
+
// Not valid JSON either — fall through
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (val === "local-first") return "prefer_local";
|
|
190
|
+
if (val === "github-first") return "prefer_github_first";
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Legacy .pi/dev-loop/settings.* (deprecated)
|
|
195
|
+
const legacyCandidates = [
|
|
196
|
+
path.join(cwd, ".pi", "dev-loop", "settings.yaml"),
|
|
197
|
+
path.join(cwd, ".pi", "dev-loop", "settings.yml"),
|
|
198
|
+
path.join(cwd, ".pi", "dev-loop", "settings.json"),
|
|
199
|
+
];
|
|
200
|
+
for (const settingsPath of legacyCandidates) {
|
|
201
|
+
try {
|
|
202
|
+
const raw = readFileSync(settingsPath, "utf8");
|
|
203
|
+
if (settingsPath.endsWith(".json")) {
|
|
204
|
+
const parsed = JSON.parse(raw);
|
|
205
|
+
const val = parsed?.strategy?.default;
|
|
206
|
+
if (val === "local-first") return "prefer_local";
|
|
207
|
+
if (val === "github-first") return "prefer_github_first";
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const match = raw.match(/strategy:\s*\n\s*default:\s*["']?([^"'\s]+)["']?/);
|
|
211
|
+
if (match) {
|
|
212
|
+
if (match[1] === "local-first") return "prefer_local";
|
|
213
|
+
if (match[1] === "github-first") return "prefer_github_first";
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return "prefer_github_first";
|
|
219
|
+
}
|
|
220
|
+
function normalizeConfigInputSource(value) {
|
|
221
|
+
if (value === "phase-docs") return "phase-docs";
|
|
222
|
+
if (value === "tracker") return "tracker";
|
|
223
|
+
return "tracker";
|
|
224
|
+
}
|
|
225
|
+
export function buildAutoResolvedInput({ issue, pr, cwd, targetPreference, inputSource }) {
|
|
226
|
+
let repoRoot = cwd;
|
|
227
|
+
try {
|
|
228
|
+
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
229
|
+
cwd,
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
232
|
+
}).trim();
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
const repo = detectRepoSlug(repoRoot);
|
|
236
|
+
if (!repo) {
|
|
237
|
+
throw new Error("Repo auto-detection failed. Set origin remote or use --input.");
|
|
238
|
+
}
|
|
239
|
+
if (issue !== undefined) {
|
|
240
|
+
const resolvedTargetPreference = targetPreference ?? resolveTargetPreference(repoRoot);
|
|
241
|
+
const resolvedInputSource = normalizeConfigInputSource(inputSource);
|
|
242
|
+
if (resolvedTargetPreference === "prefer_local" && resolvedInputSource === "phase-docs") {
|
|
243
|
+
return {
|
|
244
|
+
intent: "start_issue_locally",
|
|
245
|
+
mode: "bounded_handoff",
|
|
246
|
+
targetPreference: resolvedTargetPreference,
|
|
247
|
+
artifactState: "not_applicable",
|
|
248
|
+
issueLinkageResolution: "not_applicable",
|
|
249
|
+
issueReadiness: "not_applicable",
|
|
250
|
+
issueAssignmentState: "not_applicable",
|
|
251
|
+
loopState: "implementation_pending",
|
|
252
|
+
currentState: {
|
|
253
|
+
target: { kind: "local_phase", issue, pr: null, linkedPr: null, branch: null, phase: `issue-${issue}` },
|
|
254
|
+
ownership: "local",
|
|
255
|
+
nextActor: "local",
|
|
256
|
+
status: "active",
|
|
257
|
+
authorization: "authorized",
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
let artifactState = "not_applicable";
|
|
262
|
+
const warnings = [];
|
|
263
|
+
let issueLinkageResolution = "resolved_no_open_pr";
|
|
264
|
+
let linkedPr = null;
|
|
265
|
+
let ownership = "local";
|
|
266
|
+
try {
|
|
267
|
+
const linkageJson = execFileSync(process.execPath, [
|
|
268
|
+
path.join(repoRoot, "scripts/github/detect-linked-issue-pr.mjs"),
|
|
269
|
+
"--repo", repo, "--issue", String(issue),
|
|
270
|
+
], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
271
|
+
const linkage = JSON.parse(linkageJson);
|
|
272
|
+
if (linkage.hasOpenLinkedPr) {
|
|
273
|
+
issueLinkageResolution = "resolved_linked_pr";
|
|
274
|
+
linkedPr = linkage.prNumber;
|
|
275
|
+
try {
|
|
276
|
+
const prJson = ghJson(["pr", "view", String(linkedPr), "--repo", repo, "--json", "author,state"], repoRoot);
|
|
277
|
+
ownership = isCopilotLogin(prJson?.author?.login) ? "copilot" : "external_human";
|
|
278
|
+
artifactState = mapGhState(prJson?.state ?? "OPEN");
|
|
279
|
+
} catch {
|
|
280
|
+
warnings.push(
|
|
281
|
+
`linkedPr authorship: using default ownership "${ownership}" for PR #${linkedPr} — gh pr view failed`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
warnings.push(`issueLinkageResolution: using default "${issueLinkageResolution}" — linked-PR detection unavailable`);
|
|
287
|
+
}
|
|
288
|
+
let issueReadiness;
|
|
289
|
+
try {
|
|
290
|
+
const issueJson = ghJson(["issue", "view", String(issue), "--repo", repo, "--json", "body"], repoRoot);
|
|
291
|
+
issueReadiness = hasAcSection(issueJson.body) ? "ready" : "needs_clarification";
|
|
292
|
+
} catch {
|
|
293
|
+
issueReadiness = "needs_clarification";
|
|
294
|
+
warnings.push(`issueReadiness: using default "${issueReadiness}" — gh issue view failed`);
|
|
295
|
+
}
|
|
296
|
+
let issueAssignmentState;
|
|
297
|
+
try {
|
|
298
|
+
const assigneesJson = ghJson(["issue", "view", String(issue), "--repo", repo, "--json", "assignees"], repoRoot);
|
|
299
|
+
issueAssignmentState = (assigneesJson.assignees || []).some(a => a.login === "copilot-swe-agent")
|
|
300
|
+
? "assigned_to_copilot"
|
|
301
|
+
: "unassigned";
|
|
302
|
+
} catch {
|
|
303
|
+
issueAssignmentState = "unassigned";
|
|
304
|
+
warnings.push(`issueAssignmentState: using default "${issueAssignmentState}" — gh issue view failed`);
|
|
305
|
+
}
|
|
306
|
+
const loopState = "issue_intake_start";
|
|
307
|
+
return {
|
|
308
|
+
intent: "start_issue_locally",
|
|
309
|
+
mode: "bounded_handoff",
|
|
310
|
+
targetPreference: resolvedTargetPreference,
|
|
311
|
+
artifactState,
|
|
312
|
+
issueLinkageResolution,
|
|
313
|
+
issueReadiness,
|
|
314
|
+
issueAssignmentState,
|
|
315
|
+
loopState,
|
|
316
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
317
|
+
currentState: {
|
|
318
|
+
target: { kind: "issue", issue, pr: null, linkedPr, branch: null, phase: null },
|
|
319
|
+
ownership: ownership,
|
|
320
|
+
nextActor: ownership === "copilot"
|
|
321
|
+
? "copilot"
|
|
322
|
+
: ownership === "external_human"
|
|
323
|
+
? "external_human" : "local",
|
|
324
|
+
status: "active",
|
|
325
|
+
authorization: "authorized",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
let artifactState;
|
|
330
|
+
try {
|
|
331
|
+
const prJson = ghJson(["pr", "view", String(pr), "--repo", repo, "--json", "state,mergedAt"], repoRoot);
|
|
332
|
+
artifactState = prJson.mergedAt ? "merged" : mapGhState(prJson.state);
|
|
333
|
+
} catch {
|
|
334
|
+
artifactState = "open";
|
|
335
|
+
}
|
|
336
|
+
const resolvedTargetPreference = targetPreference ?? resolveTargetPreference(repoRoot);
|
|
337
|
+
return {
|
|
338
|
+
intent: "continue_on_pr",
|
|
339
|
+
mode: "bounded_handoff",
|
|
340
|
+
targetPreference: resolvedTargetPreference,
|
|
341
|
+
artifactState,
|
|
342
|
+
issueLinkageResolution: "not_applicable",
|
|
343
|
+
loopState: "pr_followup_start",
|
|
344
|
+
currentState: {
|
|
345
|
+
target: { kind: "pr", issue: null, pr, linkedPr: null, branch: null, phase: null },
|
|
346
|
+
ownership: "copilot",
|
|
347
|
+
nextActor: "user",
|
|
348
|
+
status: "active",
|
|
349
|
+
authorization: "authorized",
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function summarizeCanonicalState(bundle) {
|
|
354
|
+
return {
|
|
355
|
+
target: bundle.canonicalState?.target ?? null,
|
|
356
|
+
ownership: bundle.canonicalState?.ownership ?? null,
|
|
357
|
+
nextActor: bundle.canonicalState?.nextActor ?? null,
|
|
358
|
+
status: bundle.canonicalState?.status ?? null,
|
|
359
|
+
authorization: bundle.canonicalState?.authorization ?? null,
|
|
360
|
+
artifactState: bundle.artifactState ?? null,
|
|
361
|
+
issueLinkageResolution: bundle.issueLinkageResolution ?? null,
|
|
362
|
+
loopState: bundle.loopState ?? null,
|
|
363
|
+
routeKind: bundle.routeKind ?? null,
|
|
364
|
+
selectedGate: bundle.selectedGate ?? null,
|
|
365
|
+
executionMode: bundle.executionMode ?? null,
|
|
366
|
+
waitSemantics: bundle.waitSemantics ?? null,
|
|
367
|
+
requiresAsyncDispatch: bundle.selectedStrategy !== null
|
|
368
|
+
? (STRATEGY_ASYNC_DISPATCH[bundle.selectedStrategy] ?? false)
|
|
369
|
+
: false,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
export function buildResolveDevLoopStartupResult(input, { adapter = createPiAdapter(), env, cwd, asyncStartMode = "required" } = {}) {
|
|
373
|
+
const effectiveEnv = env ?? adapter.getEnv();
|
|
374
|
+
const effectiveCwd = cwd ?? adapter.getCwd();
|
|
375
|
+
try {
|
|
376
|
+
const checkpointText = readFileSync(
|
|
377
|
+
path.join(effectiveCwd, ".pi", "dev-loop-retrospective-checkpoint.json"),
|
|
378
|
+
"utf8",
|
|
379
|
+
);
|
|
380
|
+
const checkpoint = JSON.parse(checkpointText);
|
|
381
|
+
const rawState = checkpoint?.state;
|
|
382
|
+
const DURABLE_STATE_MAP = {
|
|
383
|
+
none: "none",
|
|
384
|
+
complete: "complete",
|
|
385
|
+
skipped: "skipped",
|
|
386
|
+
missing: "missing",
|
|
387
|
+
required: "missing", // durable artifact uses "required" to mean pending retrospective
|
|
388
|
+
};
|
|
389
|
+
const normalizedRaw = typeof rawState === "string" ? rawState.trim().toLowerCase() : null;
|
|
390
|
+
const mappedState = DURABLE_STATE_MAP[normalizedRaw] ?? null;
|
|
391
|
+
if (mappedState) {
|
|
392
|
+
input = { ...input, retrospectiveCheckpointState: mappedState };
|
|
393
|
+
} else {
|
|
394
|
+
input = { ...input, retrospectiveCheckpointState: "missing" };
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err?.code === "ENOENT") {
|
|
398
|
+
} else {
|
|
399
|
+
input = { ...input, retrospectiveCheckpointState: "missing" };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const bundle = resolveAuthoritativeStartupResumeBundle(input);
|
|
403
|
+
const strategyKey = bundle.selectedStrategy ?? "none";
|
|
404
|
+
if (!(strategyKey in STRATEGY_REQUIRED_READS)) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Unknown strategy key "${strategyKey}" is not in the allowed strategy required-reads map. ` +
|
|
407
|
+
`Update STRATEGY_REQUIRED_READS to include this strategy or check for a core routing contract drift.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const requiresAsyncDispatch = bundle.selectedStrategy !== null
|
|
411
|
+
? (STRATEGY_ASYNC_DISPATCH[bundle.selectedStrategy] ?? false)
|
|
412
|
+
: false;
|
|
413
|
+
if (requiresAsyncDispatch) {
|
|
414
|
+
const validation = validateAsyncStartContext({ env: effectiveEnv, asyncStartMode });
|
|
415
|
+
if (validation.status === ASYNC_START_STATUS.REJECTED) {
|
|
416
|
+
return buildAsyncStartRejection(validation);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const PI_WORKTREE_BYPASS_VAR = "PI_WORKTREE_BYPASS";
|
|
420
|
+
if (
|
|
421
|
+
strategyKey === "local_implementation" &&
|
|
422
|
+
(effectiveEnv[PI_WORKTREE_BYPASS_VAR] ?? "").trim() !== "1"
|
|
423
|
+
) {
|
|
424
|
+
try {
|
|
425
|
+
const worktreeOutput = execFileSync("git", ["worktree", "list"], {
|
|
426
|
+
cwd: effectiveCwd,
|
|
427
|
+
env: effectiveEnv,
|
|
428
|
+
encoding: "utf8",
|
|
429
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
430
|
+
});
|
|
431
|
+
const mainPath = parseMainWorktreePath(worktreeOutput);
|
|
432
|
+
const allPaths = parseAllWorktreePaths(worktreeOutput);
|
|
433
|
+
if (!isUnderWorktreePath(effectiveCwd)) {
|
|
434
|
+
const reason = mainPath !== null && isMainCheckout(effectiveCwd, mainPath)
|
|
435
|
+
? `Local implementation requires worktree isolation. Current directory is the main git checkout (${mainPath}). Create a worktree under tmp/worktrees/<slug>/ and re-run.`
|
|
436
|
+
: "Local implementation requires worktree isolation. Current directory is not under tmp/worktrees/. Create a worktree and re-run.";
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
bundleKind: "needs_reconcile",
|
|
440
|
+
selectedStrategy: "none",
|
|
441
|
+
requiredReads: STRATEGY_REQUIRED_READS["none"],
|
|
442
|
+
nextAction: reason,
|
|
443
|
+
canonicalStateSummary: summarizeCanonicalState(bundle),
|
|
444
|
+
bundle,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (!isListedWorktree(effectiveCwd, allPaths)) {
|
|
448
|
+
const reason = `Local implementation requires worktree isolation. Current directory is under tmp/worktrees/ but is not listed as a git worktree by \`git worktree list\`. Create a proper worktree with \`git worktree add\` and re-run.`;
|
|
449
|
+
return {
|
|
450
|
+
ok: true,
|
|
451
|
+
bundleKind: "needs_reconcile",
|
|
452
|
+
selectedStrategy: "none",
|
|
453
|
+
requiredReads: STRATEGY_REQUIRED_READS["none"],
|
|
454
|
+
nextAction: reason,
|
|
455
|
+
canonicalStateSummary: summarizeCanonicalState(bundle),
|
|
456
|
+
bundle,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
bundleKind: "needs_reconcile",
|
|
463
|
+
selectedStrategy: "none",
|
|
464
|
+
requiredReads: STRATEGY_REQUIRED_READS["none"],
|
|
465
|
+
nextAction: "Local implementation requires worktree isolation but git worktree list failed. Verify the repository and re-run from a worktree under tmp/worktrees/.",
|
|
466
|
+
canonicalStateSummary: summarizeCanonicalState(bundle),
|
|
467
|
+
bundle,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
bundleKind: bundle.bundleKind,
|
|
474
|
+
selectedStrategy: strategyKey,
|
|
475
|
+
requiredReads: STRATEGY_REQUIRED_READS[strategyKey],
|
|
476
|
+
nextAction: bundle.nextAction,
|
|
477
|
+
canonicalStateSummary: summarizeCanonicalState(bundle),
|
|
478
|
+
bundle,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr, adapter = createPiAdapter() } = {}) {
|
|
482
|
+
const sessionCwd = adapter.getCwd();
|
|
483
|
+
const options = parseResolveDevLoopStartupCliArgs(argv);
|
|
484
|
+
if (options.help) {
|
|
485
|
+
stdout.write(`${USAGE}\n`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Resolve repo root via the adapter so the CLI stays harness-agnostic.
|
|
489
|
+
const repoRoot = adapter.getRepoRoot();
|
|
490
|
+
const { config: devLoopConfig, errors: configErrors = [] } = await loadDevLoopConfig({ repoRoot });
|
|
491
|
+
const asyncStartMode = configErrors.length === 0
|
|
492
|
+
? resolveWorkflowConfig(devLoopConfig, "asyncStartMode")
|
|
493
|
+
: "required";
|
|
494
|
+
const targetPreference = configErrors.length === 0
|
|
495
|
+
? devLoopConfig?.strategy?.default === "local-first"
|
|
496
|
+
? "prefer_local"
|
|
497
|
+
: "prefer_github_first"
|
|
498
|
+
: "prefer_github_first";
|
|
499
|
+
const inputSource = configErrors.length === 0
|
|
500
|
+
? normalizeConfigInputSource(devLoopConfig?.inputSource?.default)
|
|
501
|
+
: "tracker";
|
|
502
|
+
let input;
|
|
503
|
+
if (options.inputPath !== undefined) {
|
|
504
|
+
const text = await readFile(path.resolve(options.inputPath), "utf8");
|
|
505
|
+
input = parseJsonText(text);
|
|
506
|
+
} else if (options.issue !== undefined) {
|
|
507
|
+
input = buildAutoResolvedInput({
|
|
508
|
+
issue: options.issue,
|
|
509
|
+
cwd: sessionCwd,
|
|
510
|
+
targetPreference,
|
|
511
|
+
inputSource,
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
input = buildAutoResolvedInput({
|
|
515
|
+
pr: options.pr,
|
|
516
|
+
cwd: sessionCwd,
|
|
517
|
+
targetPreference,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const result = buildResolveDevLoopStartupResult(input, { asyncStartMode, adapter });
|
|
521
|
+
if (result.ok === false) {
|
|
522
|
+
stderr.write(`${JSON.stringify(result)}\n`);
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
527
|
+
}
|
|
528
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
529
|
+
runCli().catch((error) => {
|
|
530
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
531
|
+
process.exitCode = 1;
|
|
532
|
+
});
|
|
533
|
+
}
|