@dev-loops/core 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 (54) hide show
  1. package/bin/capture-deep-persona-signals.mjs +143 -0
  2. package/bin/ensure-phase-files.mjs +7 -0
  3. package/bin/log-bash-exit-1.mjs +7 -0
  4. package/bin/parse-review-threads.mjs +7 -0
  5. package/package.json +78 -0
  6. package/src/analysis/change-classifier.mjs +146 -0
  7. package/src/analysis/diff-analyzer.mjs +285 -0
  8. package/src/bash-exit-one.mjs +130 -0
  9. package/src/cli/helpers.mjs +22 -0
  10. package/src/cli/primitives.mjs +70 -0
  11. package/src/cli/retry-wrapper.mjs +169 -0
  12. package/src/cli/subcommand-runner.mjs +246 -0
  13. package/src/config/config.mjs +965 -0
  14. package/src/debt/cluster.mjs +240 -0
  15. package/src/debt/debt-finding.mjs +68 -0
  16. package/src/debt/debt-signal.mjs +46 -0
  17. package/src/debt/deep-persona-signals.mjs +266 -0
  18. package/src/debt/remediation-to-issue.mjs +121 -0
  19. package/src/debt/score.mjs +127 -0
  20. package/src/debt/shape.mjs +214 -0
  21. package/src/github/copilot-helpers.mjs +343 -0
  22. package/src/github/repo-slug.mjs +105 -0
  23. package/src/github/review-threads.mjs +343 -0
  24. package/src/harness/adapter.mjs +57 -0
  25. package/src/harness/index.mjs +3 -0
  26. package/src/harness/noop-adapter.mjs +22 -0
  27. package/src/harness/pi-adapter.mjs +47 -0
  28. package/src/loop/async-start-contract.mjs +170 -0
  29. package/src/loop/conductor-routing.mjs +817 -0
  30. package/src/loop/copilot-ci-status.mjs +255 -0
  31. package/src/loop/copilot-loop-iterations.mjs +161 -0
  32. package/src/loop/copilot-loop-state.mjs +510 -0
  33. package/src/loop/handoff-envelope.mjs +800 -0
  34. package/src/loop/issue-refinement-artifact.mjs +268 -0
  35. package/src/loop/lifecycle-state.mjs +342 -0
  36. package/src/loop/phase-files.mjs +187 -0
  37. package/src/loop/policy-constants.mjs +17 -0
  38. package/src/loop/pr-gate-coordination.mjs +1278 -0
  39. package/src/loop/public-dev-loop-routing-contract.mjs +277 -0
  40. package/src/loop/public-dev-loop-routing.mjs +1746 -0
  41. package/src/loop/queue-board-ordering.mjs +38 -0
  42. package/src/loop/queue-board-sync.mjs +223 -0
  43. package/src/loop/queue-driver.mjs +164 -0
  44. package/src/loop/queue-parallel.mjs +190 -0
  45. package/src/loop/queue-state.mjs +230 -0
  46. package/src/loop/retrospective-checkpoint.mjs +178 -0
  47. package/src/loop/reviewer-loop-state.mjs +456 -0
  48. package/src/loop/run-inspection.mjs +604 -0
  49. package/src/loop/steering.mjs +793 -0
  50. package/src/loop/timeout-policy.mjs +73 -0
  51. package/src/loop/tracker-first-loop-state.mjs +87 -0
  52. package/src/loop/tracker-pr-state.mjs +301 -0
  53. package/src/loop/worktree-guard.mjs +141 -0
  54. package/src/refinement/ac-dod-matrix.mjs +95 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Queue state machine — durable .pi/dev-loop-queue.json read/write/transition.
