dev-loops 0.6.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +8 -7
  3. package/.claude/agents/fixer.md +1 -0
  4. package/.claude/agents/refiner.md +2 -2
  5. package/.claude/agents/review.md +4 -3
  6. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  7. package/.claude/commands/loop-continue.md +15 -0
  8. package/.claude/commands/loop-enqueue.md +22 -0
  9. package/.claude/commands/loop-grill.md +17 -0
  10. package/.claude/commands/{info.md → loop-info.md} +2 -2
  11. package/.claude/commands/loop-queue-status.md +24 -0
  12. package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
  13. package/.claude/commands/{start.md → loop-start.md} +1 -1
  14. package/.claude/commands/{status.md → loop-status.md} +1 -1
  15. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  16. package/.claude/hooks/_hook-decisions.mjs +138 -15
  17. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  18. package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
  19. package/.claude/skills/dev-loop/SKILL.md +16 -12
  20. package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
  21. package/.claude/skills/docs/anti-patterns.md +5 -3
  22. package/.claude/skills/docs/artifact-authority-contract.md +30 -11
  23. package/.claude/skills/docs/confirmation-rules.md +1 -0
  24. package/.claude/skills/docs/contract-style-guide.md +37 -0
  25. package/.claude/skills/docs/copilot-loop-operations.md +19 -15
  26. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  27. package/.claude/skills/docs/debt-remediation-contract.md +1 -1
  28. package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
  29. package/.claude/skills/docs/issue-intake-procedure.md +26 -19
  30. package/.claude/skills/docs/local-planning-flow.md +1 -1
  31. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  32. package/.claude/skills/docs/merge-preconditions.md +30 -12
  33. package/.claude/skills/docs/plan-file-contract.md +1 -1
  34. package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
  35. package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
  36. package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
  37. package/.claude/skills/docs/spike-mode-contract.md +14 -7
  38. package/.claude/skills/docs/stop-conditions.md +30 -15
  39. package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
  40. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  41. package/.claude/skills/docs/validation-policy.md +4 -5
  42. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  43. package/.claude/skills/local-implementation/SKILL.md +62 -145
  44. package/.claude/skills/loop-grill/SKILL.md +163 -0
  45. package/AGENTS.md +1 -0
  46. package/CHANGELOG.md +101 -0
  47. package/README.md +9 -9
  48. package/agents/dev-loop.agent.md +11 -10
  49. package/agents/developer.agent.md +1 -1
  50. package/agents/docs.agent.md +1 -1
  51. package/agents/fixer.agent.md +2 -1
  52. package/agents/quality.agent.md +1 -1
  53. package/agents/refiner.agent.md +3 -3
  54. package/agents/review.agent.md +5 -4
  55. package/cli/index.mjs +35 -42
  56. package/extension/README.md +4 -4
  57. package/extension/checks.ts +1 -5
  58. package/extension/harness-types.ts +1 -0
  59. package/extension/index.ts +6 -0
  60. package/extension/pi-extension-adapter.ts +2 -0
  61. package/extension/presentation.ts +7 -19
  62. package/package.json +12 -12
  63. package/scripts/_core-helpers.mjs +2 -0
  64. package/scripts/claude/headless-dev-loop.mjs +1 -1
  65. package/scripts/docs/validate-rule-ownership.mjs +442 -0
  66. package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
  67. package/scripts/github/_review-thread-mutations.mjs +5 -2
  68. package/scripts/github/capture-review-threads.mjs +2 -9
  69. package/scripts/github/comment-issue.mjs +2 -9
  70. package/scripts/github/create-label.mjs +133 -0
  71. package/scripts/github/create-pr.mjs +119 -10
  72. package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
  73. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  74. package/scripts/github/edit-pr.mjs +259 -0
  75. package/scripts/github/fetch-ci-logs.mjs +2 -9
  76. package/scripts/github/list-issues.mjs +2 -9
  77. package/scripts/github/list-review-threads.mjs +277 -0
  78. package/scripts/github/manage-sub-issues.mjs +46 -10
  79. package/scripts/github/offer-human-handoff.mjs +8 -5
  80. package/scripts/github/post-gate-findings.mjs +20 -5
  81. package/scripts/github/probe-ci-status.mjs +8 -2
  82. package/scripts/github/probe-copilot-review.mjs +21 -14
  83. package/scripts/github/ready-for-review.mjs +26 -8
  84. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  85. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  86. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  87. package/scripts/github/request-copilot-review.mjs +98 -26
  88. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  89. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  90. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  91. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  92. package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
  93. package/scripts/github/verify-briefing-prefixes.mjs +191 -0
  94. package/scripts/github/verify-fresh-review-context.mjs +226 -32
  95. package/scripts/github/view-pr.mjs +150 -0
  96. package/scripts/github/wait-pr-checks.mjs +171 -0
  97. package/scripts/github/write-gate-context.mjs +458 -65
  98. package/scripts/github/write-gate-findings-log.mjs +128 -2
  99. package/scripts/lib/jq-output.mjs +18 -0
  100. package/scripts/loop/_post-convergence-change.mjs +211 -0
  101. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  102. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  103. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  104. package/scripts/loop/check-retro-tooling.mjs +14 -3
  105. package/scripts/loop/checkpoint-contract.mjs +7 -4
  106. package/scripts/loop/cleanup-worktree.mjs +12 -3
  107. package/scripts/loop/conductor-monitor.mjs +12 -18
  108. package/scripts/loop/copilot-pr-handoff.mjs +162 -14
  109. package/scripts/loop/debt-remediate.mjs +24 -12
  110. package/scripts/loop/detect-change-scope.mjs +38 -9
  111. package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
  112. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  113. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  114. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  115. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  116. package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
  117. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  118. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  119. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  120. package/scripts/loop/ensure-worktree.mjs +8 -3
  121. package/scripts/loop/info.mjs +12 -4
  122. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  123. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  124. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  125. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  126. package/scripts/loop/inspect-run.mjs +8 -2
  127. package/scripts/loop/outer-loop.mjs +8 -4
  128. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  129. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  130. package/scripts/loop/pre-flight-gate.mjs +9 -9
  131. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  132. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  133. package/scripts/loop/provision-worktree.mjs +74 -3
  134. package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
  135. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  136. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  137. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  138. package/scripts/loop/run-queue.mjs +18 -9
  139. package/scripts/loop/run-refinement-audit.mjs +8 -9
  140. package/scripts/loop/run-watch-cycle.mjs +2 -9
  141. package/scripts/loop/sanctioned-commands.mjs +106 -0
  142. package/scripts/loop/steer-loop.mjs +29 -16
  143. package/scripts/loop/validate-pr-body-spec.mjs +223 -0
  144. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  145. package/scripts/pages/build-site.mjs +59 -8
  146. package/scripts/pages/build-state-atlas.mjs +443 -0
  147. package/scripts/projects/_resolve-project.mjs +1 -0
  148. package/scripts/projects/add-queue-item.mjs +60 -54
  149. package/scripts/projects/archive-done-items.mjs +49 -84
  150. package/scripts/projects/ensure-queue-board.mjs +10 -36
  151. package/scripts/projects/list-queue-items.mjs +59 -382
  152. package/scripts/projects/move-queue-item.mjs +21 -449
  153. package/scripts/projects/reconcile-queue.mjs +253 -0
  154. package/scripts/projects/reorder-queue-item.mjs +43 -68
  155. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  156. package/scripts/projects/sync-item-status.mjs +15 -10
  157. package/scripts/refine/_refine-helpers.mjs +21 -5
  158. package/scripts/refine/exit-spike.mjs +18 -8
  159. package/scripts/refine/promote-plan.mjs +22 -16
  160. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  161. package/scripts/refine/refine-plan-file.mjs +13 -4
  162. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  163. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  164. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  165. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  166. package/scripts/refine/validate-plan-file.mjs +1 -1
  167. package/scripts/refine/validate-spike-file.mjs +1 -1
  168. package/scripts/refine/verify.mjs +11 -6
  169. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  170. package/scripts/release/extract-changelog-section.mjs +16 -2
  171. package/skills/copilot-pr-followup/SKILL.md +60 -34
  172. package/skills/dev-loop/SKILL.md +11 -7
  173. package/skills/docs/acceptance-criteria-verification.md +8 -3
  174. package/skills/docs/anti-patterns.md +5 -3
  175. package/skills/docs/artifact-authority-contract.md +30 -11
  176. package/skills/docs/confirmation-rules.md +1 -0
  177. package/skills/docs/contract-style-guide.md +37 -0
  178. package/skills/docs/copilot-loop-operations.md +19 -15
  179. package/skills/docs/cross-harness-regression-contract.md +60 -0
  180. package/skills/docs/debt-remediation-contract.md +1 -1
  181. package/skills/docs/epic-tree-refinement-procedure.md +25 -22
  182. package/skills/docs/issue-intake-procedure.md +26 -19
  183. package/skills/docs/local-planning-flow.md +1 -1
  184. package/skills/docs/local-planning-worked-example.md +1 -1
  185. package/skills/docs/merge-preconditions.md +30 -12
  186. package/skills/docs/plan-file-contract.md +1 -1
  187. package/skills/docs/pr-lifecycle-contract.md +44 -35
  188. package/skills/docs/public-dev-loop-contract.md +60 -44
  189. package/skills/docs/required-rules.json +149 -0
  190. package/skills/docs/retrospective-checkpoint-contract.md +106 -79
  191. package/skills/docs/spike-mode-contract.md +14 -7
  192. package/skills/docs/stop-conditions.md +30 -15
  193. package/skills/docs/tracker-first-loop-state.md +13 -8
  194. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  195. package/skills/docs/validation-policy.md +4 -5
  196. package/skills/docs/workflow-handoff-contract.md +39 -0
  197. package/skills/local-implementation/SKILL.md +62 -145
  198. package/skills/loop-grill/SKILL.md +165 -0
  199. package/.claude/commands/continue.md +0 -15
  200. package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
  201. package/scripts/loop/conductor.mjs +0 -233
  202. package/scripts/loop/detect-stale-runner.mjs +0 -265
  203. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -1,250 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * validate-no-duplicate-rules.mjs — CI guardrail: detect duplicated imperative
