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
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
5
+ import { parsePositiveInteger, requireOptionValue, runChild } from "../_cli-primitives.mjs";
6
+ import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
+
8
+ export const FORBIDDEN_PROSE_PATTERNS = [
9
+ /Child of #/iu,
10
+ /Parent:\s*#/iu,
11
+ /Depends on:\s*#/iu,
12
+ /sub-issue of #/iu,
13
+ ];
14
+
15
+ export const DEFAULT_USAGE_SUFFIX = `
16
+ Output:
17
+ Default output is human-readable text.
18
+ Add --json for machine-readable JSON.`.trim();
19
+
20
+ export function normalizeIssueNumber(value, label, parseError) {
21
+ return parsePositiveInteger(value, label, parseError);
22
+ }
23
+
24
+ export function parseCheckerCliArgs(argv, usage, checkerName) {
25
+ const parseError = buildParseError(usage);
26
+ const args = [...argv];
27
+ const options = { help: false, input: undefined, json: false };
28
+ while (args.length > 0) {
29
+ const token = args.shift();
30
+ if (token === "--help" || token === "-h") {
31
+ options.help = true;
32
+ return options;
33
+ }
34
+ if (token === "--input") {
35
+ options.input = requireOptionValue(args, "--input", parseError, { flagPattern: /^-/u });
36
+ continue;
37
+ }
38
+ if (token === "--json") {
39
+ options.json = true;
40
+ continue;
41
+ }
42
+ throw parseError(`Unknown argument: ${token}`);
43
+ }
44
+ if (typeof options.input !== "string" || options.input.trim().length === 0) {
45
+ throw parseError(`${checkerName} requires --input <path>`);
46
+ }
47
+ return options;
48
+ }
49
+
50
+ export function normalizeTreePayload(payload) {
51
+ if (!payload || typeof payload !== "object") {
52
+ throw new Error("Refinement tree input must be a JSON object");
53
+ }
54
+ const rootIssueNumber = normalizeIssueNumber(
55
+ payload.rootIssueNumber ?? payload.root,
56
+ "root issue number",
57
+ (message) => new Error(message),
58
+ );
59
+ if (!Array.isArray(payload.issues) || payload.issues.length === 0) {
60
+ throw new Error("Refinement tree input requires a non-empty issues array");
61
+ }
62
+
63
+ const issues = [];
64
+ const byNumber = new Map();
65
+ for (const rawIssue of payload.issues) {
66
+ if (!rawIssue || typeof rawIssue !== "object") {
67
+ throw new Error("Each issue entry must be an object");
68
+ }
69
+ const number = normalizeIssueNumber(rawIssue.number, "issue number", (message) => new Error(message));
70
+ const title = typeof rawIssue.title === "string" ? rawIssue.title : "";
71
+ const body = typeof rawIssue.body === "string" ? rawIssue.body : "";
72
+ const state = typeof rawIssue.state === "string" ? rawIssue.state : "open";
73
+
74
+ let parentNumber = null;
75
+ if (rawIssue.parentNumber !== undefined && rawIssue.parentNumber !== null) {
76
+ parentNumber = normalizeIssueNumber(rawIssue.parentNumber, "parent issue number", (message) => new Error(message));
77
+ }
78
+
79
+ const children = Array.isArray(rawIssue.children)
80
+ ? rawIssue.children.map((child) => normalizeIssueNumber(child, "child issue number", (message) => new Error(message)))
81
+ : [];
82
+
83
+ if (byNumber.has(number)) {
84
+ throw new Error(`Duplicate issue number in tree input: ${number}`);
85
+ }
86
+ const issue = { number, title, body, state, parentNumber, children };
87
+ byNumber.set(number, issue);
88
+ issues.push(issue);
89
+ }
90
+
91
+ const edges = [];
92
+ for (const issue of issues) {
93
+ for (const child of issue.children) {
94
+ edges.push({ parent: issue.number, child });
95
+ }
96
+ }
97
+
98
+ return {
99
+ mode: payload.mode === "online" ? "online" : "offline",
100
+ repo: typeof payload.repo === "string" ? payload.repo : null,
101
+ rootIssueNumber,
102
+ issues,
103
+ byNumber,
104
+ edges,
105
+ };
106
+ }
107
+
108
+ export async function loadTreeFromInput(inputPath) {
109
+ const raw = await readFile(inputPath, "utf8");
110
+ return normalizeTreePayload(parseJsonText(raw));
111
+ }
112
+
113
+ async function ghApiJson(args, { ghCommand = "gh", env = process.env } = {}) {
114
+ const result = await runChild(ghCommand, ["api", ...args], env);
115
+ if (result.code !== 0) {
116
+ const detail = result.stderr.trim() || `exit code ${result.code}`;
117
+ throw new Error(`gh api command failed: ${detail}`);
118
+ }
119
+ return parseJsonText(result.stdout);
120
+ }
121
+
122
+ export async function loadTreeOnline({ issue, repo, cwd = process.cwd(), ghCommand = "gh", env = process.env }) {
123
+ const resolvedRepo = typeof repo === "string" && repo.trim().length > 0
124
+ ? repo.trim()
125
+ : detectRepoSlug(cwd);
126
+ if (!resolvedRepo) {
127
+ throw new Error("Unable to detect repository slug. Pass --repo <owner/name>.");
128
+ }
129
+ const parsed = parseRepoSlug(resolvedRepo, { errorMessage: "--repo must match <owner/name>" });
130
+ const { owner, name } = parsed;
131
+
132
+ const byNumber = new Map();
133
+ const edges = [];
134
+ const queuedNumbers = new Set([issue]);
135
+ const queue = [{ number: issue, parentNumber: null }];
136
+
137
+ while (queue.length > 0) {
138
+ const current = queue.shift();
139
+ if (!Number.isInteger(current.number) || current.number <= 0) {
140
+ continue;
141
+ }
142
+
143
+ const issuePayload = await ghApiJson([
144
+ `repos/${owner}/${name}/issues/${current.number}`,
145
+ ], { ghCommand, env });
146
+
147
+ const number = issuePayload?.number;
148
+ if (!Number.isInteger(number) || number <= 0) {
149
+ throw new Error(`Invalid issue payload for #${current.number}`);
150
+ }
151
+
152
+ const existing = byNumber.get(number);
153
+ if (!existing) {
154
+ byNumber.set(number, {
155
+ number,
156
+ title: typeof issuePayload.title === "string" ? issuePayload.title : "",
157
+ body: typeof issuePayload.body === "string" ? issuePayload.body : "",
158
+ state: typeof issuePayload.state === "string" ? issuePayload.state : "open",
159
+ parentNumber: current.parentNumber,
160
+ children: [],
161
+ });
162
+ } else if (existing.parentNumber === null && current.parentNumber !== null) {
163
+ existing.parentNumber = current.parentNumber;
164
+ }
165
+
166
+ const subIssuesPayload = await ghApiJson([
167
+ `repos/${owner}/${name}/issues/${number}/sub_issues`,
168
+ ], { ghCommand, env });
169
+
170
+ const currentIssue = byNumber.get(number);
171
+ const children = [];
172
+ if (Array.isArray(subIssuesPayload)) {
173
+ for (const entry of subIssuesPayload) {
174
+ if (!entry || typeof entry !== "object") {
175
+ continue;
176
+ }
177
+ const childNumber = entry.number;
178
+ if (!Number.isInteger(childNumber) || childNumber <= 0) {
179
+ continue;
180
+ }
181
+ children.push(childNumber);
182
+ edges.push({ parent: number, child: childNumber });
183
+ if (!byNumber.has(childNumber) && !queuedNumbers.has(childNumber)) {
184
+ queuedNumbers.add(childNumber);
185
+ queue.push({ number: childNumber, parentNumber: number });
186
+ }
187
+ }
188
+ } else {
189
+ throw new Error(`Invalid sub-issues payload for #${number}: expected array`);
190
+ }
191
+
192
+ currentIssue.children = [...new Set(children)];
193
+ }
194
+
195
+ return {
196
+ mode: "online",
197
+ repo: resolvedRepo,
198
+ rootIssueNumber: issue,
199
+ issues: [...byNumber.values()],
200
+ byNumber,
201
+ edges,
202
+ };
203
+ }
204
+
205
+ export function extractSection(body, headingText) {
206
+ if (typeof body !== "string" || body.length === 0) {
207
+ return null;
208
+ }
209
+ const escapedHeading = headingText.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
210
+ const headingPattern = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "imu");
211
+ const match = headingPattern.exec(body);
212
+ if (!match || match.index === undefined) {
213
+ return null;
214
+ }
215
+ const start = match.index + match[0].length;
216
+ const remaining = body.slice(start);
217
+ const nextHeadingMatch = /^##\s+/imu.exec(remaining);
218
+ const end = nextHeadingMatch && nextHeadingMatch.index !== undefined
219
+ ? start + nextHeadingMatch.index
220
+ : body.length;
221
+ return body.slice(start, end).trim();
222
+ }
223
+
224
+ export function normalizeScopeToken(value) {
225
+ return String(value ?? "")
226
+ .trim()
227
+ .toLowerCase()
228
+ .replace(/[`*_~]/gu, "")
229
+ .replace(/^[:\-\s]+|[:\-\s]+$/gu, "")
230
+ .replace(/\s+/gu, " ");
231
+ }
232
+
233
+ export function writeCheckerOutput(result, { stdout = process.stdout, json = false }) {
234
+ if (json) {
235
+ stdout.write(`${JSON.stringify(result)}\n`);
236
+ return;
237
+ }
238
+
239
+ const status = result.ok ? "PASS" : "FAIL";
240
+ const lines = [`${result.checker}: ${status}`];
241
+ if (result.errors.length === 0) {
242
+ lines.push(" - No problems found.");
243
+ } else {
244
+ for (const error of result.errors) {
245
+ const issuePart = Number.isInteger(error.issue) ? ` (#${error.issue})` : "";
246
+ lines.push(` - [${error.code}]${issuePart} ${error.message}`);
247
+ }
248
+ }
249
+ stdout.write(`${lines.join("\n")}\n`);
250
+ }
251
+
252
+
253
+ export function handleCliError(error) {
254
+ process.stderr.write(`${formatCliError(error)}\n`);
255
+ process.exitCode = 1;
256
+ }
257
+ // Re-exported for checker scripts
258
+ export { isDirectCliRun };
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { formatCliError } from "../_core-helpers.mjs";
3
+ import {
4
+ DEFAULT_USAGE_SUFFIX,
5
+ FORBIDDEN_PROSE_PATTERNS,
6
+ loadTreeFromInput,
7
+ parseCheckerCliArgs,
8
+ writeCheckerOutput,
9
+ isDirectCliRun,
10
+ } from "./_refine-helpers.mjs";
11
+
12
+ const USAGE = `Usage:
13
+ prose-linkage-detector.mjs --input <path> [--json]
14
+ Fail when issue bodies use prose linkage (` + "`Child of #`, `Parent: #`, `Depends on: #`, `sub-issue of #`" + `)
15
+ instead of GitHub sub-issue API links.${"\n"}${DEFAULT_USAGE_SUFFIX}`;
16
+
17
+ export function runProseLinkageDetector(tree) {
18
+ const errors = [];
19
+ const parentByChild = new Map();
20
+
21
+ for (const edge of tree.edges) {
22
+ if (!parentByChild.has(edge.child)) {
23
+ parentByChild.set(edge.child, new Set());
24
+ }
25
+ parentByChild.get(edge.child).add(edge.parent);
26
+ }
27
+
28
+ for (const issue of tree.issues) {
29
+ for (const pattern of FORBIDDEN_PROSE_PATTERNS) {
30
+ if (pattern.test(issue.body)) {
31
+ errors.push({
32
+ code: "forbidden_prose_linkage",
33
+ issue: issue.number,
34
+ message: `Issue body contains forbidden prose linkage pattern: ${pattern.source}`,
35
+ });
36
+ }
37
+ }
38
+
39
+ for (const child of issue.children) {
40
+ const childIssue = tree.byNumber.get(child);
41
+ if (!childIssue) {
42
+ errors.push({
43
+ code: "missing_child_issue",
44
+ issue: issue.number,
45
+ message: `Sub-issue link references #${child}, but that issue is missing from the tree payload.`,
46
+ });
47
+ continue;
48
+ }
49
+ const parentSet = parentByChild.get(child) ?? new Set();
50
+ if (!parentSet.has(issue.number)) {
51
+ errors.push({
52
+ code: "missing_sub_issue_link",
53
+ issue: issue.number,
54
+ message: `Expected API sub-issue link #${issue.number} -> #${child} is missing.`,
55
+ });
56
+ }
57
+
58
+ if (Number.isInteger(childIssue.parentNumber) && childIssue.parentNumber !== issue.number) {
59
+ errors.push({
60
+ code: "parent_mismatch",
61
+ issue: child,
62
+ message: `Child issue #${child} declares parent #${childIssue.parentNumber}, not #${issue.number}.`,
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ return {
69
+ checker: "prose-linkage-detector",
70
+ ok: errors.length === 0,
71
+ errors,
72
+ };
73
+ }
74
+
75
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
76
+ const options = parseCheckerCliArgs(argv, USAGE, "prose-linkage-detector");
77
+ if (options.help) {
78
+ stdout.write(`${USAGE}\n`);
79
+ return { ok: true, help: true };
80
+ }
81
+ const tree = await loadTreeFromInput(options.input);
82
+ const result = runProseLinkageDetector(tree);
83
+ writeCheckerOutput(result, { stdout, json: options.json });
84
+ return result;
85
+ }
86
+
87
+ if (isDirectCliRun(import.meta.url)) {
88
+ runCli().catch((error) => {
89
+ process.stderr.write(`${formatCliError(error)}\n`);
90
+ process.exitCode = 1;
91
+ });
92
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { formatCliError } from "../_core-helpers.mjs";
3
+ import {
4
+ DEFAULT_USAGE_SUFFIX,
5
+ extractSection,
6
+ loadTreeFromInput,
7
+ parseCheckerCliArgs,
8
+ writeCheckerOutput,
9
+ isDirectCliRun,
10
+ } from "./_refine-helpers.mjs";
11
+
12
+ const USAGE = `Usage:
13
+ refinement-completeness-checker.mjs --input <path> [--json]
14
+ Validate required refinement sections: Acceptance criteria, Definition of done, Non-goals, and AC / DoD matrix.${"\n"}${DEFAULT_USAGE_SUFFIX}`;
15
+
16
+ function hasCheckbox(sectionText) {
17
+ if (typeof sectionText !== "string") {
18
+ return false;
19
+ }
20
+ return /^\s*-\s*\[\s\]\s+/imu.test(sectionText);
21
+ }
22
+
23
+ function hasMatrixTable(sectionText) {
24
+ if (typeof sectionText !== "string") {
25
+ return false;
26
+ }
27
+ const rows = sectionText
28
+ .split(/\r?\n/gu)
29
+ .map((line) => line.trim())
30
+ .filter((line) => line.startsWith("|"));
31
+ return rows.length >= 2;
32
+ }
33
+
34
+ export function runRefinementCompletenessChecker(tree) {
35
+ const errors = [];
36
+
37
+ for (const issue of tree.issues) {
38
+ const acceptanceCriteria = extractSection(issue.body, "Acceptance criteria");
39
+ const definitionOfDone = extractSection(issue.body, "Definition of done");
40
+ const nonGoals = extractSection(issue.body, "Non-goals");
41
+ const acDodMatrix = extractSection(issue.body, "AC / DoD matrix");
42
+
43
+ if (!acceptanceCriteria) {
44
+ errors.push({ code: "missing_acceptance_criteria", issue: issue.number, message: "Missing ## Acceptance criteria section." });
45
+ } else if (!hasCheckbox(acceptanceCriteria)) {
46
+ errors.push({ code: "missing_acceptance_checkbox", issue: issue.number, message: "Acceptance criteria section must include at least one '- [ ]' checkbox." });
47
+ }
48
+
49
+ if (!definitionOfDone) {
50
+ errors.push({ code: "missing_definition_of_done", issue: issue.number, message: "Missing ## Definition of done section." });
51
+ }
52
+
53
+ if (!nonGoals) {
54
+ errors.push({ code: "missing_non_goals", issue: issue.number, message: "Missing ## Non-goals section." });
55
+ }
56
+
57
+ if (!acDodMatrix) {
58
+ errors.push({ code: "missing_ac_dod_matrix", issue: issue.number, message: "Missing ## AC / DoD matrix section." });
59
+ } else if (!hasMatrixTable(acDodMatrix)) {
60
+ errors.push({ code: "invalid_ac_dod_matrix", issue: issue.number, message: "AC / DoD matrix section must contain a markdown table." });
61
+ }
62
+ }
63
+
64
+ return {
65
+ checker: "refinement-completeness-checker",
66
+ ok: errors.length === 0,
67
+ errors,
68
+ };
69
+ }
70
+
71
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
72
+ const options = parseCheckerCliArgs(argv, USAGE, "refinement-completeness-checker");
73
+ if (options.help) {
74
+ stdout.write(`${USAGE}\n`);
75
+ return { ok: true, help: true };
76
+ }
77
+ const tree = await loadTreeFromInput(options.input);
78
+ const result = runRefinementCompletenessChecker(tree);
79
+ writeCheckerOutput(result, { stdout, json: options.json });
80
+ return result;
81
+ }
82
+
83
+ if (isDirectCliRun(import.meta.url)) {
84
+ runCli().catch((error) => {
85
+ process.stderr.write(`${formatCliError(error)}\n`);
86
+ process.exitCode = 1;
87
+ });
88
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import { formatCliError } from "../_core-helpers.mjs";
3
+ import {
4
+ DEFAULT_USAGE_SUFFIX,
5
+ extractSection,
6
+ loadTreeFromInput,
7
+ normalizeScopeToken,
8
+ parseCheckerCliArgs,
9
+ writeCheckerOutput,
10
+ isDirectCliRun,
11
+ } from "./_refine-helpers.mjs";
12
+
13
+ const USAGE = `Usage:
14
+ scope-boundary-cross-checker.mjs --input <path> [--json]
15
+ Cross-check sibling scopes and non-goal handoffs to detect scope gaps and duplicate ownership.${"\n"}${DEFAULT_USAGE_SUFFIX}`;
16
+
17
+ function extractOwnershipClaims(issue) {
18
+ const tokens = new Set();
19
+ const scopeSection = extractSection(issue.body, "Scope") ?? "";
20
+ const ownershipSection = extractSection(issue.body, "Ownership") ?? "";
21
+ const searchText = [scopeSection, ownershipSection, issue.body].filter(Boolean).join("\n");
22
+
23
+ const ownsPattern = /\bowns?\s+([^\n.;]+)/giu;
24
+ for (const match of searchText.matchAll(ownsPattern)) {
25
+ const token = normalizeScopeToken(match[1]);
26
+ if (token.length > 0) {
27
+ tokens.add(token);
28
+ }
29
+ }
30
+
31
+ for (const line of scopeSection.split(/\r?\n/gu)) {
32
+ const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
33
+ if (!bullet) continue;
34
+ const rawToken = bullet[1].replace(/^\s*owns?\s+/iu, '');
35
+ const token = normalizeScopeToken(rawToken);
36
+ if (token.length > 0) {
37
+ tokens.add(token);
38
+ }
39
+ }
40
+
41
+ return [...tokens];
42
+ }
43
+
44
+ function extractNonGoalDelegations(issue) {
45
+ const nonGoals = extractSection(issue.body, "Non-goals") ?? "";
46
+ const delegations = [];
47
+ for (const line of nonGoals.split(/\r?\n/gu)) {
48
+ const match = /not\s+(.+?)\s*(?:->|→)\s*#?(\d+)\b/iu.exec(line);
49
+ if (!match) {
50
+ continue;
51
+ }
52
+ const token = normalizeScopeToken(match[1]);
53
+ const target = Number(match[2]);
54
+ if (token.length > 0 && Number.isInteger(target) && target > 0) {
55
+ delegations.push({ token, target });
56
+ }
57
+ }
58
+ return delegations;
59
+ }
60
+
61
+ function buildSiblingGroups(tree) {
62
+ const childrenByParent = new Map();
63
+ for (const edge of tree.edges) {
64
+ if (!childrenByParent.has(edge.parent)) {
65
+ childrenByParent.set(edge.parent, new Set());
66
+ }
67
+ childrenByParent.get(edge.parent).add(edge.child);
68
+ }
69
+ return childrenByParent;
70
+ }
71
+
72
+ export function runScopeBoundaryCrossChecker(tree) {
73
+ const errors = [];
74
+ const ownershipByIssue = new Map();
75
+ const delegationsByIssue = new Map();
76
+
77
+ for (const issue of tree.issues) {
78
+ ownershipByIssue.set(issue.number, extractOwnershipClaims(issue));
79
+ delegationsByIssue.set(issue.number, extractNonGoalDelegations(issue));
80
+ }
81
+
82
+ for (const [parent, childrenSet] of buildSiblingGroups(tree)) {
83
+ const siblings = [...childrenSet].map((number) => tree.byNumber.get(number)).filter(Boolean);
84
+ if (siblings.length < 2) {
85
+ continue;
86
+ }
87
+
88
+ const ownersByToken = new Map();
89
+ for (const sibling of siblings) {
90
+ for (const token of ownershipByIssue.get(sibling.number) ?? []) {
91
+ if (!ownersByToken.has(token)) {
92
+ ownersByToken.set(token, new Set());
93
+ }
94
+ ownersByToken.get(token).add(sibling.number);
95
+ }
96
+ }
97
+
98
+ for (const sibling of siblings) {
99
+ const delegations = delegationsByIssue.get(sibling.number) ?? [];
100
+ for (const delegation of delegations) {
101
+ const target = tree.byNumber.get(delegation.target);
102
+ if (!target || !childrenSet.has(target.number)) {
103
+ continue;
104
+ }
105
+ const targetClaims = new Set(ownershipByIssue.get(target.number) ?? []);
106
+ if (!targetClaims.has(delegation.token)) {
107
+ errors.push({
108
+ code: "mutual_exclusion_gap",
109
+ issue: sibling.number,
110
+ message: `Issue #${sibling.number} delegates '${delegation.token}' to #${target.number}, but target does not claim ownership.`,
111
+ parent,
112
+ });
113
+ }
114
+
115
+ const ownerSet = ownersByToken.get(delegation.token);
116
+ if (!ownerSet || ownerSet.size === 0) {
117
+ errors.push({
118
+ code: "unowned_scope_gap",
119
+ issue: sibling.number,
120
+ message: `No sibling under parent #${parent} claims delegated scope '${delegation.token}'.`,
121
+ parent,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ for (const [token, owners] of ownersByToken.entries()) {
128
+ if (owners.size > 1) {
129
+ errors.push({
130
+ code: "duplicate_ownership",
131
+ issue: parent,
132
+ message: `Siblings under #${parent} claim duplicate ownership for '${token}': ${[...owners].map((n) => `#${n}`).join(", ")}.`,
133
+ parent,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ return {
140
+ checker: "scope-boundary-cross-checker",
141
+ ok: errors.length === 0,
142
+ errors,
143
+ };
144
+ }
145
+
146
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
147
+ const options = parseCheckerCliArgs(argv, USAGE, "scope-boundary-cross-checker");
148
+ if (options.help) {
149
+ stdout.write(`${USAGE}\n`);
150
+ return { ok: true, help: true };
151
+ }
152
+ const tree = await loadTreeFromInput(options.input);
153
+ const result = runScopeBoundaryCrossChecker(tree);
154
+ writeCheckerOutput(result, { stdout, json: options.json });
155
+ return result;
156
+ }
157
+
158
+ if (isDirectCliRun(import.meta.url)) {
159
+ runCli().catch((error) => {
160
+ process.stderr.write(`${formatCliError(error)}\n`);
161
+ process.exitCode = 1;
162
+ });
163
+ }