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,211 @@
1
+ #!/usr/bin/env node
2
+ import { formatCliError } from "../_core-helpers.mjs";
3
+ import {
4
+ DEFAULT_USAGE_SUFFIX,
5
+ loadTreeFromInput,
6
+ parseCheckerCliArgs,
7
+ writeCheckerOutput,
8
+ isDirectCliRun,
9
+ } from "./_refine-helpers.mjs";
10
+
11
+ const MAX_DEPTH = 3;
12
+
13
+ const USAGE = `Usage:
14
+ tree-integrity-validator.mjs --input <path> [--json]
15
+ Validate sub-issue tree integrity: parent links, orphaned issues, cycles, and depth <= ${MAX_DEPTH}.${"\n"}${DEFAULT_USAGE_SUFFIX}`;
16
+
17
+ function detectCycles(tree, startIssue) {
18
+ const cycles = [];
19
+ const visiting = new Set();
20
+ const visited = new Set();
21
+
22
+ function dfs(nodeNumber, path = []) {
23
+ if (visiting.has(nodeNumber)) {
24
+ const cycleStart = path.indexOf(nodeNumber);
25
+ const cyclePath = cycleStart >= 0 ? path.slice(cycleStart).concat(nodeNumber) : [...path, nodeNumber];
26
+ cycles.push(cyclePath);
27
+ return;
28
+ }
29
+ if (visited.has(nodeNumber)) {
30
+ return;
31
+ }
32
+
33
+ visiting.add(nodeNumber);
34
+ const issue = tree.byNumber.get(nodeNumber);
35
+ if (issue) {
36
+ for (const child of issue.children) {
37
+ dfs(child, [...path, nodeNumber]);
38
+ }
39
+ }
40
+ visiting.delete(nodeNumber);
41
+ visited.add(nodeNumber);
42
+ }
43
+
44
+ dfs(startIssue);
45
+ return cycles;
46
+ }
47
+
48
+ function collectReachableIssues(tree) {
49
+ const reachable = new Set();
50
+ const queue = [tree.rootIssueNumber];
51
+ while (queue.length > 0) {
52
+ const current = queue.shift();
53
+ if (reachable.has(current)) {
54
+ continue;
55
+ }
56
+ reachable.add(current);
57
+ const issue = tree.byNumber.get(current);
58
+ if (!issue) {
59
+ continue;
60
+ }
61
+ for (const child of issue.children) {
62
+ queue.push(child);
63
+ }
64
+ }
65
+ return reachable;
66
+ }
67
+
68
+ function detectDepthViolations(tree) {
69
+ const violations = [];
70
+ const queue = [{ number: tree.rootIssueNumber, depth: 1 }];
71
+ const seen = new Map();
72
+ while (queue.length > 0) {
73
+ const current = queue.shift();
74
+ const existingDepth = seen.get(current.number);
75
+ if (existingDepth !== undefined && existingDepth <= current.depth) {
76
+ continue;
77
+ }
78
+ seen.set(current.number, current.depth);
79
+
80
+ if (current.depth > MAX_DEPTH) {
81
+ violations.push({ issue: current.number, depth: current.depth });
82
+ }
83
+
84
+ const issue = tree.byNumber.get(current.number);
85
+ if (!issue) {
86
+ continue;
87
+ }
88
+ for (const child of issue.children) {
89
+ queue.push({ number: child, depth: current.depth + 1 });
90
+ }
91
+ }
92
+ return violations;
93
+ }
94
+
95
+ export function runTreeIntegrityValidator(tree) {
96
+ const errors = [];
97
+ const parentsByChild = new Map();
98
+
99
+ for (const edge of tree.edges) {
100
+ if (!parentsByChild.has(edge.child)) {
101
+ parentsByChild.set(edge.child, new Set());
102
+ }
103
+ parentsByChild.get(edge.child).add(edge.parent);
104
+ }
105
+
106
+ if (!tree.byNumber.has(tree.rootIssueNumber)) {
107
+ errors.push({
108
+ code: "missing_root_issue",
109
+ issue: tree.rootIssueNumber,
110
+ message: `Root issue #${tree.rootIssueNumber} is missing from the tree payload.`,
111
+ });
112
+ return {
113
+ checker: "tree-integrity-validator",
114
+ ok: false,
115
+ errors,
116
+ };
117
+ }
118
+
119
+ for (const issue of tree.issues) {
120
+ if (issue.number === tree.rootIssueNumber) {
121
+ continue;
122
+ }
123
+ const parentSet = parentsByChild.get(issue.number) ?? new Set();
124
+
125
+ if (issue.parentNumber !== null && !tree.byNumber.has(issue.parentNumber)) {
126
+ errors.push({
127
+ code: "orphaned_issue",
128
+ issue: issue.number,
129
+ message: `Issue #${issue.number} references missing parent #${issue.parentNumber}.`,
130
+ });
131
+ }
132
+
133
+ if (issue.parentNumber !== null && !parentSet.has(issue.parentNumber)) {
134
+ errors.push({
135
+ code: "child_missing_parent_link",
136
+ issue: issue.number,
137
+ message: `Issue #${issue.number} declares parent #${issue.parentNumber}, but parent does not link to it as a child.`,
138
+ });
139
+ }
140
+
141
+ if (parentSet.size === 0) {
142
+ errors.push({
143
+ code: "missing_parent",
144
+ issue: issue.number,
145
+ message: `Issue #${issue.number} has no parent link in the tree graph.`,
146
+ });
147
+ }
148
+
149
+ if (parentSet.size > 1) {
150
+ errors.push({
151
+ code: "multiple_parents",
152
+ issue: issue.number,
153
+ message: `Issue #${issue.number} is linked by multiple parents: ${[...parentSet].map((n) => `#${n}`).join(", ")}.`,
154
+ });
155
+ }
156
+ }
157
+
158
+ const reachable = collectReachableIssues(tree);
159
+ for (const issue of tree.issues) {
160
+ if (!reachable.has(issue.number)) {
161
+ errors.push({
162
+ code: "orphaned_issue",
163
+ issue: issue.number,
164
+ message: `Issue #${issue.number} is not reachable from root #${tree.rootIssueNumber}.`,
165
+ });
166
+ }
167
+ }
168
+
169
+ const cycles = detectCycles(tree, tree.rootIssueNumber);
170
+ for (const cycle of cycles) {
171
+ errors.push({
172
+ code: "cycle_detected",
173
+ issue: cycle[0],
174
+ message: `Cycle detected in sub-issue graph: ${cycle.map((n) => `#${n}`).join(" -> ")}.`,
175
+ });
176
+ }
177
+
178
+ const depthViolations = detectDepthViolations(tree);
179
+ for (const violation of depthViolations) {
180
+ errors.push({
181
+ code: "depth_limit_exceeded",
182
+ issue: violation.issue,
183
+ message: `Issue #${violation.issue} exceeds max depth ${MAX_DEPTH} (depth=${violation.depth}).`,
184
+ });
185
+ }
186
+
187
+ return {
188
+ checker: "tree-integrity-validator",
189
+ ok: errors.length === 0,
190
+ errors,
191
+ };
192
+ }
193
+
194
+ export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
195
+ const options = parseCheckerCliArgs(argv, USAGE, "tree-integrity-validator");
196
+ if (options.help) {
197
+ stdout.write(`${USAGE}\n`);
198
+ return { ok: true, help: true };
199
+ }
200
+ const tree = await loadTreeFromInput(options.input);
201
+ const result = runTreeIntegrityValidator(tree);
202
+ writeCheckerOutput(result, { stdout, json: options.json });
203
+ return result;
204
+ }
205
+
206
+ if (isDirectCliRun(import.meta.url)) {
207
+ runCli().catch((error) => {
208
+ process.stderr.write(`${formatCliError(error)}\n`);
209
+ process.exitCode = 1;
210
+ });
211
+ }
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { buildParseError, formatCliError } from "../_core-helpers.mjs";
3
+ import { parsePositiveInteger, requireOptionValue } from "../_cli-primitives.mjs";
4
+ import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
+
6
+ import { isDirectCliRun, loadTreeFromInput, loadTreeOnline } from "./_refine-helpers.mjs";
7
+ import { runProseLinkageDetector } from "./prose-linkage-detector.mjs";
8
+ import { runScopeBoundaryCrossChecker } from "./scope-boundary-cross-checker.mjs";
9
+ import { runRefinementCompletenessChecker } from "./refinement-completeness-checker.mjs";
10
+ import { runTreeIntegrityValidator } from "./tree-integrity-validator.mjs";
11
+
12
+ const USAGE = `Usage:
13
+ dev-loops refine verify --issue <number> [--repo <owner/name>] [--json]
14
+ dev-loops refine verify --input <path> [--json]
15
+ Run epic-tree refinement verification with four checkers:
16
+ 1) prose-linkage-detector
17
+ 2) scope-boundary-cross-checker
18
+ 3) refinement-completeness-checker
19
+ 4) tree-integrity-validator
20
+
21
+ Required (exactly one mode):
22
+ --issue <number> Online mode: fetch tree via GitHub sub-issues API (use --repo or git remote)
23
+ --input <path> Offline mode: validate local tree JSON snapshot
24
+
25
+ Optional:
26
+ --repo <owner/name> Repository slug for online mode
27
+ --json Machine-readable JSON output (default: human-readable summary)
28
+ --help Show this help`.trim();
29
+
30
+ const parseError = buildParseError(USAGE);
31
+
32
+ export function parseRefineVerifyCliArgs(argv) {
33
+ const args = [...argv];
34
+ const options = {
35
+ help: false,
36
+ issue: undefined,
37
+ repo: undefined,
38
+ input: undefined,
39
+ json: false,
40
+ };
41
+
42
+ while (args.length > 0) {
43
+ const token = args.shift();
44
+ if (token === "--help" || token === "-h") {
45
+ options.help = true;
46
+ return options;
47
+ }
48
+ if (token === "--issue") {
49
+ options.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "Issue number", parseError);
50
+ continue;
51
+ }
52
+ if (token === "--repo") {
53
+ options.repo = requireOptionValue(args, "--repo", parseError, { flagPattern: /^-/u });
54
+ continue;
55
+ }
56
+ if (token === "--input") {
57
+ options.input = requireOptionValue(args, "--input", parseError, { flagPattern: /^-/u });
58
+ continue;
59
+ }
60
+ if (token === "--json") {
61
+ options.json = true;
62
+ continue;
63
+ }
64
+ throw parseError(`Unknown argument: ${token}`);
65
+ }
66
+
67
+ const hasIssueMode = options.issue !== undefined;
68
+ const hasInputMode = typeof options.input === "string";
69
+ if (hasIssueMode === hasInputMode) {
70
+ throw parseError("Specify exactly one of --issue <number> or --input <path>");
71
+ }
72
+
73
+ if (options.repo !== undefined && !hasIssueMode) {
74
+ throw parseError("--repo is only valid with --issue mode");
75
+ }
76
+
77
+ if (typeof options.repo === "string") {
78
+ try { parseRepoSlug(options.repo, { errorMessage: "--repo must match <owner/name>" }); } catch (err) { throw parseError(err.message); }
79
+ }
80
+
81
+ return options;
82
+ }
83
+
84
+ function runAllCheckers(tree) {
85
+ const checks = [
86
+ runProseLinkageDetector(tree),
87
+ runScopeBoundaryCrossChecker(tree),
88
+ runRefinementCompletenessChecker(tree),
89
+ runTreeIntegrityValidator(tree),
90
+ ];
91
+
92
+ const errors = checks.flatMap((check) => check.errors.map((error) => ({ checker: check.checker, ...error })));
93
+
94
+ return {
95
+ ok: checks.every((check) => check.ok),
96
+ checks,
97
+ errors,
98
+ };
99
+ }
100
+
101
+ function writeHumanOutput(result, tree, { stdout = process.stdout }) {
102
+ const lines = [
103
+ `refine verify: ${result.ok ? "PASS" : "FAIL"}`,
104
+ `mode: ${tree.mode}`,
105
+ `root issue: #${tree.rootIssueNumber}`,
106
+ ];
107
+ if (tree.repo) {
108
+ lines.push(`repo: ${tree.repo}`);
109
+ }
110
+ for (const check of result.checks) {
111
+ lines.push(`${check.checker}: ${check.ok ? "PASS" : "FAIL"}`);
112
+ for (const error of check.errors) {
113
+ const issuePart = Number.isInteger(error.issue) ? ` (#${error.issue})` : "";
114
+ lines.push(` - [${error.code}]${issuePart} ${error.message}`);
115
+ }
116
+ }
117
+ if (result.errors.length === 0) {
118
+ lines.push("No checker errors.");
119
+ }
120
+ stdout.write(`${lines.join("\n")}\n`);
121
+ }
122
+
123
+ export async function runCli(
124
+ argv = process.argv.slice(2),
125
+ { stdout = process.stdout, cwd = process.cwd(), ghCommand = "gh", env = process.env } = {},
126
+ ) {
127
+ const options = parseRefineVerifyCliArgs(argv);
128
+ if (options.help) {
129
+ stdout.write(`${USAGE}\n`);
130
+ return { ok: true, help: true };
131
+ }
132
+
133
+ let resolvedRepo = options.repo;
134
+ if (options.issue !== undefined && typeof resolvedRepo !== "string") {
135
+ try {
136
+ resolvedRepo = detectRepoSlug(cwd);
137
+ if (!resolvedRepo) {
138
+ throw parseError("Unable to detect repository slug. Pass --repo <owner/name>.");
139
+ }
140
+ } catch (err) {
141
+ throw parseError(err.message);
142
+ }
143
+ }
144
+
145
+ const tree = options.input
146
+ ? await loadTreeFromInput(options.input)
147
+ : await loadTreeOnline({ issue: options.issue, repo: resolvedRepo, cwd, ghCommand, env });
148
+
149
+
150
+ const result = runAllCheckers(tree);
151
+
152
+ const payload = {
153
+ ok: result.ok,
154
+ mode: tree.mode,
155
+ repo: tree.repo,
156
+ rootIssue: tree.rootIssueNumber,
157
+ checkers: result.checks,
158
+ errors: result.errors,
159
+ };
160
+
161
+ if (options.json) {
162
+ stdout.write(`${JSON.stringify(payload)}\n`);
163
+ } else {
164
+ writeHumanOutput(result, tree, { stdout });
165
+ }
166
+
167
+ if (!result.ok) {
168
+ process.exitCode = 1;
169
+ }
170
+ return payload;
171
+ }
172
+
173
+ if (isDirectCliRun(import.meta.url)) {
174
+ runCli().catch((error) => {
175
+ process.stderr.write(`${formatCliError(error)}\n`);
176
+ process.exitCode = 1;
177
+ });
178
+ }
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // Pinned-source fallback repo-wiki wrapper. Clones mfittko/repo-wiki at a fixed
3
+ // commit, installs/builds it locally, and proxies the requested command against
4
+ // this repo.
5
+ //
6
+ // This helper is retained for environments that prefer a pinned source checkout
7
+ // over the published npm install path (deterministic source pin, controlled
8
+ // GitHub-only network access, offline reproduction after the initial clone).
9
+ // It still requires git access to https://github.com/mfittko/repo-wiki.git for
10
+ // the initial clone/fetch step.
11
+ //
12
+ // The primary repo-wiki entrypoint is `scripts/repo-wiki.mjs` (npm-installed).
13
+ import { spawnSync } from "node:child_process";
14
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
15
+ import path from "node:path";
16
+ import process from "node:process";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
20
+
21
+ export const REPO_WIKI_GIT_URL = "https://github.com/mfittko/repo-wiki.git";
22
+ export const REPO_WIKI_REF = "d7e772e3d702a75896a6f4eec574a4e4e5bfa6dd";
23
+ export const REPO_WIKI_MIN_NODE_MAJOR = 24;
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
27
+
28
+ export function parseCliArgs(argv) {
29
+ const args = Array.isArray(argv) ? [...argv] : [];
30
+ if (args.length === 0) {
31
+ return { prepareOnly: false, passthroughArgs: ["--help"] };
32
+ }
33
+ if (args[0] === "prepare") {
34
+ return { prepareOnly: true, passthroughArgs: [] };
35
+ }
36
+ return { prepareOnly: false, passthroughArgs: args };
37
+ }
38
+
39
+ export function resolveRepoWikiPaths(projectRoot = PROJECT_ROOT, ref = REPO_WIKI_REF) {
40
+ const baseDir = path.join(projectRoot, ".tmp", "repo-wiki", ref);
41
+ const sourceDir = path.join(baseDir, "source");
42
+ const cliPath = path.join(sourceDir, "dist", "bin", "repo-wiki.js");
43
+ const buildStampPath = path.join(baseDir, "build-stamp.json");
44
+ return { projectRoot, baseDir, sourceDir, cliPath, buildStampPath };
45
+ }
46
+
47
+ export function assertSupportedNodeVersion(version = process.versions.node) {
48
+ const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
49
+ if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
50
+ throw new Error(
51
+ `repo-wiki local helper requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+ because repo-wiki itself requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
52
+ );
53
+ }
54
+ }
55
+
56
+ function run(command, args, options = {}) {
57
+ const result = spawnSync(command, args, {
58
+ cwd: options.cwd ?? PROJECT_ROOT,
59
+ env: { ...process.env, ...(options.env ?? {}) },
60
+ stdio: options.stdio ?? "inherit",
61
+ encoding: options.encoding ?? "utf8",
62
+ });
63
+
64
+ if (result.error) {
65
+ throw result.error;
66
+ }
67
+ if (result.status !== 0) {
68
+ const printable = [command, ...args].join(" ");
69
+ throw new Error(`Command failed (${result.status ?? "unknown"}): ${printable}`);
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ async function readBuildStamp(buildStampPath) {
76
+ try {
77
+ return JSON.parse(await readFile(buildStampPath, "utf8"));
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ async function writeBuildStamp(buildStampPath) {
84
+ await writeFile(buildStampPath, JSON.stringify({ ref: REPO_WIKI_REF }, null, 2) + "\n", "utf8");
85
+ }
86
+
87
+ export async function ensureRepoWikiPrepared(projectRoot = PROJECT_ROOT) {
88
+ assertSupportedNodeVersion();
89
+ const { baseDir, sourceDir, cliPath, buildStampPath } = resolveRepoWikiPaths(projectRoot);
90
+ await mkdir(baseDir, { recursive: true });
91
+
92
+ let currentHead = null;
93
+ try {
94
+ run("git", ["-C", sourceDir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
95
+ currentHead = run("git", ["-C", sourceDir, "rev-parse", "HEAD"], { stdio: "pipe" }).stdout.trim();
96
+ } catch {
97
+ run("git", ["clone", REPO_WIKI_GIT_URL, sourceDir], { cwd: baseDir });
98
+ }
99
+
100
+ if (currentHead !== REPO_WIKI_REF) {
101
+ run("git", ["-C", sourceDir, "fetch", "origin", REPO_WIKI_REF, "--depth", "1"]);
102
+ run("git", ["-C", sourceDir, "checkout", "--force", REPO_WIKI_REF]);
103
+ }
104
+
105
+ const stamp = await readBuildStamp(buildStampPath);
106
+ if (stamp?.ref !== REPO_WIKI_REF) {
107
+ run("npm", ["install", "--silent"], { cwd: sourceDir });
108
+ run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
109
+ await writeBuildStamp(buildStampPath);
110
+ } else {
111
+ try {
112
+ run(process.execPath, [cliPath, "--help"], { stdio: "ignore" });
113
+ } catch {
114
+ run("npm", ["install", "--silent"], { cwd: sourceDir });
115
+ run("npm", ["run", "build", "--silent"], { cwd: sourceDir });
116
+ await writeBuildStamp(buildStampPath);
117
+ }
118
+ }
119
+
120
+ return resolveRepoWikiPaths(projectRoot);
121
+ }
122
+
123
+ // Composable entry point: returns a structured result instead of calling
124
+ // process.exit so importers can reuse this wrapper without terminating the host
125
+ // process. The direct-run block below owns the process-level exit-code mapping.
126
+ export async function runRepoWikiLocal(argv, projectRoot = PROJECT_ROOT) {
127
+ const { prepareOnly, passthroughArgs } = parseCliArgs(argv);
128
+ const { cliPath } = await ensureRepoWikiPrepared(projectRoot);
129
+ if (prepareOnly) {
130
+ return { ok: true, status: 0, prepared: true, cliPath };
131
+ }
132
+
133
+ const result = spawnSync(process.execPath, [cliPath, ...passthroughArgs], {
134
+ cwd: projectRoot,
135
+ env: process.env,
136
+ stdio: "inherit",
137
+ });
138
+ if (result.error) {
139
+ throw result.error;
140
+ }
141
+ if (result.status !== 0) {
142
+ return { ok: false, status: result.status ?? 1, cliPath };
143
+ }
144
+ return { ok: true, status: 0, prepared: true, cliPath };
145
+ }
146
+
147
+ if (isDirectCliRun(import.meta.url)) {
148
+ runRepoWikiLocal(process.argv.slice(2)).then((result) => {
149
+ if (!result.ok) {
150
+ process.exitCode = result.status;
151
+ }
152
+ }).catch((error) => {
153
+ console.error(error instanceof Error ? error.message : String(error));
154
+ process.exitCode = 1;
155
+ });
156
+ }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Primary repo-wiki wrapper. Proxies to the published @mfittko/repo-wiki npm package
3
+ // at a pinned version, after validating that the consumer-repo config exists.
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync } from "node:fs";
6
+ import path from "node:path";
7
+ import process from "node:process";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ import { isDirectCliRun } from "@dev-loops/core/cli/helpers";
11
+
12
+ // Pinned to the latest published release at the time this slice was opened.
13
+ // Bump deliberately when the consumer repo wants to adopt a newer release.
14
+ export const REPO_WIKI_NPM_PACKAGE = "@mfittko/repo-wiki";
15
+ export const REPO_WIKI_NPM_VERSION = "0.2.6";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
19
+
20
+ export const REPO_WIKI_MIN_NODE_MAJOR = 20;
21
+
22
+ export function resolveRepoWikiConfigPath(projectRoot = PROJECT_ROOT) {
23
+ return path.join(projectRoot, ".llmwiki", "config.json");
24
+ }
25
+
26
+ export const REPO_WIKI_CONFIG_PATH = resolveRepoWikiConfigPath();
27
+ export const REPO_WIKI_SCHEMA_PATH = path.join(PROJECT_ROOT, ".llmwiki", "schema.md");
28
+
29
+ export function parseCliArgs(argv) {
30
+ const args = Array.isArray(argv) ? [...argv] : [];
31
+ if (args.length === 0) {
32
+ return { passthroughArgs: ["--help"] };
33
+ }
34
+ if (args[0] === "--help" || args[0] === "-h") {
35
+ return { passthroughArgs: ["--help"] };
36
+ }
37
+ return { passthroughArgs: args };
38
+ }
39
+
40
+ export function assertSupportedNodeVersion(version = process.versions.node) {
41
+ const major = Number.parseInt(String(version).split(".")[0] ?? "", 10);
42
+ if (!Number.isInteger(major) || major < REPO_WIKI_MIN_NODE_MAJOR) {
43
+ throw new Error(
44
+ `repo-wiki npm wrapper requires Node.js ${REPO_WIKI_MIN_NODE_MAJOR}+. Current runtime: ${version}`,
45
+ );
46
+ }
47
+ }
48
+
49
+ export function assertConsumerConfigPresent({
50
+ configPath = REPO_WIKI_CONFIG_PATH,
51
+ projectRoot = PROJECT_ROOT,
52
+ } = {}) {
53
+ // When callers override projectRoot without overriding configPath, derive the
54
+ // expected config path from the override so tests and other call sites stay
55
+ // consistent with the per-project layout.
56
+ const resolvedConfigPath =
57
+ configPath === REPO_WIKI_CONFIG_PATH ? resolveRepoWikiConfigPath(projectRoot) : configPath;
58
+ if (!existsSync(resolvedConfigPath)) {
59
+ throw new Error(
60
+ `Missing required repo-wiki config at ${path.relative(projectRoot, resolvedConfigPath) || resolvedConfigPath}.\n` +
61
+ `This repository expects a checked-in \`.llmwiki/config.json\`. ` +
62
+ `If you deleted it intentionally, restore it from git or regenerate it with \`repo-wiki init --repo .\`.`,
63
+ );
64
+ }
65
+ return resolvedConfigPath;
66
+ }
67
+
68
+ export function buildNpxInvocation({
69
+ packageName = REPO_WIKI_NPM_PACKAGE,
70
+ version = REPO_WIKI_NPM_VERSION,
71
+ passthroughArgs = [],
72
+ } = {}) {
73
+ return ["npx", "--yes", `${packageName}@${version}`, ...passthroughArgs];
74
+ }
75
+
76
+ export function runNpxInvocation({
77
+ command,
78
+ args,
79
+ cwd = PROJECT_ROOT,
80
+ env = process.env,
81
+ } = {}) {
82
+ const result = spawnSync(command, args, {
83
+ cwd,
84
+ env,
85
+ stdio: "inherit",
86
+ });
87
+ if (result.error) {
88
+ throw result.error;
89
+ }
90
+ return result;
91
+ }
92
+
93
+ // Composable entry point: returns a structured result instead of calling
94
+ // process.exit so importers can reuse this wrapper without terminating the host
95
+ // process. The direct-run block below owns the process-level exit-code mapping.
96
+ export async function runRepoWiki(argv, projectRoot = PROJECT_ROOT) {
97
+ assertSupportedNodeVersion();
98
+ assertConsumerConfigPresent({ projectRoot });
99
+ const { passthroughArgs } = parseCliArgs(argv);
100
+ const invocation = buildNpxInvocation({ passthroughArgs });
101
+
102
+ const result = runNpxInvocation({ command: invocation[0], args: invocation.slice(1), cwd: projectRoot });
103
+
104
+ if (result.status !== 0) {
105
+ return { ok: false, status: result.status ?? 1, invocation };
106
+ }
107
+ return { ok: true, status: 0, invocation };
108
+ }
109
+
110
+ if (isDirectCliRun(import.meta.url)) {
111
+ runRepoWiki(process.argv.slice(2)).then((result) => {
112
+ if (!result.ok) {
113
+ process.exitCode = result.status;
114
+ }
115
+ }).catch((error) => {
116
+ console.error(error instanceof Error ? error.message : String(error));
117
+ process.exitCode = 1;
118
+ });
119
+ }