4
- * rule text across skill doc files under skills/.
5
- *
6
- * Scans skills/ for Markdown files, extracts imperative sentences (those
7
- * containing "must", "never", "do not", or "require"/"required"), and reports
8
- * any sentence that appears in more than one file.
9
- *
10
- * Symlink note:
11
- * .pi/skills/ -> ../skills on this repo, so scanning skills/ covers both.
12
- *
13
- * False-positive suppression:
14
- * - Fenced code blocks (``` or ~~~)
15
- * - Inline code spans (`)
16
- * - Blockquotes (lines starting with ">")
17
- * - Markdown link URLs (text inside (...))
18
- * - Headings (lines starting with "#")
19
- * - Only cross-file duplicates reported (same-file dupes are intentional)
20
- *
21
- * Exclusion list:
22
- * Duplicates where all occurrences are within canonical contract docs
23
- * (which own their content by design) are suppressed. Duplicates involving
24
- * any non-canonical file are still reported.
25
- * Known intentional duplicates (mirrored skill procedure text) are also
26
- * excluded so that the guardrail enforces new duplication without failing
27
- * on deliberate mirrors.
28
- *
29
- * Exit 0 when clean, exit 1 when duplicates found.
30
- */
31
-
32
- import { readFile, readdir } from "node:fs/promises";
33
- import path from "node:path";
34
- import { fileURLToPath } from "node:url";
35
-
36
- import { isDirectCliRun } from "../_core-helpers.mjs";
37
-
38
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
- const REPO_ROOT = path.resolve(__dirname, "..", "..");
40
- const SKILLS_DIR = path.join(REPO_ROOT, "skills");
41
-
42
- const CANONICAL_CONTRACT_DOCS = new Set([
43
- "skills/docs/copilot-loop-operations.md",
44
- "skills/docs/public-dev-loop-contract.md",
45
- ]);
46
-
47
- /**
48
- * Known intentional duplicates: text that appears in multiple skill files
49
- * because it is deliberately mirrored (shared procedure sections, template
50
- * mirrors, etc.). These are excluded from cross-file duplicate reporting so
51
- * the guardrail stays fatal for new duplicates without noise from mirrors.
52
- */
53
- const KNOWN_INTENTIONAL_DUPLICATES = new Set([
54
- "- **PERSISTENCE RULE: Do not exit your session until the PR is merged or you hit a hard stop that requires conductor authorization.**",
55
- "If any required bundled contract doc is missing from the installed skill layout, treat that as a packaging/installer bug.",
56
- "Each reviewer starts in fresh context with the briefing artifact, inspects the diff, returns findings via output artifacts only, and never edits files.",
57
- "3. **Consolidation:** reconcile all review outputs into a consolidated fix plan with classified findings (must-fix, worth-fixing-now, defer).",
58
- "5. **Fix cycle:** apply only accepted must-fix changes on the same branch.",
59
- "- remains a stop/fix state, never a wait loop",
60
- "Do not create a fresh PR directly in ready-for-review state unless the user explicitly overrides that policy for the current PR scope.",
61
- "Each reviewer starts in fresh context (subagent({context:\"fresh\"}) mandatory), inspects the diff, returns findings via output artifacts only, and never edits files. **Before starting:** run to self-verify fresh context; refuse to proceed on contamination.",
62
- "If includes , then worth-fixing-now findings must be fixed before the gate can reach .",
63
- ]);
64
-
65
- const IMPERATIVE_PATTERNS = [
66
- /\bmust\b/i,
67
- /\bnever\b/i,
68
- /\bdo not\b/i,
69
- /\brequire[sd]?\b/i,
70
- ];
71
-
72
- const MIN_SENTENCE_LENGTH = 20;
73
-
74
- const USAGE = `Usage: validate-no-duplicate-rules.mjs [--help]
75
-
76
- Scan skills/ for duplicated imperative rule text across Markdown files.
77
- Exit 0 when no duplicates found. Exit 1 when duplicates found.
78
-
79
- Options:
80
- --help, -h Show this help`.trim();
81
-
82
-
83
- export async function* collectMarkdownFiles(dir, repoRoot = REPO_ROOT) {
84
- let entries;
85
- try {
86
- entries = await readdir(dir, { withFileTypes: true });
87
- } catch (err) {
88
- // Ignore ENOENT (missing directory) silently; other errors must fail loudly
89
- // so the CI guardrail doesn't silently pass with 0 files scanned.
90
- if (err?.code === "ENOENT") {
91
- return;
92
- }
93
- throw err;
94
- }
95
-
96
- for (const entry of entries) {
97
- const fullPath = path.join(dir, entry.name);
98
-
99
- if (entry.isSymbolicLink()) {
100
- continue;
101
- }
102
-
103
- if (entry.isDirectory()) {
104
- yield* collectMarkdownFiles(fullPath, repoRoot);
105
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
106
- yield fullPath;
107
- }
108
- }
109
- }
110
-
111
- export function isImperativeSentence(sentence) {
112
- return IMPERATIVE_PATTERNS.some((pattern) => pattern.test(sentence));
113
- }
114
-
115
- export function normalizeSentence(text) {
116
- return text.replace(/\s+/g, " ").trim();
117
- }
118
-
119
- /**
120
- * Normalize a repo-relative path to POSIX separators.
121
- * On Windows, path.relative() produces backslashes, which would break
122
- * comparisons against CANONICAL_CONTRACT_DOCS and report formatting.
123
- */
124
- function toPosixPath(p) {
125
- return p.replace(/\\/g, "/");
126
- }
127
-
128
- export function extractSentences(content) {
129
- const lines = content.split(/\r?\n/);
130
- const sentences = [];
131
- let inFencedBlock = false;
132
- let fencedDelimiter = "";
133
-
134
- for (let i = 0; i < lines.length; i++) {
135
- let line = lines[i];
136
-
137
- const rawTrimmed = line.trim();
138
-
139
- if (/^\s*#/.test(line)) {
140
- continue;
141
- }
142
-
143
- if (/^\s*>/.test(line)) {
144
- continue;
145
- }
146
-
147
- const fenceMatch = rawTrimmed.match(/^(```|~~~)/);
148
- if (fenceMatch) {
149
- if (!inFencedBlock) {
150
- inFencedBlock = true;
151
- fencedDelimiter = fenceMatch[1];
152
- continue;
153
- } else if (rawTrimmed.startsWith(fencedDelimiter)) {
154
- inFencedBlock = false;
155
- fencedDelimiter = "";
156
- continue;
157
- }
158
- }
159
-
160
- if (inFencedBlock) {
161
- continue;
162
- }
163
-
164
- line = line
165
- .replace(/`[^`]*`/g, "")
166
- .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
167
-
168
- const parts = line.split(/(?<=[.!?])\s+(?=[A-Z])/);
169
- for (const part of parts) {
170
- const normalized = normalizeSentence(part);
171
- if (normalized.length >= MIN_SENTENCE_LENGTH && isImperativeSentence(normalized)) {
172
- sentences.push({ text: normalized, line: i + 1 });
173
- }
174
- }
175
- }
176
-
177
- return sentences;
178
- }
179
-
180
- export async function scanSkills(skillsDir = SKILLS_DIR, repoRoot = REPO_ROOT) {
181
- const fileMap = new Map();
182
- let totalFilesScanned = 0;
183
-
184
- for await (const filePath of collectMarkdownFiles(skillsDir, repoRoot)) {
185
- const relativePath = toPosixPath(path.relative(repoRoot, filePath));
186
-
187
- totalFilesScanned++;
188
- const content = await readFile(filePath, "utf8");
189
- const sentences = extractSentences(content);
190
-
191
- for (const { text, line } of sentences) {
192
- if (!fileMap.has(text)) {
193
- fileMap.set(text, []);
194
- }
195
- fileMap.get(text).push({ file: relativePath, line });
196
- }
197
- }
198
-
199
- const duplicates = new Map();
200
- for (const [text, occurrences] of fileMap) {
201
- if (KNOWN_INTENTIONAL_DUPLICATES.has(text)) {
202
- continue;
203
- }
204
- const uniqueFiles = new Set(occurrences.map((o) => o.file));
205
- if (uniqueFiles.size > 1) {
206
- // Suppress duplicates where all occurrences are within canonical
207
- // contract docs (these own their content by design and may mirror
208
- // each other). Duplicates that involve any non-canonical file
209
- // are still reported.
210
- const allCanonical = [...uniqueFiles].every((f) => CANONICAL_CONTRACT_DOCS.has(f));
211
- if (!allCanonical) {
212
- duplicates.set(text, occurrences);
213
- }
214
- }
215
- }
216
-
217
- return { fileMap, duplicates, totalFilesScanned };
218
- }
219
-
220
- async function main(argv = process.argv.slice(2)) {
221
- if (argv.includes("--help") || argv.includes("-h")) {
222
- process.stdout.write(`${USAGE}\n`);
223
- return 0;
224
- }
225
-
226
- const { fileMap, duplicates, totalFilesScanned } = await scanSkills();
227
-
228
- if (duplicates.size === 0) {
229
- process.stdout.write(`No duplicate imperative rules found across skill docs.\n`);
230
- process.stdout.write(`\n${totalFilesScanned} files scanned, ${fileMap.size} unique imperative sentences extracted, 0 duplicates found.\n`);
231
- return 0;
232
- }
233
-
234
- process.stdout.write(`Duplicate imperative rules found across skill docs:\n\n`);
235
- for (const [text, occurrences] of duplicates) {
236
- for (const { file, line } of occurrences) {
237
- process.stdout.write(` ${file}:${line}\n`);
238
- }
239
- process.stdout.write(` "${text}"\n\n`);
240
- }
241
-
242
- process.stdout.write(`${totalFilesScanned} files scanned, ${fileMap.size} unique imperative sentences extracted, ${duplicates.size} duplicates found.\n`);
243
- return 1;
244
- }
245
-
246
- // Only run main() when executed directly, not when imported for testing.
247
- if (isDirectCliRun(import.meta.url)) {
248
- const exitCode = await main();
249
- process.exitCode = exitCode;
250
- }
@@ -1,233 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runConductorCycle } from "./run-conductor-cycle.mjs";
3
- import { runConductorMonitor } from "./conductor-monitor.mjs";
4
- import { requireTokenValue } from "../_cli-primitives.mjs";
5
- import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
6
- import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
- import {
8
- loadDevLoopConfig,
9
- resolveWorkflowConfig,
10
- resolveAutonomyStopAt,
11
- resolveGateConfig,
12
- } from "@dev-loops/core/config";
13
- import { readFileSync } from "node:fs";
14
- import path from "node:path";
15
- import { parseArgs } from "node:util";
16
- const USAGE = `Usage: conductor.mjs --repo <owner/name> [--auto-resume] [--cycle-only] [--monitor-only] [--require-retrospective]
17
- Unified conductor entrypoint for dev-loop lifecycle orchestration.`.trim();
18
- const parseError = buildParseError(USAGE);
19
- function checkRetrospectiveGate(cwd, requireRetrospective) {
20
- if (!requireRetrospective) return { blocked: false };
21
- try {
22
- const checkpointPath = path.join(cwd, ".pi", "dev-loop-retrospective-checkpoint.json");
23
- const checkpointText = readFileSync(checkpointPath, "utf8");
24
- const checkpoint = JSON.parse(checkpointText);
25
- const state = typeof checkpoint?.state === "string" ? checkpoint.state.trim().toLowerCase() : null;
26
- if (state === "none" || state === "complete" || state === "skipped") {
27
- return { blocked: false };
28
- }
29
- if (state === "required" || state === "missing" || state === null || state === "") {
30
- return {
31
- blocked: true,
32
- reason: state === "required" || state === "missing"
33
- ? `Retrospective checkpoint pending (state: ${state}). Complete the retrospective before running the conductor.`
34
- : "Retrospective checkpoint file exists but has an unrecognized or empty state; cannot determine retrospective status safely.",
35
- };
36
- }
37
- return {
38
- blocked: true,
39
- reason: `Retrospective checkpoint has an unrecognized state: "${state}".`,
40
- };
41
- } catch (err) {
42
- if (err?.code === "ENOENT") return { blocked: false };
43
- return { blocked: true, reason: `Cannot read retrospective checkpoint: ${err.message}` };
44
- }
45
- }
46
- function parseCliArgs(argv) {
47
- const options = {
48
- help: false,
49
- repo: undefined,
50
- autoResume: false,
51
- cycleOnly: false,
52
- monitorOnly: false,
53
- requireRetrospective: false,
54
- };
55
- const { tokens } = parseArgs({
56
- args: [...argv],
57
- options: {
58
- help: { type: "boolean", short: "h" },
59
- repo: { type: "string" },
60
- "auto-resume": { type: "boolean" },
61
- "cycle-only": { type: "boolean" },
62
- "monitor-only": { type: "boolean" },
63
- "require-retrospective": { type: "boolean" },
64
- },
65
- allowPositionals: true,
66
- strict: false,
67
- tokens: true,
68
- });
69
- for (const token of tokens) {
70
- if (token.kind === "positional") {
71
- throw parseError(`Unknown argument: ${token.value}`);
72
- }
73
- if (token.kind !== "option") {
74
- continue;
75
- }
76
- if (token.name === "help") {
77
- options.help = true;
78
- return options;
79
- }
80
- if (token.name === "repo") {
81
- options.repo = requireTokenValue(token, parseError).trim();
82
- continue;
83
- }
84
- if (token.name === "auto-resume") {
85
- options.autoResume = true;
86
- continue;
87
- }
88
- if (token.name === "cycle-only") {
89
- options.cycleOnly = true;
90
- continue;
91
- }
92
- if (token.name === "monitor-only") {
93
- options.monitorOnly = true;
94
- continue;
95
- }
96
- if (token.name === "require-retrospective") {
97
- options.requireRetrospective = true;
98
- continue;
99
- }
100
- throw parseError(`Unknown argument: ${token.rawName}`);
101
- }
102
- if (options.repo === undefined) {
103
- throw parseError("conductor requires --repo <owner/name>");
104
- }
105
- if (options.cycleOnly && options.monitorOnly) {
106
- throw parseError("--cycle-only and --monitor-only are mutually exclusive");
107
- }
108
- try {
109
- parseRepoSlug(options.repo);
110
- } catch (error) {
111
- throw parseError(error instanceof Error ? error.message : String(error));
112
- }
113
- return options;
114
- }
115
- export async function runConductor(options, runtime = {}) {
116
- const { cycleOnly = false, monitorOnly = false, autoResume = false, requireRetrospective: forceRetrospective = false } = options;
117
- const cwd = runtime.repoRoot || process.cwd();
118
- const loadConfig = runtime.loadConfigImpl || loadDevLoopConfig;
119
- let configLoadResult;
120
- let requireRetrospective = false;
121
- let autonomyStopAt = ["merge"];
122
- let gateConfig = { draft: { requireCi: true }, preApproval: { requireCi: true } };
123
- try {
124
- configLoadResult = await loadConfig({ repoRoot: cwd });
125
- const hasErrors = Array.isArray(configLoadResult.errors) && configLoadResult.errors.length > 0;
126
- if (hasErrors) {
127
- configLoadResult = { ...configLoadResult };
128
- } else {
129
- const cfg = configLoadResult.config ?? {};
130
- requireRetrospective = resolveWorkflowConfig(cfg, "requireRetrospective");
131
- autonomyStopAt = resolveAutonomyStopAt(cfg);
132
- const draftCfg = resolveGateConfig(cfg, "draft");
133
- const preApprovalCfg = resolveGateConfig(cfg, "preApproval");
134
- gateConfig = {
135
- draft: { requireCi: draftCfg.requireCi },
136
- preApproval: { requireCi: preApprovalCfg.requireCi },
137
- };
138
- }
139
- } catch (error) {
140
- const errorMessage = error instanceof Error ? error.message : String(error);
141
- configLoadResult = { config: null, warnings: [], errors: [{ path: "<config>", message: `Failed to load config: ${errorMessage}`, layer: "merged" }] };
142
- }
143
- const effectiveRequireRetrospective = forceRetrospective || requireRetrospective;
144
- const retroGate = checkRetrospectiveGate(cwd, effectiveRequireRetrospective);
145
- if (retroGate.blocked) {
146
- return {
147
- ok: false,
148
- error: retroGate.reason,
149
- repo: options.repo,
150
- blockedByRetrospective: true,
151
- checkedAt: new Date().toISOString(),
152
- };
153
- }
154
- const runCycle = !monitorOnly;
155
- const runMonitor = !cycleOnly;
156
- const cycleResult = runCycle
157
- ? await runConductorCycle({ repo: options.repo, autonomyStopAt, gateConfig }, runtime).catch((error) => ({
158
- ok: false,
159
- error: error instanceof Error ? error.message : String(error),
160
- }))
161
- : null;
162
- const monitorResult = runMonitor
163
- ? await runConductorMonitor({ repo: options.repo, autoResume }, runtime).catch((error) => ({
164
- ok: false,
165
- error: error instanceof Error ? error.message : String(error),
166
- }))
167
- : null;
168
- const cycleOk = cycleResult?.ok === true;
169
- const monitorOk = monitorResult?.ok === true;
170
- return {
171
- ok: (runCycle ? cycleOk : true) && (runMonitor ? monitorOk : true),
172
- repo: options.repo,
173
- checkedAt: new Date().toISOString(),
174
- cycle: cycleResult ?? null,
175
- monitor: monitorResult ?? null,
176
- cycleOk,
177
- monitorOk,
178
- config: {
179
- requireRetrospective: effectiveRequireRetrospective,
180
- configRequireRetrospective: requireRetrospective,
181
- autonomyStopAt,
182
- gateConfig,
183
- configErrors: configLoadResult?.errors?.length ?? 0,
184
- },
185
- summary: {
186
- totalPrs: (cycleResult?.prCount ?? 0) || (monitorResult?.prCount ?? 0),
187
- cycleActions: cycleResult?.actions?.length ?? 0,
188
- needsSubagent: cycleResult?.summary?.needsSubagent ?? 0,
189
- readyToMerge: cycleResult?.summary?.readyToMerge ?? 0,
190
- waiting: cycleResult?.summary?.waiting ?? 0,
191
- blocked: cycleResult?.summary?.blocked ?? 0,
192
- done: cycleResult?.summary?.done ?? 0,
193
- errors: cycleResult?.summary?.errors ?? 0,
194
- queueStatus: monitorResult?.queueStatus ?? "unknown",
195
- needsAttentionCount: monitorResult?.needsAttentionCount ?? 0,
196
- orphanedPrCount: monitorResult?.orphanedPrCount ?? 0,
197
- resumePlanCount: monitorResult?.resumePlanCount ?? 0,
198
- manualAttentionCount: monitorResult?.manualAttentionCount ?? 0,
199
- },
200
- };
201
- }
202
- export async function runCli(
203
- argv = process.argv.slice(2),
204
- {
205
- stdout = process.stdout,
206
- env = process.env,
207
- ghCommand = "gh",
208
- cwd = process.cwd(),
209
- } = {},
210
- ) {
211
- const options = parseCliArgs(argv);
212
- if (options.help) {
213
- stdout.write(`${USAGE}\n`);
214
- return;
215
- }
216
- const result = await runConductor(options, {
217
- env,
218
- ghCommand,
219
- repoRoot: cwd,
220
- });
221
- if (result.ok === false) {
222
- process.stderr.write(`${JSON.stringify(result)}\n`);
223
- process.exitCode = 1;
224
- return;
225
- }
226
- stdout.write(`${JSON.stringify(result)}\n`);
227
- }
228
- if (isDirectCliRun(import.meta.url)) {
229
- runCli().catch((error) => {
230
- process.stderr.write(`${formatCliError(error)}\n`);
231
- process.exitCode = 1;
232
- });
233
- }