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,125 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { buildParseError, isDirectCliRun, formatCliError } from "../_core-helpers.mjs";
5
+ const USAGE = `Usage: verify-fresh-review-context.mjs [--help] [--scope <name>]
6
+ Verify that the current subagent session has fresh context.
7
+ Options:
8
+ --scope <name> Unique reviewer scope (e.g. "draft-gate-coverage").
9
+ Must be non-empty, containing only alphanumeric
10
+ characters and hyphens. When provided, the sentinel
11
+ is scoped so parallel reviewers in the same working
12
+ directory do not trigger false contamination.
13
+ Output (stdout, JSON):
14
+ { "ok": true, "fresh": true, "sentinelCreated": true }
15
+ { "ok": true, "fresh": false, "sentinelCreated": false, "reason": "..." }
16
+ On error (stderr, JSON):
17
+ { "ok": false, "error": "...", "usage": "..." }
18
+ Exit codes:
19
+ 0 Clean (first run)
20
+ 1 Contaminated (prior session detected)
21
+ 2 Usage or internal error`.trim();
22
+ const VALID_SCOPE_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
23
+ const parseError = buildParseError(USAGE);
24
+ function resolveScope(argv) {
25
+ const idx = argv.indexOf("--scope");
26
+ if (idx === -1) return null;
27
+ const val = argv[idx + 1];
28
+ if (val === undefined || val === "" || (val.length > 0 && val[0] === "-")) {
29
+ return ""; // provided but missing/empty/flag-like
30
+ }
31
+ return val;
32
+ }
33
+ function resolveValidatedScope(argv) {
34
+ const raw = resolveScope(argv);
35
+ if (raw === null) return null;
36
+ if (raw === "" || !VALID_SCOPE_RE.test(raw)) {
37
+ process.stderr.write(`${formatCliError(
38
+ parseError(`Invalid --scope value "${raw}": must be non-empty and contain only alphanumeric characters and hyphens.`)
39
+ )}\n`);
40
+ return undefined; // signals invalid
41
+ }
42
+ return raw;
43
+ }
44
+ function sentinelRelative(scope) {
45
+ const suffix = scope ? `-${scope}` : "";
46
+ return path.join("tmp", `checkpoint-context-sentinel${suffix}.json`);
47
+ }
48
+ function legacySentinelRelative(scope) {
49
+ const suffix = scope ? `-${scope}` : "";
50
+ return path.join("tmp", `gate-review-context-sentinel${suffix}.json`);
51
+ }
52
+ async function checkSentinelExists(scope, cwd = process.cwd()) {
53
+ const sentinelPath = path.resolve(cwd, sentinelRelative(scope));
54
+ try { await stat(sentinelPath); return { exists: true, path: sentinelPath, legacy: false }; } catch (err) {
55
+ if (err.code !== "ENOENT") throw err;
56
+ }
57
+ const legacyPath = path.resolve(cwd, legacySentinelRelative(scope));
58
+ try { await stat(legacyPath); return { exists: true, path: legacyPath, legacy: true }; } catch (err) {
59
+ if (err.code !== "ENOENT") throw err;
60
+ }
61
+ return { exists: false, path: sentinelPath, legacy: false };
62
+ }
63
+ async function main(argv = process.argv.slice(2)) {
64
+ if (argv.includes("--help") || argv.includes("-h")) {
65
+ process.stdout.write(`${USAGE}\n`);
66
+ return 0;
67
+ }
68
+ const scope = resolveValidatedScope(argv);
69
+ if (scope === undefined) return 2;
70
+ const sentinelPath = path.resolve(process.cwd(), sentinelRelative(scope));
71
+ try {
72
+ await mkdir(path.dirname(sentinelPath), { recursive: true });
73
+ } catch (err) {
74
+ process.stderr.write(`${formatCliError(err)}\n`);
75
+ return 2;
76
+ }
77
+ const existing = await checkSentinelExists(scope);
78
+ if (existing.exists) {
79
+ process.stdout.write(JSON.stringify({
80
+ ok: true,
81
+ fresh: false,
82
+ sentinelCreated: false,
83
+ reason: `Checkpoint context sentinel already exists${existing.legacy ? " (legacy name)" : ""} — inherited session context detected. Restart the subagent with fresh context (subagent({context:\"fresh\"})).`,
84
+ }) + "\n");
85
+ return 1;
86
+ }
87
+ const sentinel = {
88
+ createdAt: new Date().toISOString(),
89
+ pid: process.pid,
90
+ ...(scope ? { scope } : {}),
91
+ };
92
+ try {
93
+ await writeFile(sentinelPath, JSON.stringify(sentinel, null, 2) + "\n", {
94
+ encoding: "utf8",
95
+ flag: "wx",
96
+ });
97
+ } catch (err) {
98
+ if (err.code === "EEXIST") {
99
+ process.stdout.write(JSON.stringify({
100
+ ok: true,
101
+ fresh: false,
102
+ sentinelCreated: false,
103
+ reason: "Checkpoint context sentinel already exists (detected on atomic create) — inherited session context detected. Restart the subagent with fresh context (subagent({context:\"fresh\"})).",
104
+ }) + "\n");
105
+ return 1;
106
+ }
107
+ process.stderr.write(`${formatCliError(err)}\n`);
108
+ return 2;
109
+ }
110
+ process.stdout.write(JSON.stringify({
111
+ ok: true,
112
+ fresh: true,
113
+ sentinelCreated: true,
114
+ }) + "\n");
115
+ return 0;
116
+ }
117
+ if (isDirectCliRun(import.meta.url)) {
118
+ try {
119
+ const exitCode = await main();
120
+ process.exitCode = exitCode;
121
+ } catch (err) {
122
+ process.stderr.write(`${formatCliError(err)}\n`);
123
+ process.exitCode = 2;
124
+ }
125
+ }
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parsePrNumber, requireOptionValue } from "../_cli-primitives.mjs";
5
+ import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
6
+ const USAGE = `Usage: write-gate-findings-log.mjs --repo <owner/name> --pr <number> --gate <draft_gate|pre_approval_gate> --head-sha <sha> --verdict <clean|findings_present|blocked> --findings <json> [--tmp-root <path>]
7
+ Write a durable <gate>-<headSha>.json log under deterministic tmp/ paths.
8
+ Required:
9
+ --repo <owner/name>
10
+ --pr <number>
11
+ --gate <draft_gate|pre_approval_gate>
12
+ --head-sha <sha>
13
+ --verdict <clean|findings_present|blocked>
14
+ --findings <json> JSON array of finding objects with severity, disposition, angle, and summary
15
+ Optional:
16
+ --tmp-root <path> Root tmp directory (default: tmp/)
17
+ `.trim();
18
+ function parseError(message) {
19
+ return Object.assign(new Error(message), { usage: USAGE });
20
+ }
21
+ function normalizeGate(value) {
22
+ const gates = new Set(["draft_gate", "pre_approval_gate"]);
23
+ const normalized = String(value).trim().toLowerCase();
24
+ return gates.has(normalized) ? normalized : null;
25
+ }
26
+ function normalizeVerdict(value) {
27
+ const verdicts = new Set(["clean", "findings_present", "blocked"]);
28
+ const normalized = String(value).trim().toLowerCase();
29
+ return verdicts.has(normalized) ? normalized : null;
30
+ }
31
+ function normalizeHeadSha(value) {
32
+ const normalized = String(value).trim().toLowerCase();
33
+ return /^[0-9a-f]{7,64}$/i.test(normalized) ? normalized : null;
34
+ }
35
+ const VALID_SEVERITIES = new Set(["must-fix", "worth-fixing-now", "defer"]);
36
+ const VALID_DISPOSITIONS = new Set(["accepted-for-fix", "deferred", "disputed", "operator_acknowledged"]);
37
+ function parseFindingsJson(raw) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ } catch {
42
+ throw parseError("--findings must be valid JSON");
43
+ }
44
+ if (!Array.isArray(parsed)) {
45
+ throw parseError("--findings must be a JSON array");
46
+ }
47
+ return parsed.map((f, i) => {
48
+ if (!f || typeof f !== "object") {
49
+ throw parseError(`--findings[${i}] must be an object`);
50
+ }
51
+ if (!f.severity || !VALID_SEVERITIES.has(f.severity)) {
52
+ throw parseError(`--findings[${i}].severity must be one of: must-fix, worth-fixing-now, defer`);
53
+ }
54
+ if (!f.angle || typeof f.angle !== "string" || f.angle.trim().length === 0) {
55
+ throw parseError(`--findings[${i}].angle is required`);
56
+ }
57
+ if (!f.summary || typeof f.summary !== "string" || f.summary.trim().length === 0) {
58
+ throw parseError(`--findings[${i}].summary is required`);
59
+ }
60
+ const entry = {
61
+ severity: f.severity,
62
+ angle: f.angle.trim(),
63
+ summary: f.summary.trim(),
64
+ };
65
+ if ("disposition" in f) {
66
+ if (typeof f.disposition !== "string" || f.disposition.trim().length === 0) {
67
+ throw parseError(`--findings[${i}].disposition must be a non-empty string`);
68
+ }
69
+ const disp = f.disposition.trim();
70
+ if (!VALID_DISPOSITIONS.has(disp)) {
71
+ throw parseError(`--findings[${i}].disposition must be one of: accepted-for-fix, deferred, disputed, operator_acknowledged`);
72
+ }
73
+ entry.disposition = disp;
74
+ }
75
+ if (Array.isArray(f.files)) {
76
+ entry.files = f.files.filter(x => typeof x === "string" && x.trim().length > 0);
77
+ }
78
+ if ("resolvedIn" in f) {
79
+ if (typeof f.resolvedIn !== "string" || f.resolvedIn.trim().length === 0) {
80
+ throw parseError(`--findings[${i}].resolvedIn must be a non-empty string`);
81
+ }
82
+ const sha = f.resolvedIn.trim();
83
+ if (!/^[0-9a-f]{7,64}$/i.test(sha)) {
84
+ throw parseError(`--findings[${i}].resolvedIn must be a 7-64 char hex SHA`);
85
+ }
86
+ entry.resolvedIn = sha;
87
+ }
88
+ return entry;
89
+ });
90
+ }
91
+ export function parseWriteGateFindingsLogCliArgs(argv) {
92
+ const args = [...argv];
93
+ const options = {
94
+ repo: undefined,
95
+ pr: undefined,
96
+ gate: undefined,
97
+ headSha: undefined,
98
+ verdict: undefined,
99
+ findings: undefined,
100
+ tmpRoot: "tmp",
101
+ };
102
+ while (args.length > 0) {
103
+ const token = args.shift();
104
+ if (token === "--help" || token === "-h") {
105
+ return { help: true };
106
+ }
107
+ if (token === "--repo") {
108
+ options.repo = requireOptionValue(args, "--repo", parseError).trim();
109
+ continue;
110
+ }
111
+ if (token === "--pr") {
112
+ options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
113
+ continue;
114
+ }
115
+ if (token === "--gate") {
116
+ const gate = normalizeGate(requireOptionValue(args, "--gate", parseError));
117
+ if (!gate) throw parseError("--gate must be draft_gate or pre_approval_gate");
118
+ options.gate = gate;
119
+ continue;
120
+ }
121
+ if (token === "--head-sha") {
122
+ const sha = normalizeHeadSha(requireOptionValue(args, "--head-sha", parseError));
123
+ if (!sha) throw parseError("--head-sha must be a 7-64 character hex SHA");
124
+ options.headSha = sha;
125
+ continue;
126
+ }
127
+ if (token === "--verdict") {
128
+ const verdict = normalizeVerdict(requireOptionValue(args, "--verdict", parseError));
129
+ if (!verdict) throw parseError("--verdict must be clean, findings_present, or blocked");
130
+ options.verdict = verdict;
131
+ continue;
132
+ }
133
+ if (token === "--findings") {
134
+ options.findings = requireOptionValue(args, "--findings", parseError);
135
+ continue;
136
+ }
137
+ if (token === "--tmp-root") {
138
+ options.tmpRoot = requireOptionValue(args, "--tmp-root", parseError).trim();
139
+ continue;
140
+ }
141
+ throw parseError(`Unknown argument: ${token}`);
142
+ }
143
+ const missing = ["repo", "pr", "gate", "headSha", "verdict", "findings"]
144
+ .filter(k => options[k] === undefined);
145
+ if (missing.length > 0) {
146
+ throw parseError(`Missing required arguments: ${missing.join(", ")}`);
147
+ }
148
+ return options;
149
+ }
150
+ function buildLogPath({ repo, pr, gate, headSha, tmpRoot }) {
151
+ const parts = repo.split("/");
152
+ if (parts.length !== 2 || parts.some(p => p.length === 0)) {
153
+ throw new Error(`--repo must be in owner/name format, got: ${JSON.stringify(repo)}`);
154
+ }
155
+ for (const p of parts) {
156
+ if (p === "." || p === ".." || /[\s\\]/.test(p)) {
157
+ throw new Error(`--repo segment ${JSON.stringify(p)} contains unsafe characters (dots, whitespace, or backslashes)`);
158
+ }
159
+ }
160
+ const repoSlug = parts.join("-");
161
+ return path.join(tmpRoot, "gate-findings", repoSlug, `pr-${pr}`, `${gate}-${headSha}.json`);
162
+ }
163
+ export async function writeGateFindingsLog(options, { repoRoot = process.cwd() } = {}) {
164
+ const findings = parseFindingsJson(options.findings);
165
+ const logPath = buildLogPath({
166
+ repo: options.repo,
167
+ pr: options.pr,
168
+ gate: options.gate,
169
+ headSha: options.headSha,
170
+ tmpRoot: options.tmpRoot || "tmp",
171
+ });
172
+ const fullPath = path.resolve(repoRoot, logPath);
173
+ const log = {
174
+ repo: options.repo,
175
+ pr: options.pr,
176
+ gate: options.gate,
177
+ headSha: options.headSha,
178
+ verdict: options.verdict,
179
+ loggedAt: new Date().toISOString(),
180
+ findings,
181
+ };
182
+ await mkdir(path.dirname(fullPath), { recursive: true });
183
+ await writeFile(fullPath, JSON.stringify(log, null, 2) + "\n", "utf8");
184
+ return { ok: true, path: logPath, log };
185
+ }
186
+ async function main() {
187
+ let options;
188
+ try {
189
+ options = parseWriteGateFindingsLogCliArgs(process.argv.slice(2));
190
+ } catch (error) {
191
+ process.stderr.write(`${formatCliError(error, { usage: USAGE })}\n`);
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ if (options.help) {
196
+ process.stdout.write(`${USAGE}\n`);
197
+ return;
198
+ }
199
+ try {
200
+ const result = await writeGateFindingsLog(options);
201
+ process.stdout.write(JSON.stringify(result) + "\n");
202
+ } catch (error) {
203
+ process.stderr.write(JSON.stringify({
204
+ ok: false,
205
+ error: error instanceof Error ? error.message : String(error),
206
+ }) + "\n");
207
+ process.exitCode = 1;
208
+ }
209
+ }
210
+ if (isDirectCliRun(import.meta.url)) {
211
+ await main();
212
+ }
@@ -0,0 +1,55 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseJsonText } from "../_core-helpers.mjs";
3
+ import {
4
+ buildCheckpointFilePath,
5
+ buildDefaultCheckpointDir,
6
+ buildLegacyDefaultCheckpointDir,
7
+ } from "./_checkpoint-paths.mjs";
8
+ export async function readExistingCheckpoint(repo, pr, { checkpointDir, failSilently = false } = {}) {
9
+ const normalizedRepo = typeof repo === "string" ? repo.trim().toLowerCase() : repo;
10
+ if (checkpointDir !== undefined) {
11
+ const filePath = buildCheckpointFilePath(checkpointDir);
12
+ try {
13
+ const text = await readFile(filePath, "utf8");
14
+ return { checkpoint: parseJsonText(text), filePath };
15
+ } catch (error) {
16
+ if (error && error.code === "ENOENT") {
17
+ return { checkpoint: null, filePath: null };
18
+ }
19
+ if (failSilently) {
20
+ return { checkpoint: null, filePath: null };
21
+ }
22
+ throw new Error(`Failed to read checkpoint '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
23
+ }
24
+ }
25
+ const preferredDir = buildDefaultCheckpointDir(normalizedRepo, pr);
26
+ const preferredPath = buildCheckpointFilePath(preferredDir);
27
+ try {
28
+ const text = await readFile(preferredPath, "utf8");
29
+ return { checkpoint: parseJsonText(text), filePath: preferredPath };
30
+ } catch (error) {
31
+ if (error && error.code !== "ENOENT") {
32
+ if (failSilently) {
33
+ return { checkpoint: null, filePath: null };
34
+ }
35
+ throw new Error(`Failed to read checkpoint '${preferredPath}': ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ }
38
+ const legacyPath = buildCheckpointFilePath(buildLegacyDefaultCheckpointDir(pr));
39
+ try {
40
+ const text = await readFile(legacyPath, "utf8");
41
+ const checkpoint = parseJsonText(text);
42
+ if (checkpoint?.repo === normalizedRepo && checkpoint?.pr === pr) {
43
+ return { checkpoint, filePath: legacyPath };
44
+ }
45
+ return { checkpoint: null, filePath: null };
46
+ } catch (error) {
47
+ if (error && error.code === "ENOENT") {
48
+ return { checkpoint: null, filePath: null };
49
+ }
50
+ if (failSilently) {
51
+ return { checkpoint: null, filePath: null };
52
+ }
53
+ throw new Error(`Failed to read checkpoint '${legacyPath}': ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ }
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ function splitRepo(repo) {
3
+ if (typeof repo !== "string") {
4
+ throw new Error("repo must be a string");
5
+ }
6
+ const normalized = repo.trim().toLowerCase();
7
+ const parts = normalized.split("/");
8
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
9
+ throw new Error(`repo must be owner/name, received ${repo}`);
10
+ }
11
+ return { owner: parts[0], name: parts[1], normalized };
12
+ }
13
+ export function buildDefaultCheckpointDir(repo, pr) {
14
+ const { owner, name } = splitRepo(repo);
15
+ return path.join("tmp", "copilot-loop", owner, name, `pr-${pr}`);
16
+ }
17
+ export function buildLegacyDefaultCheckpointDir(pr) {
18
+ return path.join("tmp", "copilot-loop", `pr-${pr}`);
19
+ }
20
+ export function buildCheckpointFilePath(checkpointDir) {
21
+ return path.join(checkpointDir, "outer-loop-state.json");
22
+ }
23
+ export function buildDefaultCheckpointFilePath(repo, pr) {
24
+ return buildCheckpointFilePath(buildDefaultCheckpointDir(repo, pr));
25
+ }
26
+ export function buildLegacyDefaultCheckpointFilePath(pr) {
27
+ return buildCheckpointFilePath(buildLegacyDefaultCheckpointDir(pr));
28
+ }
@@ -0,0 +1,230 @@
1
+ import { PR_CHECKPOINT } from "@dev-loops/core/loop/pr-gate-coordination";
2
+ const HANDOFF_OWNERSHIP = Object.freeze({
3
+ SUBAGENT: "subagent",
4
+ PARENT: "parent",
5
+ HUMAN: "human",
6
+ TERMINAL: "terminal",
7
+ });
8
+ const HANDOFF_STOP_BOUNDARY = Object.freeze({
9
+ SUBAGENT_EXIT: "subagent_exit",
10
+ WATCH_BOUNDARY: "watch_boundary",
11
+ APPROVAL_BOUNDARY: "approval_boundary",
12
+ MERGE_BOUNDARY: "merge_boundary",
13
+ CONFLICT_BOUNDARY: "conflict_boundary",
14
+ TERMINAL: "terminal_boundary",
15
+ });
16
+ const HANDOFF_RESUME_POLICY = Object.freeze({
17
+ RESUME_AFTER_SUBAGENT_EXIT: "resume_after_subagent_exit",
18
+ RESUME_AFTER_STATE_REFRESH: "resume_after_state_refresh",
19
+ RESUME_AFTER_HUMAN_APPROVAL: "resume_after_human_approval",
20
+ RESUME_AFTER_MERGE_AUTHORIZATION: "resume_after_merge_authorization",
21
+ MANUAL_ATTENTION: "manual_attention",
22
+ NONE: "none",
23
+ });
24
+ const GATE_BOUNDARY_STOP_VALUES = new Set(Object.values(PR_CHECKPOINT));
25
+ const SUBAGENT_ACTIONS = new Set([
26
+ "fix_threads",
27
+ "draft_gate",
28
+ "request_review",
29
+ "rerequest_review",
30
+ "run_pre_approval",
31
+ ]);
32
+ function normalizeContractValue(value) {
33
+ return typeof value === "string" ? value.trim().toLowerCase() : null;
34
+ }
35
+ function buildContract({ ownership, stopBoundary, resumePolicy }) {
36
+ return {
37
+ ownership,
38
+ stopBoundary,
39
+ resumePolicy,
40
+ };
41
+ }
42
+ export function buildHandoffContractForConductorAction({ action, gateBoundary, requiresApproval = false } = {}) {
43
+ const normalizedAction = normalizeContractValue(action);
44
+ if (SUBAGENT_ACTIONS.has(normalizedAction)) {
45
+ if (requiresApproval) {
46
+ return buildContract({
47
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
48
+ stopBoundary: HANDOFF_STOP_BOUNDARY.APPROVAL_BOUNDARY,
49
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_HUMAN_APPROVAL,
50
+ });
51
+ }
52
+ return buildContract({
53
+ ownership: HANDOFF_OWNERSHIP.SUBAGENT,
54
+ stopBoundary: HANDOFF_STOP_BOUNDARY.SUBAGENT_EXIT,
55
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_SUBAGENT_EXIT,
56
+ });
57
+ }
58
+ switch (normalizedAction) {
59
+ case "watch":
60
+ return buildContract({
61
+ ownership: HANDOFF_OWNERSHIP.PARENT,
62
+ stopBoundary: HANDOFF_STOP_BOUNDARY.WATCH_BOUNDARY,
63
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_STATE_REFRESH,
64
+ });
65
+ case "merge":
66
+ return buildContract({
67
+ ownership: requiresApproval ? HANDOFF_OWNERSHIP.HUMAN : HANDOFF_OWNERSHIP.PARENT,
68
+ stopBoundary: HANDOFF_STOP_BOUNDARY.MERGE_BOUNDARY,
69
+ resumePolicy: requiresApproval
70
+ ? HANDOFF_RESUME_POLICY.RESUME_AFTER_MERGE_AUTHORIZATION
71
+ : HANDOFF_RESUME_POLICY.NONE,
72
+ });
73
+ case "await_approval":
74
+ return buildContract({
75
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
76
+ stopBoundary: HANDOFF_STOP_BOUNDARY.APPROVAL_BOUNDARY,
77
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_HUMAN_APPROVAL,
78
+ });
79
+ case "resolve_conflicts":
80
+ case "blocked":
81
+ case "error":
82
+ return buildContract({
83
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
84
+ stopBoundary: HANDOFF_STOP_BOUNDARY.CONFLICT_BOUNDARY,
85
+ resumePolicy: HANDOFF_RESUME_POLICY.MANUAL_ATTENTION,
86
+ });
87
+ case "done":
88
+ return buildContract({
89
+ ownership: HANDOFF_OWNERSHIP.TERMINAL,
90
+ stopBoundary: HANDOFF_STOP_BOUNDARY.TERMINAL,
91
+ resumePolicy: HANDOFF_RESUME_POLICY.NONE,
92
+ });
93
+ default:
94
+ return buildContract({
95
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
96
+ stopBoundary: HANDOFF_STOP_BOUNDARY.CONFLICT_BOUNDARY,
97
+ resumePolicy: HANDOFF_RESUME_POLICY.MANUAL_ATTENTION,
98
+ });
99
+ }
100
+ }
101
+ export function buildHandoffContractForResumeAction(resumeAction) {
102
+ const normalizedAction = normalizeContractValue(resumeAction);
103
+ switch (normalizedAction) {
104
+ case "needs_feedback_fix":
105
+ return buildContract({
106
+ ownership: HANDOFF_OWNERSHIP.SUBAGENT,
107
+ stopBoundary: HANDOFF_STOP_BOUNDARY.SUBAGENT_EXIT,
108
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_SUBAGENT_EXIT,
109
+ });
110
+ case "needs_reply_resolve":
111
+ return buildContract({
112
+ ownership: HANDOFF_OWNERSHIP.SUBAGENT,
113
+ stopBoundary: HANDOFF_STOP_BOUNDARY.SUBAGENT_EXIT,
114
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_SUBAGENT_EXIT,
115
+ });
116
+ case "needs_rerequest_or_watch":
117
+ return buildContract({
118
+ ownership: HANDOFF_OWNERSHIP.PARENT,
119
+ stopBoundary: HANDOFF_STOP_BOUNDARY.WATCH_BOUNDARY,
120
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_STATE_REFRESH,
121
+ });
122
+ case "await_final_approval":
123
+ return buildContract({
124
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
125
+ stopBoundary: HANDOFF_STOP_BOUNDARY.APPROVAL_BOUNDARY,
126
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_HUMAN_APPROVAL,
127
+ });
128
+ case "await_merge_authorization":
129
+ return buildContract({
130
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
131
+ stopBoundary: HANDOFF_STOP_BOUNDARY.MERGE_BOUNDARY,
132
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_MERGE_AUTHORIZATION,
133
+ });
134
+ case "await_ready_for_review_authorization":
135
+ return buildContract({
136
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
137
+ stopBoundary: HANDOFF_STOP_BOUNDARY.APPROVAL_BOUNDARY,
138
+ resumePolicy: HANDOFF_RESUME_POLICY.RESUME_AFTER_HUMAN_APPROVAL,
139
+ });
140
+ case "done_or_merged":
141
+ return buildContract({
142
+ ownership: HANDOFF_OWNERSHIP.TERMINAL,
143
+ stopBoundary: HANDOFF_STOP_BOUNDARY.TERMINAL,
144
+ resumePolicy: HANDOFF_RESUME_POLICY.NONE,
145
+ });
146
+ case "needs_manual_attention":
147
+ default:
148
+ return buildContract({
149
+ ownership: HANDOFF_OWNERSHIP.HUMAN,
150
+ stopBoundary: HANDOFF_STOP_BOUNDARY.CONFLICT_BOUNDARY,
151
+ resumePolicy: HANDOFF_RESUME_POLICY.MANUAL_ATTENTION,
152
+ });
153
+ }
154
+ }
155
+ function parseContractLine(text, label) {
156
+ const pattern = new RegExp(String.raw`(?:^|\n)\s*[-*]?\s*${label}:\s*(.+)$`, "imu");
157
+ const match = text.match(pattern);
158
+ return match?.[1]?.trim() ?? null;
159
+ }
160
+ export function parseRecordedHandoffContract(text) {
161
+ if (typeof text !== "string" || text.length === 0) {
162
+ return { contract: null, reason: null };
163
+ }
164
+ const ownership = normalizeContractValue(parseContractLine(text, "Handoff ownership"));
165
+ const stopBoundary = normalizeContractValue(parseContractLine(text, "Stop boundary"));
166
+ const resumePolicy = normalizeContractValue(parseContractLine(text, "Resume policy"));
167
+ const foundCount = [ownership, stopBoundary, resumePolicy].filter((value) => value !== null).length;
168
+ if (foundCount === 0) {
169
+ return { contract: null, reason: null };
170
+ }
171
+ if (foundCount !== 3) {
172
+ return {
173
+ contract: null,
174
+ reason: "incomplete_handoff_contract",
175
+ details: {
176
+ ownership,
177
+ stopBoundary,
178
+ resumePolicy,
179
+ },
180
+ };
181
+ }
182
+ const validOwnership = Object.values(HANDOFF_OWNERSHIP).includes(ownership);
183
+ const validStopBoundary = Object.values(HANDOFF_STOP_BOUNDARY).includes(stopBoundary)
184
+ || GATE_BOUNDARY_STOP_VALUES.has(stopBoundary);
185
+ const validResumePolicy = Object.values(HANDOFF_RESUME_POLICY).includes(resumePolicy);
186
+ if (!validOwnership || !validStopBoundary || !validResumePolicy) {
187
+ return {
188
+ contract: null,
189
+ reason: "invalid_handoff_contract",
190
+ details: {
191
+ ownership,
192
+ stopBoundary,
193
+ resumePolicy,
194
+ },
195
+ };
196
+ }
197
+ return {
198
+ contract: buildContract({
199
+ ownership,
200
+ stopBoundary,
201
+ resumePolicy,
202
+ }),
203
+ reason: null,
204
+ };
205
+ }
206
+ export function compareHandoffContracts(recorded, expected) {
207
+ if (!recorded || !expected) {
208
+ return null;
209
+ }
210
+ const mismatches = [];
211
+ for (const key of ["ownership", "stopBoundary", "resumePolicy"]) {
212
+ if (recorded[key] !== expected[key]) {
213
+ mismatches.push(key);
214
+ }
215
+ }
216
+ if (mismatches.length === 0) {
217
+ return null;
218
+ }
219
+ return {
220
+ mismatches,
221
+ recorded,
222
+ expected,
223
+ };
224
+ }
225
+ export {
226
+ HANDOFF_OWNERSHIP,
227
+ HANDOFF_STOP_BOUNDARY,
228
+ HANDOFF_RESUME_POLICY,
229
+ SUBAGENT_ACTIONS
230
+ };