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
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.
|