3
+ *
4
+ * Entry lifecycle:
5
+ * queued → running → waiting_review → gates_passing → merging → done
6
+ * any state → blocked | failed
7
+ */
8
+
9
+ import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ // ── Schema constants ────────────────────────────────────────────────
13
+
14
+ export const QUEUE_VERSION = 1;
15
+
16
+ export const ENTRY_STATUS = Object.freeze([
17
+ "queued",
18
+ "running",
19
+ "waiting_review",
20
+ "gates_passing",
21
+ "merging",
22
+ "done",
23
+ "blocked",
24
+ "failed",
25
+ ]);
26
+
27
+ export const VALID_TRANSITIONS = Object.freeze({
28
+ queued: ["running", "blocked"],
29
+ running: ["waiting_review", "blocked", "failed", "done"],
30
+ waiting_review: ["running", "gates_passing", "blocked", "failed", "done"],
31
+ gates_passing: ["merging", "blocked", "failed", "done"],
32
+ merging: ["done", "failed", "blocked"],
33
+ done: [],
34
+ blocked: ["queued", "running", "failed"],
35
+ failed: ["queued"],
36
+ });
37
+
38
+ export const RECOVERABLE_FAILURES = new Set([
39
+ "acceptance_report_parse_failure",
40
+ "round_cap_reached",
41
+ "timeout",
42
+ ]);
43
+
44
+ // ── Default queue shape ─────────────────────────────────────────────
45
+
46
+ function emptyQueue() {
47
+ return { version: QUEUE_VERSION, entries: [] };
48
+ }
49
+
50
+ // ── File I/O ─────────────────────────────────────────────────────────
51
+
52
+ export function queueFilePath(repoRoot) {
53
+ return path.join(repoRoot, ".pi", "dev-loop-queue.json");
54
+ }
55
+
56
+ export async function readQueue(repoRoot) {
57
+ const fp = queueFilePath(repoRoot);
58
+ try {
59
+ const raw = await readFile(fp, "utf8");
60
+ const parsed = JSON.parse(raw);
61
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
62
+ return emptyQueue();
63
+ }
64
+ return { version: parsed.version ?? QUEUE_VERSION, entries: parsed.entries };
65
+ } catch {
66
+ return emptyQueue();
67
+ }
68
+ }
69
+
70
+ export async function writeQueue(repoRoot, queue) {
71
+ const fp = queueFilePath(repoRoot);
72
+ await mkdir(path.dirname(fp), { recursive: true });
73
+ // Atomic write: write to temp file then rename
74
+ const tmp = fp + ".tmp." + Date.now();
75
+ await writeFile(tmp, JSON.stringify(queue, null, 2) + "\n", "utf8");
76
+ await rename(tmp, fp);
77
+ }
78
+
79
+ // ── Entry helpers ────────────────────────────────────────────────────
80
+
81
+ export function createEntry(target, kind, dependsOn = []) {
82
+ return {
83
+ target,
84
+ kind, // "issue" | "pr"
85
+ status: "queued",
86
+ dependsOn: Array.isArray(dependsOn) ? dependsOn : [],
87
+ pr: null,
88
+ runId: null,
89
+ retrospectiveWritten: false,
90
+ failureReason: null,
91
+ failureKind: null,
92
+ retryCount: 0,
93
+ };
94
+ }
95
+
96
+ export function snapshotEntry(entry) {
97
+ return { ...entry };
98
+ }
99
+
100
+ export function findEntry(queue, target) {
101
+ return queue.entries.find((e) => e.target === target);
102
+ }
103
+
104
+ export function findEntryIndex(queue, target) {
105
+ return queue.entries.findIndex((e) => e.target === target);
106
+ }
107
+
108
+ // ── State machine ────────────────────────────────────────────────────
109
+
110
+ export function isValidTransition(from, to) {
111
+ const allowed = VALID_TRANSITIONS[from];
112
+ if (!allowed) return false;
113
+ return allowed.includes(to);
114
+ }
115
+
116
+ export function transitionEntry(entry, to, metadata = {}) {
117
+ if (!isValidTransition(entry.status, to)) {
118
+ throw new Error(
119
+ `Invalid transition: ${entry.status} → ${to} for entry ${entry.target}`
120
+ );
121
+ }
122
+ entry.status = to;
123
+ if (metadata.pr != null) entry.pr = metadata.pr;
124
+ if (metadata.runId != null) entry.runId = metadata.runId;
125
+ if (metadata.retrospectiveWritten != null) entry.retrospectiveWritten = metadata.retrospectiveWritten;
126
+ if (metadata.failureReason != null) entry.failureReason = metadata.failureReason;
127
+ if (metadata.failureKind != null) entry.failureKind = metadata.failureKind;
128
+ return entry;
129
+ }
130
+
131
+ // ── Dependency resolution ────────────────────────────────────────────
132
+
133
+ export function entryDependenciesSatisfied(queue, entry) {
134
+ if (!entry.dependsOn || entry.dependsOn.length === 0) return true;
135
+ return entry.dependsOn.every((depTarget) => {
136
+ const dep = findEntry(queue, depTarget);
137
+ return dep && dep.status === "done";
138
+ });
139
+ }
140
+
141
+ // ── Queue ordering ───────────────────────────────────────────────────
142
+
143
+ export function topologicalOrder(entries) {
144
+ const visited = new Set();
145
+ const result = [];
146
+
147
+ function visit(entry, path = new Set()) {
148
+ if (visited.has(entry.target)) return;
149
+ if (path.has(entry.target)) {
150
+ throw new Error(`Circular dependency detected involving ${entry.target}`);
151
+ }
152
+ path.add(entry.target);
153
+ for (const depTarget of entry.dependsOn || []) {
154
+ const dep = entries.find((e) => e.target === depTarget);
155
+ if (dep) visit(dep, new Set(path));
156
+ }
157
+ path.delete(entry.target);
158
+ visited.add(entry.target);
159
+ result.push(entry);
160
+ }
161
+
162
+ for (const entry of entries) {
163
+ visit(entry);
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ function applyOrderHint(ordered, orderHint) {
170
+ if (orderHint.length === 0) return ordered;
171
+ const hinted = new Set(orderHint);
172
+ const hintIndex = new Map(orderHint.map((target, i) => [target, i]));
173
+ const inHint = [];
174
+ const rest = [];
175
+ for (const entry of ordered) {
176
+ if (hinted.has(entry.target)) {
177
+ inHint.push(entry);
178
+ } else {
179
+ rest.push(entry);
180
+ }
181
+ }
182
+ inHint.sort((a, b) => hintIndex.get(a.target) - hintIndex.get(b.target));
183
+ return [...inHint, ...rest];
184
+ }
185
+
186
+ export function nextReadyEntry(queue, maxRetries = 1, orderHint = []) {
187
+ const ordered = topologicalOrder(queue.entries);
188
+ const sorted = applyOrderHint(ordered, orderHint);
189
+ for (const entry of sorted) {
190
+ if (entry.status === "queued" && entryDependenciesSatisfied(queue, entry)) {
191
+ return entry;
192
+ }
193
+ if (
194
+ entry.status === "failed" &&
195
+ RECOVERABLE_FAILURES.has(entry.failureKind) &&
196
+ (entry.retryCount ?? 0) < maxRetries
197
+ ) {
198
+ return entry;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+
204
+ export function allDone(queue) {
205
+ return queue.entries.every((e) => e.status === "done" || e.status === "blocked");
206
+ }
207
+
208
+ export function pendingEntries(queue) {
209
+ return queue.entries.filter(
210
+ (e) => e.status !== "done" && e.status !== "blocked"
211
+ );
212
+ }
213
+
214
+ // ── Bug injection ────────────────────────────────────────────────────
215
+
216
+ export function appendBugIssue(queue, issueNumber, dependsOn = null) {
217
+ const entry = createEntry(issueNumber, "issue", dependsOn ? [dependsOn] : []);
218
+ entry.status = "queued";
219
+ queue.entries.push(entry);
220
+ return entry;
221
+ }
222
+
223
+ // ── Serialization helpers ────────────────────────────────────────────
224
+
225
+ export function serializeQueue(queue) {
226
+ return {
227
+ version: queue.version,
228
+ entries: queue.entries.map((e) => ({ ...e })),
229
+ };
230
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Post-run behavioral retrospective checkpoint contract.
3
+ *
4
+ * Defines the enforcement seam for the required post-run behavioral retrospective
5
+ * after qualifying async dev-loop completions in this repository.
6
+ *
7
+ * This module is intentionally pure and side-effect free. Callers are responsible
8
+ * for reading/writing the durable checkpoint artifact and passing the resolved
9
+ * checkpoint state to the enforcement gate.
10
+ *
11
+ * Relationship to formal dev mode:
12
+ * - Formal local dev mode is scoped to local implementation/self-improvement work.
13
+ * - The required post-run behavioral retrospective applies to qualifying async
14
+ * GitHub-first dev-loop completions, independent of whether that run was in
15
+ * formal local dev mode.
16
+ * - These are related but distinct requirements.
17
+ */
18
+
19
+ /**
20
+ * Stable state constants for the post-run behavioral retrospective checkpoint.
21
+ *
22
+ * These represent the state that a caller derives from the durable checkpoint
23
+ * artifact on disk, then passes to the enforcement gate.
24
+ *
25
+ * Mapping from durable artifact to checkpoint state:
26
+ * - No artifact file → NONE (no qualifying completion has occurred)
27
+ * - Artifact file with state "required" → MISSING (completion detected, retrospective pending)
28
+ * - Artifact file with state "complete" → COMPLETE (retrospective recorded)
29
+ * - Artifact file with state "skipped" → SKIPPED (explicitly skipped with reason)
30
+ */
31
+ export const RETROSPECTIVE_CHECKPOINT_STATE = Object.freeze({
32
+ /** No qualifying async dev-loop completion has occurred; no retrospective is required. */
33
+ NONE: "none",
34
+ /** The required retrospective has been completed and recorded. */
35
+ COMPLETE: "complete",
36
+ /** The required retrospective was explicitly skipped with a stated reason. */
37
+ SKIPPED: "skipped",
38
+ /** A qualifying async dev-loop completion was detected but no retrospective checkpoint exists. */
39
+ MISSING: "missing",
40
+ });
41
+
42
+ /**
43
+ * The set of internal dev-loop strategy gate names that represent qualifying
44
+ * GitHub-first async completions in this repository.
45
+ *
46
+ * A post-run behavioral retrospective is required before the next dev-loop
47
+ * start/resume when the previous run used one of these gates.
48
+ *
49
+ * Qualifying gates:
50
+ * - copilot_pr_followup: Copilot-owned PR follow-up (primary routed GitHub-first path)
51
+ * - issue_intake: Copilot-first issue intake (GitHub-first issue assignment path)
52
+ */
53
+ export const RETROSPECTIVE_QUALIFYING_GATES = Object.freeze([
54
+ "copilot_pr_followup",
55
+ "issue_intake",
56
+ ]);
57
+
58
+ /**
59
+ * Normalizes an external retrospective checkpoint-state input to one of the
60
+ * stable RETROSPECTIVE_CHECKPOINT_STATE values. Returns null when the value is
61
+ * absent or unrecognized.
62
+ *
63
+ * @param {unknown} value
64
+ * @returns {"none"|"complete"|"skipped"|"missing"|null}
65
+ */
66
+ export function normalizeRetrospectiveCheckpointState(value) {
67
+ if (value === undefined || value === null) {
68
+ return null;
69
+ }
70
+
71
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
72
+ return Object.values(RETROSPECTIVE_CHECKPOINT_STATE).includes(normalized) ? normalized : null;
73
+ }
74
+
75
+ /**
76
+ * Returns true if a routing result represents a qualifying GitHub-first async
77
+ * dev-loop completion that requires a post-run behavioral retrospective before
78
+ * the next start/resume.
79
+ *
80
+ * A qualifying completion is one that:
81
+ * - has a `selectedGate` in RETROSPECTIVE_QUALIFYING_GATES
82
+ * - with `routeKind === "route"` (inspect/status-only results do not qualify)
83
+ */
84
+ export function isQualifyingAsyncCompletion(routingResult) {
85
+ if (!routingResult || typeof routingResult !== "object") return false;
86
+ const { routeKind, selectedGate } = routingResult;
87
+ if (routeKind !== "route") {
88
+ return false;
89
+ }
90
+ if (typeof selectedGate !== "string") return false;
91
+ return RETROSPECTIVE_QUALIFYING_GATES.includes(selectedGate);
92
+ }
93
+
94
+ /**
95
+ * Enforcement gate for the required post-run behavioral retrospective.
96
+ *
97
+ * Evaluates whether a proposed dev-loop routing result should proceed or be
98
+ * blocked due to a missing retrospective checkpoint from the previous qualifying
99
+ * async completion.
100
+ *
101
+ * Pass-through cases (proposed routing is returned unchanged):
102
+ * - checkpoint state is NONE (no qualifying completion has happened; no requirement exists)
103
+ * - checkpoint state is COMPLETE (retrospective was recorded; requirement satisfied)
104
+ * - checkpoint state is SKIPPED (explicitly skipped with reason; requirement satisfied)
105
+ * - proposed routing is already a stop or needs_reconcile result
106
+ * - proposed routing is an inspect-only result
107
+ *
108
+ * Fail-closed case:
109
+ * - checkpoint state is MISSING: returns a needs_reconcile result that blocks start/resume
110
+ * - unrecognized checkpoint state: returns a needs_reconcile result
111
+ *
112
+ * @param {object} input
113
+ * @param {string} input.checkpointState - One of the RETROSPECTIVE_CHECKPOINT_STATE values
114
+ * @param {object} input.proposedRouting - The routing result from evaluatePublicDevLoopRouting
115
+ * @returns {object} The original or replacement routing result
116
+ */
117
+ export function evaluateRetrospectiveGate({ checkpointState, proposedRouting } = {}) {
118
+ if (!proposedRouting || typeof proposedRouting !== "object") {
119
+ return {
120
+ publicEntrypoint: "dev-loop",
121
+ routeKind: "needs_reconcile",
122
+ selectedGate: "fail_closed_reconcile",
123
+ selectedStrategy: null,
124
+ executionMode: "bounded_handoff",
125
+ waitSemantics: "default",
126
+ canonicalState: null,
127
+ issueAssignmentSeam: "not_applicable",
128
+ nextAction: "Reconcile the retrospective checkpoint state before routing.",
129
+ reason: "Missing or invalid proposed routing result for retrospective gate evaluation.",
130
+ };
131
+ }
132
+
133
+ // Already a terminal/inspect result — pass through regardless of checkpoint state.
134
+ if (
135
+ proposedRouting.routeKind === "stop" ||
136
+ proposedRouting.routeKind === "needs_reconcile" ||
137
+ proposedRouting.routeKind === "inspect"
138
+ ) {
139
+ return proposedRouting;
140
+ }
141
+
142
+ // No qualifying completion, or retrospective satisfied — pass through.
143
+ if (
144
+ checkpointState === RETROSPECTIVE_CHECKPOINT_STATE.NONE ||
145
+ checkpointState === RETROSPECTIVE_CHECKPOINT_STATE.COMPLETE ||
146
+ checkpointState === RETROSPECTIVE_CHECKPOINT_STATE.SKIPPED
147
+ ) {
148
+ return proposedRouting;
149
+ }
150
+
151
+ // Missing retrospective checkpoint — fail closed.
152
+ if (checkpointState === RETROSPECTIVE_CHECKPOINT_STATE.MISSING) {
153
+ return {
154
+ ...proposedRouting,
155
+ routeKind: "needs_reconcile",
156
+ selectedGate: "fail_closed_reconcile",
157
+ selectedStrategy: null,
158
+ waitSemantics: proposedRouting.waitSemantics ?? "default",
159
+ issueAssignmentSeam: proposedRouting.issueAssignmentSeam ?? "not_applicable",
160
+ nextAction:
161
+ "Complete or explicitly skip the required post-run behavioral retrospective before starting or resuming the next dev-loop run.",
162
+ reason:
163
+ "The previous qualifying async dev-loop completion is missing its required behavioral retrospective checkpoint.",
164
+ };
165
+ }
166
+
167
+ // Unrecognized checkpoint state — fail closed.
168
+ return {
169
+ ...proposedRouting,
170
+ routeKind: "needs_reconcile",
171
+ selectedGate: "fail_closed_reconcile",
172
+ selectedStrategy: null,
173
+ waitSemantics: proposedRouting.waitSemantics ?? "default",
174
+ issueAssignmentSeam: proposedRouting.issueAssignmentSeam ?? "not_applicable",
175
+ nextAction: "Reconcile the retrospective checkpoint state before routing.",
176
+ reason: `Unrecognized retrospective checkpoint state: "${String(checkpointState)}".`,
177
+ };
178
+ }