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.
Files changed (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
package/cli/index.mjs ADDED
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { realpathSync, constants as fsConstants } from "node:fs";
4
+ import { access } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import {
9
+ describeReadiness,
10
+ executeDevLoopsCommand,
11
+ renderCheckLines,
12
+ summarizeChecks,
13
+ DEV_LOOP_CHECK_IDS,
14
+ } from "../lib/dev-loops-core.mjs";
15
+ import { isUsageError, buildCorrectedArgs } from "@dev-loops/core/cli/retry-wrapper";
16
+ import { createPiAdapter } from "@dev-loops/core/harness";
17
+
18
+ const REPO_ROOT = fileURLToPath(new URL("..", import.meta.url));
19
+
20
+ const SUBCOMMAND_ROUTES = {
21
+ gate: {
22
+ "upsert-verdict": "scripts/github/upsert-checkpoint-verdict.mjs",
23
+ "detect-evidence": "scripts/github/detect-checkpoint-evidence.mjs",
24
+ "write-findings-log": "scripts/github/write-gate-findings-log.mjs",
25
+ "request-copilot": "scripts/github/request-copilot-review.mjs",
26
+ "probe-copilot": "scripts/github/probe-copilot-review.mjs",
27
+ "capture-threads": "scripts/github/capture-review-threads.mjs",
28
+ "reply-resolve": "scripts/github/reply-resolve-review-threads.mjs",
29
+ },
30
+ loop: {
31
+ startup: "scripts/loop/resolve-dev-loop-startup.mjs",
32
+ "build-envelope": "scripts/loop/build-handoff-envelope.mjs",
33
+ outer: "scripts/loop/outer-loop.mjs",
34
+ "watch-cycle": "scripts/loop/run-watch-cycle.mjs",
35
+ handoff: "scripts/loop/copilot-pr-handoff.mjs",
36
+ "watch-initial": "scripts/loop/watch-initial-copilot-pr.mjs",
37
+ "loop-state": "scripts/loop/detect-copilot-loop-state.mjs",
38
+ "reviewer-state": "scripts/loop/detect-reviewer-loop-state.mjs",
39
+ "gate-coordination": "scripts/loop/detect-pr-gate-coordination-state.mjs",
40
+ "linked-issue-pr": "scripts/github/detect-linked-issue-pr.mjs",
41
+ "info": "scripts/loop/info.mjs",
42
+ "issue-refinement": "scripts/loop/detect-issue-refinement-artifact.mjs",
43
+ "debt-remediate": "scripts/loop/debt-remediate.mjs",
44
+ },
45
+ pr: {
46
+ "create-draft": "scripts/github/create-draft-pr.mjs",
47
+ "ready-for-review": "scripts/github/ready-for-review.mjs",
48
+ "reconcile-draft": "scripts/github/reconcile-draft-gate.mjs",
49
+ },
50
+ project: {
51
+ list: "scripts/projects/list-queue-items.mjs",
52
+ add: "scripts/projects/add-queue-item.mjs",
53
+ move: "scripts/projects/move-queue-item.mjs",
54
+ reorder: "scripts/projects/reorder-queue-item.mjs",
55
+ ensure: "scripts/projects/ensure-queue-board.mjs",
56
+ },
57
+ queue: {
58
+ run: "scripts/loop/run-queue.mjs",
59
+ },
60
+ inspect: {
61
+ run: "scripts/loop/inspect-run.mjs",
62
+ viewer: "scripts/loop/inspect-run-viewer.mjs",
63
+ },
64
+ refine: {
65
+ verify: "scripts/refine/verify.mjs",
66
+ },
67
+ };
68
+
69
+ const TOP_LEVEL_COMMANDS = new Set(["help", "status", "doctor", "gates", "hide"]);
70
+
71
+ const HELP_CATEGORY_LABELS = {
72
+ gate: "Gate verdicts, evidence, and review operations",
73
+ loop: "Loop lifecycle",
74
+ pr: "PR helpers",
75
+ project: "GitHub Projects queue helpers",
76
+ inspect: "Inspection (Pi extension only)",
77
+ refine: "Epic tree refinement verification",
78
+ };
79
+
80
+ const TOP_LEVEL_HELP_CATEGORY_ORDER = ["gate", "loop", "pr", "project", "inspect", "refine"];
81
+
82
+ const SUBCOMMAND_DESCRIPTIONS = {
83
+ gate: {
84
+ "upsert-verdict": "Post/update gate review comment",
85
+ "detect-evidence": "Check merge preconditions",
86
+ "write-findings-log": "Write disposition ledger",
87
+ "request-copilot": "Request Copilot review",
88
+ "probe-copilot": "Poll for Copilot review activity",
89
+ "capture-threads": "Capture review threads",
90
+ "reply-resolve": "Reply and resolve review threads",
91
+ },
92
+ loop: {
93
+ startup: "Resolve dev-loop startup bundle",
94
+ "build-envelope": "Build handoff envelope from startup output",
95
+ outer: "Run outer-loop detection",
96
+ "watch-cycle": "Run Copilot wait cycle",
97
+ handoff: "Copilot PR handoff",
98
+ "watch-initial": "Watch initial Copilot PR",
99
+ "loop-state": "Detect Copilot loop state",
100
+ "reviewer-state": "Detect reviewer loop state",
101
+ "gate-coordination": "Detect PR gate coordination state",
102
+ "linked-issue-pr": "Detect linked issue ↔ PR",
103
+ "issue-refinement": "Detect issue refinement artifact",
104
+ info: "Show read-only issue/PR state summary",
105
+ "debt-remediate": "File debt remediation issues",
106
+ },
107
+ pr: {
108
+ "create-draft": "Create draft PR",
109
+ "ready-for-review": "Mark PR ready for review",
110
+ "reconcile-draft": "Reconcile non-draft PR",
111
+ },
112
+ project: {
113
+ list: "List queue board items",
114
+ add: "Add issue/PR to queue board",
115
+ move: "Move queue item between Status columns",
116
+ reorder: "Reorder queue board items",
117
+ ensure: "Create/repair queue board bootstrap surface",
118
+ },
119
+ queue: {
120
+ run: "Run queue driver",
121
+ },
122
+ inspect: {
123
+ run: "Inspect run state",
124
+ viewer: "Start inspection viewer",
125
+ },
126
+ refine: {
127
+ verify: "Verify epic tree refinement integrity",
128
+ },
129
+ };
130
+
131
+ const CLI_SETUP_GUIDANCE = {
132
+ "gh-installed": "Install GitHub CLI to enable remote GitHub/Copilot workflows.",
133
+ "gh-auth": "Run `gh auth login` so remote GitHub/Copilot workflows can use your GitHub session.",
134
+ "subagent-command": "Install or enable subagent support so the `subagent` command is available.",
135
+ "git-repo": "Run the command from a git repository checkout before using repo-scoped workflows.",
136
+ };
137
+
138
+ function spawnResult(command, args, options = {}) {
139
+ try {
140
+ const result = spawnSync(command, args, { encoding: "utf8", ...options });
141
+ return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
142
+ } catch {
143
+ return { ok: false, stdout: "", stderr: "" };
144
+ }
145
+ }
146
+
147
+ function executableCandidates(command, platform, pathExt) {
148
+ if (platform !== "win32") return [command];
149
+ if (path.extname(command)) return [command];
150
+ const extensions = [...new Set(pathExt.split(";").map((e) => e.trim()).filter(Boolean))];
151
+ return extensions.map((ext) => `${command}${ext}`);
152
+ }
153
+
154
+ async function commandExists(
155
+ command,
156
+ { searchPath = process.env.PATH ?? "", platform = process.platform, pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD" } = {},
157
+ ) {
158
+ if (/[\\/]/.test(command)) return false;
159
+ const accessMode = platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK;
160
+ for (const entry of searchPath.split(path.delimiter)) {
161
+ if (!entry) continue;
162
+ for (const candidateName of executableCandidates(command, platform, pathExt)) {
163
+ try { await access(path.join(entry, candidateName), accessMode); return true; } catch { /* continue */ }
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+
169
+ function buildSubcommandLines(category, { includeHeader = false } = {}) {
170
+ const routes = SUBCOMMAND_ROUTES[category];
171
+ if (!routes) return [];
172
+ const descriptions = SUBCOMMAND_DESCRIPTIONS[category] ?? {};
173
+ const lines = Object.keys(routes).map((subcommand) => {
174
+ const description = descriptions[subcommand];
175
+ return description ? ` ${subcommand.padEnd(16)} ${description}` : ` ${subcommand}`;
176
+ });
177
+ if (!includeHeader) return lines;
178
+ const label = HELP_CATEGORY_LABELS[category] ?? `${category} helpers`;
179
+ return [`- dev-loops ${category} <sub> [...] ${label}`, ...lines];
180
+ }
181
+
182
+ function buildCategoryHelp(category) {
183
+ const routes = SUBCOMMAND_ROUTES[category];
184
+ if (!routes) return [`Unknown category: ${category}`];
185
+ return [
186
+ `dev-loops ${category} <subcommand> [...]`,
187
+ "",
188
+ "Available subcommands:",
189
+ ...buildSubcommandLines(category),
190
+ ];
191
+ }
192
+
193
+ function buildCliHelpLines() {
194
+ return [
195
+ "dev-loops help",
196
+ "",
197
+ "Workflow entry:",
198
+ "- /skill:dev-loop (in Pi) or `subagent dev-loop` — single public entrypoint; routing handles the rest",
199
+ "",
200
+ "Commands:",
201
+ "- dev-loops help Show this help",
202
+ "- dev-loops status Show readiness snapshot",
203
+ "- dev-loops doctor Show full diagnostic checks",
204
+ "- dev-loops gates Print gate state",
205
+ "",
206
+ "Subcommands:",
207
+ ...TOP_LEVEL_HELP_CATEGORY_ORDER.flatMap((category) => buildSubcommandLines(category, { includeHeader: true })),
208
+ "",
209
+ "Use `dev-loops <category> <subcommand> --help` for per-subcommand usage.",
210
+ "",
211
+ "`/dev-loops hide` remains an extension-only Pi command.",
212
+ "Use `pi install git:github.com/mfittko/dev-loops` to install skills and agents, or",
213
+ "`pi update git:github.com/mfittko/dev-loops` to refresh the package.",
214
+ ];
215
+ }
216
+
217
+ function buildCliUsageLines(action) {
218
+ switch (action) {
219
+ case "help": case "status": case "doctor": case "gates":
220
+ return ["Usage:", `- dev-loops ${action}`];
221
+ case "hide":
222
+ return ["Usage:", "- dev-loops hide", "`hide` is only supported without extra arguments, and only inside the Pi extension."];
223
+ default:
224
+ throw new Error(`Unknown CLI usage action: ${action}`);
225
+ }
226
+ }
227
+
228
+ function orderedCliSetupSteps(checks) {
229
+ const byId = new Map(checks.map((c) => [c.id, c]));
230
+ const steps = [...new Set(DEV_LOOP_CHECK_IDS.filter((id) => byId.get(id)?.ok === false).map((id) => CLI_SETUP_GUIDANCE[id]))];
231
+ if (steps.length > 0) return steps.map((step, i) => `${i + 1}. ${step}`);
232
+ return [
233
+ "1. Use `/skill:dev-loop` (in Pi) or `subagent dev-loop` to start or continue a dev loop — the single public entry.",
234
+ "2. Run `dev-loops status` whenever you want a concise readiness snapshot.",
235
+ "3. Use `pi install git:github.com/mfittko/dev-loops` to install the package, or `pi update git:github.com/mfittko/dev-loops` to refresh it.",
236
+ ];
237
+ }
238
+
239
+ function writeLines(stream, lines) { stream.write(`${lines.join("\n")}\n`); }
240
+
241
+ export function createCliRuntime({
242
+ adapter = createPiAdapter(),
243
+ cwd, searchPath,
244
+ platform, pathExt,
245
+ } = {}) {
246
+ const effectiveCwd = cwd ?? adapter.getCwd();
247
+ const effectiveSearchPath = searchPath ?? adapter.getEnv().PATH ?? "";
248
+ const effectivePlatform = platform ?? process.platform;
249
+ const effectivePathExt = pathExt ?? adapter.getEnv().PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
250
+ return {
251
+ surface: "cli",
252
+ cwd: effectiveCwd,
253
+ async commandExists(command) { return commandExists(command, { searchPath: effectiveSearchPath, platform: effectivePlatform, pathExt: effectivePathExt }); },
254
+ async ghAuthOk() { return spawnResult("gh", ["auth", "status"], { cwd: effectiveCwd }).ok; },
255
+ async insideGitRepo() { return spawnResult("git", ["rev-parse", "--is-inside-work-tree"], { cwd: effectiveCwd }).ok; },
256
+ async getSubagentAvailability() {
257
+ const ok = await commandExists("subagent", { searchPath: effectiveSearchPath, platform: effectivePlatform, pathExt: effectivePathExt });
258
+ return { ok, availableDetail: "`subagent` command is available.", unavailableDetail: "Install or enable subagent support so `subagent` is available." };
259
+ },
260
+ };
261
+ }
262
+
263
+ // ── Subcommand routing dispatch ────────────────────────────────────
264
+
265
+ function resolveSubcommandRoute(args) {
266
+ if (args.length === 0) return null;
267
+ const category = args[0];
268
+ const routes = SUBCOMMAND_ROUTES[category];
269
+ if (!routes) return null;
270
+
271
+ if (args.length < 2) {
272
+ const subs = Object.keys(routes).join(", ");
273
+ return { error: `Missing subcommand for '${category}'. Available: ${subs}` };
274
+ }
275
+
276
+ const subcommand = args[1];
277
+ const scriptPath = routes[subcommand];
278
+ if (!scriptPath) {
279
+ const subs = Object.keys(routes).join(", ");
280
+ return { error: `Unknown subcommand '${subcommand}' for '${category}'. Available: ${subs}` };
281
+ }
282
+
283
+ return {
284
+ scriptPath: path.resolve(REPO_ROOT, scriptPath),
285
+ forwardedArgs: args.slice(2),
286
+ };
287
+ }
288
+
289
+ function parseTopLevelCommand(argv) {
290
+ const args = [...argv];
291
+ if (args.length === 0) return { kind: "help" };
292
+
293
+ const [cmd, sub] = args;
294
+
295
+ // Bare --help / -h
296
+ if (cmd === "--help" || cmd === "-h") return { kind: "help" };
297
+
298
+ // Top-level commands
299
+ if (TOP_LEVEL_COMMANDS.has(cmd)) {
300
+ if (args.some((a) => a === "--help" || a === "-h")) return { kind: "help" };
301
+ if (args.length > 1) return { kind: "malformed", message: `\`${cmd}\` does not accept additional arguments.`, usageAction: cmd };
302
+ return { kind: "action", action: cmd };
303
+ }
304
+
305
+ // Subcommand routing
306
+ const routes = SUBCOMMAND_ROUTES[cmd];
307
+ if (routes) {
308
+ // If second arg is --help/-h or missing, show category help
309
+ if (!sub || sub === "--help" || sub === "-h") {
310
+ return { kind: "category_help", category: cmd };
311
+ }
312
+ // Check if any remaining arg is --help — delegate to script
313
+ if (args.slice(1).some((a) => a === "--help" || a === "-h")) {
314
+ const scriptPath = routes[sub];
315
+ if (!scriptPath) return { kind: "category_help", category: cmd };
316
+ return { kind: "subcommand_help", scriptPath: path.resolve(REPO_ROOT, scriptPath) };
317
+ }
318
+ const route = resolveSubcommandRoute(args);
319
+ if (route) return { kind: "subcommand", ...route };
320
+ return { kind: "category_help", category: cmd };
321
+ }
322
+
323
+ // Unknown
324
+ return { kind: "malformed", message: `Unrecognized command: ${cmd}.` };
325
+ }
326
+
327
+ export async function runCli({
328
+ argv = process.argv.slice(2),
329
+ stdout = process.stdout,
330
+ stderr = process.stderr,
331
+ runtime,
332
+ cwd = process.cwd(),
333
+ } = {}) {
334
+ const fromTop = parseTopLevelCommand(argv);
335
+
336
+ switch (fromTop.kind) {
337
+ case "help": {
338
+ writeLines(stdout, buildCliHelpLines());
339
+ return 0;
340
+ }
341
+ case "category_help": {
342
+ writeLines(stdout, buildCategoryHelp(fromTop.category));
343
+ return 0;
344
+ }
345
+ case "subcommand_help": {
346
+ const result = spawnSync("node", [fromTop.scriptPath, "--help"], {
347
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
348
+ });
349
+ if (result.stdout) stdout.write(result.stdout);
350
+ if (result.stderr) stderr.write(result.stderr);
351
+ return result.status ?? (result.signal ? 1 : result.error ? 1 : 0);
352
+ }
353
+ case "action": {
354
+ const activeRuntime = runtime ?? createCliRuntime({ adapter: createPiAdapter({ cwd }), cwd });
355
+ const result = await executeDevLoopsCommand({ input: argv, surface: "cli", runtime: activeRuntime, stdout });
356
+ switch (result.kind) {
357
+ case "help": { writeLines(stdout, buildCliHelpLines()); return 0; }
358
+ case "checks": {
359
+ const summary = summarizeChecks(result.checks);
360
+ const readiness = describeReadiness(result.checks);
361
+ const lines = [
362
+ `dev-loops ${result.action}: ${summary.ok}/${summary.total} checks passed`,
363
+ `Local loop readiness: ${readiness.localReady ? "ready" : "needs setup"}`,
364
+ `Remote GitHub/Copilot readiness: ${readiness.remoteReady ? "ready" : "needs setup"}`,
365
+ ];
366
+ if (result.action === "status") { lines.push("Suggested next steps:", ...orderedCliSetupSteps(result.checks)); }
367
+ else { lines.push(...renderCheckLines(result.checks)); }
368
+ writeLines(stdout, lines);
369
+ return 0;
370
+ }
371
+ case "unsupported": { writeLines(stderr, [result.message]); return 1; }
372
+ case "gates": { return 0; }
373
+ case "malformed": {
374
+ const lines = [result.message, ...buildCliHelpLines()];
375
+ if (result.usageAction) lines.splice(1, 0, ...buildCliUsageLines(result.usageAction));
376
+ writeLines(stderr, lines);
377
+ return 1;
378
+ }
379
+ default: throw new Error(`Unhandled CLI result: ${result.kind}`);
380
+ }
381
+ }
382
+ case "subcommand": {
383
+ if (fromTop.error) { writeLines(stderr, [fromTop.error]); return 1; }
384
+ const scriptArgs = fromTop.forwardedArgs || [];
385
+ const result = spawnSync("node", [fromTop.scriptPath, ...scriptArgs], {
386
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
387
+ });
388
+ // Retry on usage/flag errors: parse usage for valid flags, retry once (#483)
389
+ if (result.status !== 0 && isUsageError(result.stderr)) {
390
+ const correctedArgs = buildCorrectedArgs(scriptArgs, result.stderr);
391
+ if (correctedArgs && correctedArgs.length > 0) {
392
+ const retryResult = spawnSync("node", [fromTop.scriptPath, ...correctedArgs], {
393
+ cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
394
+ });
395
+ if (retryResult.stdout) stdout.write(retryResult.stdout);
396
+ if (retryResult.stderr) stderr.write(retryResult.stderr);
397
+ return retryResult.status ?? (retryResult.signal ? 1 : retryResult.error ? 1 : 0);
398
+ }
399
+ }
400
+ if (result.stdout) stdout.write(result.stdout);
401
+ if (result.stderr) stderr.write(result.stderr);
402
+ return result.status ?? (result.signal ? 1 : result.error ? 1 : 0);
403
+ }
404
+ case "malformed": {
405
+ const lines = [fromTop.message, ...buildCliHelpLines()];
406
+ if (fromTop.usageAction) lines.splice(1, 0, ...buildCliUsageLines(fromTop.usageAction));
407
+ writeLines(stderr, lines);
408
+ return 1;
409
+ }
410
+ default:
411
+ throw new Error(`Unhandled parse result: ${fromTop.kind}`);
412
+ }
413
+ }
414
+
415
+ const invokedAsScript = (() => {
416
+ if (!process.argv[1]) return false;
417
+ try {
418
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
419
+ } catch { return false; }
420
+ })();
421
+
422
+ if (invokedAsScript) {
423
+ process.exitCode = await runCli();
424
+ }
@@ -0,0 +1,233 @@
1
+ # Extension scaffold
2
+
3
+ `dev-loops` ships a lightweight package extension for readiness UX plus one bounded local UI lifecycle seam.
4
+
5
+ Installing the package exposes two thin wrappers over one shared deterministic core:
6
+ - the Pi extension command family rooted at `/dev-loops`
7
+ - the shell CLI entrypoint `dev-loops`
8
+ - a bounded post-merge helper that queues one `pi update git:github.com/mfittko/dev-loops` after a successful in-session `gh pr merge ...` or `git merge ...` inside this repo and flushes it on `agent_end`
9
+
10
+ Installing the package with `pi install git:github.com/mfittko/dev-loops` exposes the packaged skills through `package.json` `pi.skills`, and the extension syncs packaged agent files (`agents/*.agent.md`) into `~/.agents/` on `session_start`.
11
+
12
+ ## Command surface
13
+
14
+ - `/dev-loops`
15
+ - defaults to help output for the available subcommands
16
+ - `/dev-loops status`
17
+ - concise readiness summary plus lightweight next steps
18
+ - `/dev-loops doctor`
19
+ - full diagnostic report with explicit pass/fail detail
20
+ - `/dev-loops hide`
21
+ - removes the readiness widget cleanly
22
+ - `/dev-loops inspect open [--repo <owner/name>]`
23
+ - start or reuse the managed local inspect-run viewer and best-effort open it in the browser
24
+ - `/dev-loops inspect resume [--repo <owner/name>]`
25
+ - reattach only to a confirmed live managed inspect-run viewer; fails closed when nothing live is managed
26
+ - `/dev-loops inspect status [--repo <owner/name>]`
27
+ - report one bounded local lifecycle state plus the current URL when known
28
+ - `/dev-loops inspect stop [--repo <owner/name>]`
29
+ - stop only the recorded managed inspect-run viewer process
30
+ - `/dev-loops inspect restart [--repo <owner/name>]`
31
+ - explicitly restart the recorded managed inspect-run viewer; never kill an unknown listener
32
+ - `dev-loops`
33
+ - defaults to help output for the available subcommands
34
+ - `dev-loops help`
35
+ - prints shell help for the shared command family
36
+ - `dev-loops status`
37
+ - prints the concise readiness summary in shell-friendly output
38
+ - `dev-loops doctor`
39
+ - prints the full diagnostic report in shell-friendly output
40
+ - `dev-loops gates`
41
+ - prints active review angles with their prompts from config
42
+ - `/dev-loops gates`
43
+ - same as above, but inside the Pi extension
44
+ - `dev-loops hide`
45
+ - is intentionally unsupported and exits non-zero with a shell-friendly stderr message because `hide` is session-local Pi UI behavior
46
+
47
+ ## Inspect local UI lifecycle ownership
48
+
49
+ This slice is intentionally narrow.
50
+
51
+ Extension-owned behavior:
52
+ - operator-facing lifecycle UX under `/dev-loops inspect ...`
53
+ - repo-local managed-instance record at `.pi/ui-servers/inspect-run-viewer.json`
54
+ - safe URL discovery, liveness checks, resume/reattach, stop, and explicit restart handling
55
+ - best-effort browser open
56
+ - fail-closed handling for stale ownership and unknown listeners
57
+
58
+ Viewer-script-owned behavior:
59
+ - HTTP server implementation
60
+ - viewer HTML/JS rendering
61
+ - inbox and query-state behavior
62
+ - snapshot loading through the existing adapter
63
+ - read-only route behavior and localhost safety rules
64
+
65
+ Lifecycle states reported by the extension-managed seam are intentionally bounded to:
66
+ - `running`
67
+ - `stopped`
68
+ - `stale_record`
69
+ - `conflict_unmanaged_listener`
70
+
71
+ Guard rails for this seam:
72
+ - loopback-first local-only posture
73
+ - no remote/public hosting
74
+ - no generic local app platform
75
+ - no background watcher/supervisor behavior
76
+ - no inspect-run viewer redesign
77
+
78
+ ## Current readiness checks
79
+
80
+ The extension currently reports on:
81
+ - `gh` installed
82
+ - `gh` authenticated
83
+ - `subagent` command available
84
+ - inside a git repository
85
+
86
+ Readiness and help messaging should lead with `dev-loop` as the single public workflow entrypoint. Internal compatibility seams may still exist for runtime/routing purposes, but the readiness surface should not present them as separate user-facing checks or workflow choices.
87
+
88
+ The messaging distinguishes between local loop readiness and remote GitHub/Copilot readiness. Missing `gh` or `gh auth` blocks remote-loop readiness, but does not imply that local phase-based work is completely unavailable.
89
+
90
+ ## Package install contract for this phase
91
+
92
+ - `pi install git:github.com/mfittko/dev-loops` is the distribution mechanism for the extension, skills, scripts, packaged agents, and required installed runtime contract docs
93
+ - `pi install -l git:github.com/mfittko/dev-loops` is the project-local replacement for the old `install repo` flow
94
+ - `pi update git:github.com/mfittko/dev-loops` refreshes an installed package
95
+ - source-tree canonical contract docs live under `skills/docs/`; installer/package output must ship this shared docs bundle with the installed skills subtree: [Public Dev Loop Contract](../skills/docs/public-dev-loop-contract.md) and [Retrospective Checkpoint Contract](../skills/docs/retrospective-checkpoint-contract.md)
96
+ - installed skill/runtime guidance must read those bundled shared docs (from installed `skills/<skill>/`, resolve via `../docs/`) instead of assuming a source checkout is present; a missing bundled contract doc is a packaging/installer bug
97
+ - packaged agents are refreshed into `~/.agents/` on each `session_start`
98
+ - `/dev-loops install ...` and `/dev-loops update ...` are removed; use `pi install` / `pi update` directly instead
99
+
100
+ ## Configuration
101
+
102
+ The dev-loop workflow is driven by a YAML config at `.pi/dev-loop/defaults.yaml` (shipped with the package) and an optional consumer settings file at `.devloops` at repo root (the loader also accepts `.devloops.yaml`, `.devloops.yml`, and `.devloops.json`; legacy `.pi/dev-loop/settings.*` and `overrides.*` still load as fallbacks with a deprecation warning).
103
+
104
+ ### How consumers customize config
105
+
106
+ Create `.devloops` at your project root. It merges on top of the shipped defaults. Accepted formats: `.devloops` (bare, YAML-format), `.devloops.yaml`, `.devloops.yml`, or `.devloops.json`. Legacy `.pi/dev-loop/settings.*` and `overrides.*` still load as fallbacks with a deprecation warning. You can override any section, including workflow policy defaults:
107
+
108
+ ```yaml
109
+ # Example: add a custom review angle with a dedicated persona agent
110
+ gates:
111
+ preApproval:
112
+ angles:
113
+ - dry
114
+ - kiss
115
+ - yagni
116
+ - security # your custom angle
117
+
118
+ personas:
119
+ security:
120
+ persona: security-reviewer
121
+ prompt: >-
122
+ Audit for auth bypasses, secret leaks, insecure defaults,
123
+ unsafe command execution, and data exposure risks.
124
+ defaultModel: null
125
+
126
+ # Override an existing angle's prompt
127
+ dry:
128
+ persona: review
129
+ prompt: >-
130
+ Flag duplication. In this repo, also check for duplicated
131
+ contract language across docs/ and skills/.
132
+ defaultModel: null
133
+
134
+ # Override gate requirements
135
+ refinement:
136
+ fanOut: 5 # run 5 parallel review variants instead of 3
137
+
138
+ autonomy:
139
+ stopAt:
140
+ - draft-pr
141
+ - merge # stop for confirmation at both gates
142
+
143
+ workflow:
144
+ requireRetrospective: true
145
+ requireRetrospectiveGate: true
146
+ requireDraftFirst: true
147
+ devModeDefault: true
148
+ ```
149
+
150
+ ### Available review angles
151
+
152
+ The shipped defaults activate these angles. Additional angles are available as opt-in — add them to your `gates.draft.angles` or `gates.preApproval.angles` and they'll use the prompts defined in the personas registry. Opt-in prompts are generic and can be overridden in consumer repos through `personas.<angle>.prompt` without depending on this repository's audit examples.
153
+
154
+ | Default (active) | Opt-in (add to gates) |
155
+ |---|---|
156
+ | `dry` — duplication | `ocp` — Open/Closed (extension over modification) |
157
+ | `kiss` — over-engineering | `lsp` — Liskov Substitution (subtype contracts) |
158
+ | `yagni` — speculative features | `isp` — Interface Segregation (fat interfaces) |
159
+ | `srp` — Single Responsibility | `dip` — Dependency Inversion (abstractions) |
160
+ | `soc` — Separation of Concerns | `docs` — documentation links, command references, stale docs |
161
+ | `deep` — structural quality / deslop audit | |
162
+ | `scope` — scope compliance (draft gate) | `link-check` — Markdown links, anchors, doc paths |
163
+ | `coverage` — test coverage (draft gate) | `config-drift` — config/schema/docs/runtime disagreement |
164
+ | `correctness` — acceptance criteria (draft gate) | `gate-evidence` — missing/stale gate-review PR evidence |
165
+ | `ci-guard` — CI/workflow reproducibility (draft gate) | `no-op` — ineffective tool or command usage |
166
+ | `contract-surface` — schema/runtime/docs plus CLI help/stdout/stderr drift (draft gate) | `input-validation` — CLI/API args, repo slugs, IDs, sentinels |
167
+ | | `packaging-runtime` — installers, bundles, copied assets, runtime imports |
168
+ | | `state-concurrency` — state files, locks, polling/process cleanup |
169
+ | | `renderer-security` — HTML/attribute/URL escaping and user content |
170
+ | | `determinism` — ordering, tie-breakers, strict stubs, env/time independence |
171
+
172
+ ### Workflow defaults
173
+
174
+ The optional `workflow` family carries repo-level workflow posture without hardcoding it into prose-only guidance. Shipped defaults stay permissive:
175
+
176
+ ```yaml
177
+ workflow:
178
+ requireRetrospective: false
179
+ requireRetrospectiveGate: false
180
+ requireDraftFirst: false
181
+ devModeDefault: false
182
+ ```
183
+
184
+ - `requireRetrospective` — when enabled by repo settings, the next qualifying GitHub-first async start/resume must honor the retrospective checkpoint gate
185
+ - `requireRetrospectiveGate` — when enabled by repo settings, merge readiness after `pre_approval_gate` requires a completed retrospective checkpoint that explicitly approves merge. The retrospective must include: `state: "complete"`, `mergeApproved: true`, `followedWorkingAgreement` (boolean), `gateQuality` (string), `unexpectedFindings` (string), and `mergeRecommendation` (string). A `SKIPPED` retrospective does not satisfy the merge gate when this flag is enabled.
186
+ - `requireDraftFirst` — marks draft-first PR creation as required workflow policy for repos that opt in
187
+ - `devModeDefault` — declares that local implementation should default to formal dev mode; this is config-only for now and establishes source-of-truth config plus docs for future runtime consumers
188
+
189
+ ### Config precedence
190
+
191
+ 1. Built-in defaults (`packages/core/src/config/config.mjs` `BUILT_IN_DEFAULTS`)
192
+ 2. Shipped defaults (`.pi/dev-loop/defaults.yaml` — committed in source repo)
193
+ 3. Consumer settings (`.devloops` at repo root — preferred; `.devloops.yaml`/`.devloops.yml`/`.devloops.json` also load; legacy `.pi/dev-loop/settings.*` and `overrides.*` still load as fallbacks with deprecation warning)
194
+
195
+ ### Adding custom review angles
196
+
197
+ 1. Add the angle name to `gates.draft.angles` or `gates.preApproval.angles`
198
+ 2. Add a `personas.<angle>` entry with a `persona` agent name and a `prompt` instruction
199
+ 3. Create the corresponding `Agent file` (`agents/<persona>.agent.md`) if using a new persona
200
+ 4. Optionally set a per-angle model override via `models.roles.<angle>`
201
+
202
+ ### Config format
203
+
204
+ YAML is preferred (`.yaml` or `.yml`). JSON (`.json`) is supported as a fallback for backward compatibility. When both exist, YAML takes priority.
205
+
206
+ Config is validated at runtime by Zod schemas (`packages/core/src/config/config.mjs`).
207
+
208
+ ## Runtime / build / test contract
209
+
210
+ Current Phase 3+ contract:
211
+ - Node runtime floor: `>=20` (from `package.json`)
212
+ - Pi host expectations are documented from current peer dependencies rather than a tested pinned Pi version range
213
+ - the extension is source-loaded from `./extension/index.ts` through `package.json` `pi.extensions`
214
+ - the package exposes `skills` through `package.json` `pi.skills` for install-based global skill loading
215
+ - the shell CLI is exposed through `package.json` `bin.dev-loops`
216
+ - the extension syncs packaged agent files (`agents/*.agent.md`) into `~/.agents/` on `session_start` so user-level agents are available outside this repo
217
+ - package install/update happens through `pi install` / `pi update`
218
+ - this phase does not yet claim a specific supported `gh` version; it only checks `gh` presence and authentication state
219
+ - this phase does not require a separate compiled build or `dist/` pipeline
220
+
221
+ Root verification and test commands are intentionally explicit:
222
+ - `npm run verify` is the canonical root verification path (`npm test` + `npm run test:dev-loop`)
223
+ - `npm test` runs the current root test suite (`test:assets`, `test:extension`, `test:scripts`, and `test:core`)
224
+ - `npm run test:extension`
225
+ - `npm run test:extension` currently expands to one `node --import tsx --test ...` invocation in `package.json`; prefer the script entrypoint over copying the file list into downstream docs or runbooks
226
+ - `npm run test:scripts`
227
+ - `npm run test:assets`
228
+ - `npm run test:dev-loop`
229
+ - `npm run test:playwright:viewer` remains an explicit viewer/browser smoke, not part of the default root verify path
230
+
231
+ ## Design rule
232
+
233
+ Both wrappers should stay thin. Shared workflow mechanics should live in deterministic `packages/core/` modules and `scripts/`, not in extension-only or CLI-only command logic. Runtime command support that bridges both surfaces belongs in `lib/dev-loops-core.mjs`. See [Library vs Packages Core Boundary](../docs/lib-vs-packages-core-boundary.md) for the full ownership rule.