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,1007 @@
1
+ # Shared script area
2
+
3
+ This directory is reserved for deterministic workflow entrypoints.
4
+
5
+ Scripts here should prefer:
6
+
7
+ 1. native `gh ... watch` support when it matches the exact wait condition,
8
+ 2. shared package helpers for pure parsing and state logic,
9
+ 3. stable machine-readable JSON output for skills and async workflows.
10
+
11
+ In this source-loaded workspace repo, root scripts may consume shared package helpers through a thin local adapter rather than a published package import path so the checkout remains runnable without an install step.
12
+
13
+ ## Authority note
14
+
15
+ For the script surfaces documented here:
16
+ - code, tests, and the helper entrypoints themselves are authoritative for shipped runtime behavior
17
+ - this README summarizes those contracts for operators and maintainers; if behavior changes, update the code/tests and then sync this document
18
+ - use the more specific state-graph and contract docs under `docs/` when a helper family has a narrower machine-readable contract that this README is summarizing
19
+
20
+ ## Scripts
21
+
22
+ ### `scripts/docs/validate-links.mjs`
23
+
24
+ Validate repo-owned markdown relative links for the shipped docs / skills / agent surface.
25
+
26
+ Usage:
27
+ - `node scripts/docs/validate-links.mjs`
28
+
29
+ Optional:
30
+ - `--root <path>` — override the repo root to scan (used by deterministic tests and local dry-runs against another checkout)
31
+
32
+ Contract:
33
+ - scans these markdown sources only: `README.md`, `PLAN.md`, `AGENTS.md`, `scripts/README.md`, `extension/README.md`, `docs/**/*.md` (excluding `docs/archive/**`), `skills/**/*.md`, and `agents/**/*.md`
34
+ - validates inline relative markdown links after resolving them from the containing file
35
+ - strips any `#fragment` before checking the filesystem
36
+ - treats existing files and directories as valid targets
37
+ - ignores external URLs, `mailto:`, fragment-only links, image links, and links inside fenced code blocks
38
+ - supports a checked-in narrow ignore list through repo-root `.linkcheckignore` for intentional symbolic placeholder targets (for example `docs/phases/phase-x.md`)
39
+ - prints actionable broken-link output with source file, line, raw target, resolved path, and a suggestion only when one clear candidate exists
40
+
41
+ Failure behavior:
42
+ - exits `0` when all validated links resolve
43
+ - exits `1` when one or more broken links are found
44
+ - exits non-zero (other than `1`) for usage/runtime failures
45
+
46
+ ### `scripts/github/capture-review-threads.mjs`
47
+
48
+ Capture and normalize PR review-thread JSON.
49
+
50
+ Supported inputs:
51
+ - `--input <path>`
52
+ - stdin JSON
53
+ - live GitHub capture with `--repo <owner/name> --pr <number>`
54
+
55
+ Optional:
56
+ - `--output <path>` writes the same success JSON emitted on stdout
57
+
58
+ Success output shape:
59
+ - `{ "ok": true, "source": { ... }, "summary": { ... }, "threads": [...], "comments": [...] }`
60
+ - normalized `comments[]` preserve both the GraphQL comment node id (`id`) and the REST-safe numeric review-comment id (`databaseId`) when available
61
+ - normalized `threads[]` include `commentDatabaseIds` and `actionableCommentDatabaseIds` so follow-up helpers can pair `--comment-id` and `--thread-id` from the same fresh snapshot
62
+ - when `--output` is used, success output also includes `"outputPath"`
63
+
64
+ Failure behavior:
65
+ - malformed arguments, invalid JSON, and `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
66
+ - live capture is only allowed when both `--repo` and `--pr` are present
67
+
68
+ ### `scripts/github/create-draft-pr.mjs`
69
+
70
+ Thin wrapper around `gh pr create` for draft-first PR creation.
71
+
72
+ Usage:
73
+ - `node scripts/github/create-draft-pr.mjs [gh pr create args...]`
74
+ - `node <resolved-skill-scripts>/github/create-draft-pr.mjs [gh pr create args...]`
75
+
76
+ Contract:
77
+ - injects exactly one `--draft` when the caller did not already supply it
78
+ - rejects `--ready` before invoking `gh`; use `gh pr ready` later after draft-gate approval
79
+ - forwards every other argument to `gh pr create` unchanged and in order
80
+ - preserves the underlying `gh pr create` stdout, stderr, and exit code without wrapping success output
81
+ - stays intentionally narrow: this is the prevention layer for draft-first creation, while `scripts/github/reconcile-draft-gate.mjs` remains the separate recovery path for already-open non-draft PRs
82
+
83
+ Failure behavior:
84
+ - wrapper-owned validation failures emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
85
+ - when `gh pr create` itself exits non-zero, this wrapper preserves the original stdout/stderr and propagates the same exit code
86
+
87
+ ### `scripts/github/request-copilot-review.mjs`
88
+
89
+ Request Copilot review on a PR and verify the request deterministically.
90
+
91
+ Required:
92
+ - `--repo <owner/name>`
93
+ - `--pr <number>`
94
+
95
+ Optional:
96
+ - `--force-rerequest-review` — bypass the round cap when new commits exist since the last Copilot review. Refused with `no_changes_since_last_review` when the PR head has not changed since the last review. No-op when the round cap has not been reached. Normal requests now automatically re-open at round cap when the head changed, all prior feedback is resolved, and current GitHub CI is green.
97
+
98
+ Contract:
99
+ - checks `requested_reviewers` first so an existing Copilot request is detected without mutating PR state again
100
+ - requests Copilot via `gh pr edit <pr> --repo <owner/name> --add-reviewer @copilot`
101
+ - is suitable both for the first request after ready-for-review and for later explicit re-requests after follow-up fix commits land on the PR head
102
+ - enforces a round cap (default: 5, configurable via `maxCopilotRounds` in settings); clean round-cap PRs with new commits, resolved threads, and green current GitHub CI automatically re-open for a normal re-request, while `--force-rerequest-review` remains the operator override for other new-commit cases
103
+ - should be paired with a fresh unresolved-thread check after Copilot posts again; requesting review alone does not complete the loop
104
+ - verifies the result through `gh api repos/<owner>/<name>/pulls/<pr>/requested_reviewers`
105
+ - does **not** rely on `gh pr view --json reviewRequests`, which can be incomplete for Copilot reviewer state
106
+ - normalizes known repository/tooling limitations into a machine-readable `unavailable` result instead of forcing callers to parse ad hoc stderr
107
+
108
+ Success output shape:
109
+ - `{ "ok": true, "status": "requested"|"already-requested"|"unavailable"|"suppressed_same_head_clean"|"blocked_by_copilot_comment"|"round_cap_reached"|"no_changes_since_last_review"|"suppressed_draft", "repo": "owner/name", "pr": 17, "reviewer": "Copilot", ... }`
110
+ - `unavailable` also includes a `detail` string with the normalized GitHub/CLI limitation
111
+ - `round_cap_reached` includes `completedRounds` and `maxRounds` fields
112
+ - `no_changes_since_last_review` is returned by `--force-rerequest-review` when the PR head SHA has not changed since the last Copilot review
113
+
114
+ Failure behavior:
115
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
116
+
117
+ ### `scripts/github/detect-linked-issue-pr.mjs`
118
+
119
+ Detect whether an issue already has an open linked PR in the same repository.
120
+
121
+ Required:
122
+ - `--repo <owner/name>`
123
+ - `--issue <number>`
124
+
125
+ Contract:
126
+ - queries issue timeline linked-PR events (`CONNECTED_EVENT`, `CROSS_REFERENCED_EVENT`)
127
+ - pages through timeline items until `hasNextPage=false`
128
+ - keeps only open linked PRs in the same repository (`repository.nameWithOwner === <repo>`)
129
+ - also tracks closed-unmerged (state=`CLOSED`) same-repo linked PRs separately
130
+ - chooses deterministically when multiple candidates remain:
131
+ 1. prefer `CONNECTED_EVENT` candidates over `CROSS_REFERENCED_EVENT`
132
+ 2. then choose newest linked-event `createdAt`
133
+ 3. then stable fallback by PR number/url
134
+ - returns a machine-readable selection payload for skills/workflows; callers should not re-implement query/pagination/tie-break logic in markdown policy text
135
+
136
+ Success output shape:
137
+ - when `hasOpenLinkedPr: true`: `{ "ok": true, "repo": "owner/name", "issue": 85, "hasOpenLinkedPr": true, "prNumber": 90, "prUrl": "...", "selection": { "eventType": "...", "eventCreatedAt": "..." } }`
138
+ - when `hasOpenLinkedPr: false`: `{ "ok": true, "repo": "owner/name", "issue": 85, "hasOpenLinkedPr": false, "prNumber": null, "prUrl": null, "hasPriorClosedUnmergedPr": true|false, "priorClosedUnmergedPrNumber": 149|null, "priorClosedUnmergedPrUrl": "..."|null }`
139
+
140
+ Failure behavior:
141
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
142
+
143
+ ### `scripts/github/resolve-tracker-local-spec.mjs`
144
+
145
+ Resolve the canonical spec bundle for tracker-backed local implementation from one
146
+ GitHub issue reference. This is the bounded GitHub-backed path for tracker-backed
147
+ local spec resolution; it does not create or read `docs/phases/phase-<n>.md`.
148
+
149
+ Allowed inputs:
150
+ - `--repo <owner/name>` with `--issue <number>`
151
+ - `--issue-url <github issue url>`
152
+
153
+ Contract:
154
+ - deterministically resolves exactly one repo slug + issue number pair
155
+ - reads the GitHub issue via `gh issue view <number> --repo <owner/name> --json number,title,body,url,state`
156
+ - treats the issue as canonical for tracker-backed local sessions
157
+ - always reports `localPhaseDocAllowed: false` so callers do not silently maintain a duplicate local phase doc for the same session
158
+ - leaves full tracker-sync policy to higher-level callers; this helper's bounded responsibility is spec resolution only
159
+
160
+ Success output shape:
161
+ - `{ "ok": true, "repo": "owner/name", "issue": 85, "issueUrl": "...", "state": "OPEN"|"CLOSED", "title": "...", "body": "...", "canonicalSpecSource": "tracker_issue", "localImplementationMode": "tracker_backed", "localPhaseDocAllowed": false, "stateSync": "tracker_issue_is_canonical" }`
162
+
163
+ Failure behavior:
164
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
165
+ - unexpected `gh` failures and malformed `gh` JSON emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
166
+
167
+ ### `scripts/github/manage-sub-issues.mjs`
168
+
169
+ Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
170
+ Use this for epic/umbrella issue decomposition. See [Sub-Issue Tree Contract](../docs/sub-issue-tree-contract.md) for the full workflow.
171
+
172
+ Commands:
173
+ - `list` — list sub-issues of a parent issue in tree order
174
+ - `add` — attach a child issue to a parent as a real GitHub sub-issue
175
+ - `reorder` — set the execution order of sub-issues (highest priority first)
176
+ - `verify` — verify that the current sub-issue tree matches an expected set (and optionally order)
177
+
178
+ Required for all commands:
179
+ - `--repo <owner/name>`
180
+ - `--issue <number>` (parent issue)
181
+
182
+ `add` adds: `--child <number>`
183
+ `reorder` adds: `--order <n1,n2,...>` (comma-separated issue numbers in desired execution order)
184
+ `verify` adds: `--expected <n1,n2,...>` and optional `--ordered` flag
185
+
186
+ Contract:
187
+ - `add` resolves the child issue's internal GitHub id before calling the sub-issues REST endpoint
188
+ - `reorder` first lists current sub-issues to validate all specified numbers are already in the tree, then issues sequential priority-update API calls
189
+ - `verify` is read-only and exits 0 for mismatch-only results; `"verified": false` is a machine-readable signal, not a process failure. Argument errors and unexpected `gh`/runtime failures still exit non-zero
190
+ - do not re-implement sub-issue management ad hoc or bypass this helper
191
+
192
+ Success output shapes:
193
+ - `list`: `{ "ok": true, "repo": "owner/name", "issue": N, "command": "list", "subIssues": [{ "number": M, "title": "...", "state": "...", "id": ID }, ...] }`
194
+ - `add`: `{ "ok": true, "repo": "owner/name", "issue": N, "command": "add", "child": M }`
195
+ - `reorder`: `{ "ok": true, "repo": "owner/name", "issue": N, "command": "reorder", "order": [n1, n2, ...] }`
196
+ - `verify`: `{ "ok": true, ..., "command": "verify", "verified": true|false, "expected": [...], "actual": [...], "missing": [...], "unexpected": [...] }` (plus `"orderMismatch": true` when `--ordered` and only the order differs)
197
+
198
+ Failure behavior:
199
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
200
+ - argument errors also include `"usage"` in the error payload
201
+
202
+ ### `scripts/refine/verify.mjs`
203
+
204
+ Verify epic-tree refinement structure and contracts in one pass.
205
+
206
+ Usage:
207
+ - `node scripts/refine/verify.mjs --issue <number> [--repo <owner/name>] [--json]`
208
+ - `node scripts/refine/verify.mjs --input <path> [--json]`
209
+
210
+ Contract:
211
+ - runs four deterministic checkers:
212
+ - `prose-linkage-detector` (forbidden prose parent/child/dependency markers + missing API links)
213
+ - `scope-boundary-cross-checker` (mutual-exclusion gaps + duplicate sibling ownership)
214
+ - `refinement-completeness-checker` (AC/DoD/non-goals/matrix presence)
215
+ - `tree-integrity-validator` (parent links, orphan detection, cycles, max depth 3)
216
+ - online mode (`--issue`) walks GitHub sub-issues API from the root issue
217
+ - offline mode (`--input`) validates a local refinement tree JSON snapshot
218
+ - default output is human-readable text; `--json` emits machine-readable output for CI
219
+
220
+ Failure behavior:
221
+ - checker findings are process-failing (`exit 1`) because verification did not pass
222
+ - malformed arguments and runtime errors emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
223
+
224
+ ### `scripts/github/stage-reviewer-draft.mjs`
225
+
226
+ Stage a pending reviewer-side draft review from a merged deterministic review package.
227
+
228
+ Required:
229
+ - `--repo <owner/name>`
230
+ - `--pr <number>`
231
+ - `--review-file <path>`
232
+
233
+ Optional:
234
+ - `--local-state-output <path>` writes/merges deterministic draft-review metadata for later
235
+ `detect-reviewer-loop-state.mjs --local-state` use
236
+
237
+ Contract:
238
+ - reads a merged reviewer result JSON file (for example output derived from `mergeReviewerResults`)
239
+ - builds a deterministic pending-review payload pinned to the review package `headSha`
240
+ - posts the pending review to `repos/<owner>/<name>/pulls/<pr>/reviews` without an `event` field so GitHub keeps it pending
241
+ - returns the draft review id/url/commit sha
242
+ - optionally writes bounded local reviewer-loop metadata including `draftReviewPosted`, `draftReviewId`, `draftReviewUrl`, `draftReviewCommitSha`, and `draftReviewNotificationStatus`
243
+
244
+ Success output shape:
245
+ - `{ "ok": true, "repo": "owner/name", "pr": 17, "reviewId": 456, "reviewUrl": "...", "reviewState": "PENDING", "commitSha": "abc123", "localStatePath": "..."|null }`
246
+
247
+ Failure behavior:
248
+ - malformed arguments, invalid review JSON, missing `headSha`, unexpected `gh` failures, and malformed review-create responses emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
249
+
250
+ ### `scripts/github/reply-resolve-review-thread.mjs`
251
+
252
+ Reply to a PR review comment and resolve the associated review thread deterministically.
253
+
254
+ Required:
255
+ - `--repo <owner/name>`
256
+ - `--pr <number>`
257
+ - `--comment-id <number>`
258
+ - `--thread-id <node-id>`
259
+ - `--body-file <path>`
260
+
261
+ Contract:
262
+ - reads the reply body from a file so shell quoting does not become part of the workflow logic
263
+ - validates the live PR thread snapshot before mutating GitHub so `--comment-id` and `--thread-id` must refer to the same thread on the target PR
264
+ - posts the reply to `repos/<owner>/<name>/pulls/<pr>/comments/<comment-id>/replies`
265
+ - resolves the thread with the GraphQL `resolveReviewThread` mutation
266
+ - fails if the thread does not report resolved after the mutation
267
+
268
+ Success output shape:
269
+ - `{ "ok": true, "repo": "owner/name", "pr": 17, "commentId": 123, "threadId": "...", "replyId": 456, "replyUrl": "...", "resolved": true }`
270
+
271
+ Failure behavior:
272
+ - malformed arguments, empty body files, missing threads, missing comments, comment/thread mismatches, unexpected `gh` failures, and unsuccessful resolve responses emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
273
+
274
+ ### `scripts/github/reply-resolve-review-threads.mjs`
275
+
276
+ Reply to all matching unresolved review threads on one PR and optionally resolve them with one bounded note.
277
+
278
+ Required:
279
+ - `--repo <owner/name>`
280
+ - `--pr <number>`
281
+
282
+ Optional:
283
+ - `--author <login>` (default `Copilot`)
284
+ - exactly one message source: `--message <text>` or stdin
285
+ - `--resolve`
286
+
287
+ Contract:
288
+ - captures one authoritative review-thread snapshot via `capture-review-threads.mjs`
289
+ - filters to unresolved threads containing at least one comment by the selected author
290
+ - chooses the newest matching author-authored comment in each matched thread as the REST reply target
291
+ - processes matched threads sequentially in deterministic snapshot order
292
+ - reuses the shared single-thread reply/resolve primitives instead of duplicating GitHub mutation logic
293
+ - with `--resolve`, re-captures the review-thread snapshot at the end and fails closed if any targeted thread remains unresolved
294
+ - zero-match runs are deterministic no-ops with success JSON
295
+
296
+ Success output shape:
297
+ - `{ "ok": true, "repo": "owner/name", "pr": 17, "author": "Copilot", "resolve": true|false, "matchedThreadCount": 2, "repliedThreadCount": 2, "resolvedThreadCount": 2, "skippedThreadCount": 1, "results": [{ "threadId": "...", "commentId": 123, "replyId": 456, "replyUrl": "...", "resolved": true }] }`
298
+
299
+ Failure behavior:
300
+ - malformed arguments, empty/conflicting message input, malformed thread snapshots, unexpected `gh` failures, reply failures, resolve failures, and failed post-resolve verification emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
301
+ - when partial progress exists, stderr JSON also includes `partialProgress`
302
+
303
+ For new GitHub mutation helpers in this repo, do not stop at fixture-only confidence when a real PR is available and mutation is authorized. Run a bounded real-PR smoke check before depending on the helper inside a longer async review/fix loop.
304
+
305
+ ### `scripts/github/probe-copilot-review.mjs`
306
+
307
+ Watch for fresh Copilot-authored review activity on a PR.
308
+
309
+ Required:
310
+ - `--repo <owner/name>`
311
+ - `--pr <number>`
312
+
313
+ Optional:
314
+ - `--poll-interval-ms <positive-integer>` (default `60000`, i.e. 1 minute)
315
+ - `--timeout-ms <non-negative-integer>` (default `1800000`, i.e. 30 minutes)
316
+
317
+ Contract:
318
+ - captures a baseline snapshot, then performs a bounded number of follow-up polls
319
+ - returns `changed` for any fresh Copilot-authored review-thread comments, PR review summaries, or PR issue comments that were not present in the baseline snapshot
320
+ - ignores fresh non-Copilot review activity across those same surfaces
321
+ - `--timeout-ms 0` performs a single immediate recheck and returns `idle` if unchanged
322
+
323
+ Success output shape:
324
+ - `{ "ok": true, "status": "changed"|"timeout"|"idle", "repo": "owner/name", "pr": 17, "attempts": 1, "newComments": [...], "newReviews": [...], "newIssueComments": [...] }`
325
+
326
+ Failure behavior:
327
+ - malformed arguments and `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
328
+
329
+ ### `scripts/loop/detect-initial-copilot-pr-state.mjs`
330
+
331
+ Detect the post-assignment issue-to-linked-PR seam for Copilot handoff.
332
+
333
+ Required:
334
+ - `--repo <owner/name>`
335
+ - `--issue <number>`
336
+
337
+ Contract:
338
+ - uses `scripts/github/detect-linked-issue-pr.mjs` as the authoritative linked-PR selector
339
+ - returns exactly one deterministic state:
340
+ - `no_linked_pr`
341
+ - `prior_linked_pr_closed_unmerged`
342
+ - `copilot_session_active`
343
+ - `waiting_for_initial_copilot_implementation`
344
+ - `linked_pr_ready_for_followup`
345
+ - uses `scripts/loop/detect-copilot-session-activity.mjs` on the linked PR head branch for Copilot-authored draft PRs
346
+ - while `activity=active`, emits `copilot_session_active` regardless of commit/file-count heuristics
347
+ - approval-gated `action_required` Copilot/Actions runs are treated as observational (non-active) for this bootstrap seam
348
+ - for non-bootstrap linked PRs, falls back to the existing substantive PR heuristics when session activity is `idle` or `concluded`
349
+ - if the session-activity check itself fails, the helper fails closed instead of pretending session state was unavailable
350
+ - classifies `prior_linked_pr_closed_unmerged` when there is no open linked PR but a same-repo linked PR was previously closed without merging; this is a terminal non-wait state requiring human reconciliation
351
+ - classifies `waiting_for_initial_copilot_implementation` only for the bounded bootstrap-only draft shape:
352
+ - open same-repo linked PR
353
+ - draft
354
+ - Copilot-authored (`Copilot`, `copilot-swe-agent`, `app/copilot-swe-agent`, or `copilot-swe-agent[bot]`)
355
+ - exactly 1 commit
356
+ - sole commit headline exactly `Initial plan`
357
+ - exactly 0 changed files
358
+ - fails closed with explicit error output when required PR facts cannot be fetched
359
+
360
+ Success output shape:
361
+ - `{ "ok": true, "repo": "owner/name", "issue": 59, "state": "...", "prNumber": 79|null, "prUrl": "..."|null, "headBranch": "..."|null, "authorLogin": "Copilot"|null, "isDraft": true|false|null, "changedFiles": 0|null, "commitCount": 1|null, "soleCommitHeadline": "Initial plan"|null, "sessionActivity": "active"|"concluded"|"idle"|null, "sessionRunId": 123|null, "sessionRunName": "..."|null, "sessionRunStatus": "..."|null, "sessionRunConclusion": string|null, "sessionRunCreatedAt": "..."|null, "sessionConfidence": "high"|null }`
362
+
363
+ ### `scripts/loop/run-conductor-cycle.mjs`
364
+
365
+ Poll all open PRs, detect gate state, and emit an ordered action queue.
366
+
367
+ Output `actions[]` entries include:
368
+ - `requiresSubagent`
369
+ - `requiresApproval`
370
+ - `handoffContract` with:
371
+ - `ownership`
372
+ - `stopBoundary`
373
+ - `resumePolicy`
374
+
375
+ The handoff contract is recorded on each action so parent, child, and resume
376
+ actions can share one explicit stop/resume boundary.
377
+
378
+ Contract values:
379
+ - `ownership`: `subagent`, `parent`, `human`, or `terminal`
380
+ - `stopBoundary`: `subagent_exit`, `watch_boundary`, `approval_boundary`,
381
+ `merge_boundary`, `conflict_boundary`, or `terminal_boundary`
382
+ - `resumePolicy`: `resume_after_subagent_exit`,
383
+ `resume_after_state_refresh`, `resume_after_human_approval`,
384
+ `resume_after_merge_authorization`, `manual_attention`, or `none`
385
+
386
+ `run-conductor-cycle.mjs` emits the contract-boundary values above. The parser
387
+ also accepts `PR_CHECKPOINT` values when reading recorded artifacts so older
388
+ recorded handoff notes can still be validated fail-closed.
389
+
390
+ ### `scripts/loop/conductor-monitor.mjs`
391
+
392
+ Aggregate the current Copilot-loop status for every open PR in one repository.
393
+
394
+ Required:
395
+ - `--repo <owner/name>`
396
+
397
+ Optional:
398
+ - `--auto-resume` — inspect documented pi-subagents async/session artifacts on disk, detect orphaned open PR follow-up runs, and emit deterministic resume plans without executing `subagent({ action: "resume" })`
399
+
400
+ Contract:
401
+ - lists all open PRs via `gh pr list --state open --limit 1000` to avoid GitHub CLI default truncation
402
+ - reuses `detect-copilot-loop-state.mjs` logic for each PR instead of inventing a second state classifier
403
+ - reports one queue-level summary with per-PR loop state, next action, and whether human follow-up is needed now
404
+ - reports `queue_complete` when the open-PR queue is empty
405
+ - keeps the current implementation human-in-the-loop; fully autonomous monitor ownership is a follow-up slice
406
+ - when `--auto-resume` is present, scans documented async evidence sources only:
407
+ - pi-subagents async run dirs under `<tmpdir>/pi-subagents-*/async-subagent-runs/<runId>/`
408
+ (`status.json`, `events.jsonl`, `output-<n>.log`)
409
+ - pi-subagents async result artifacts under
410
+ `<tmpdir>/pi-subagents-*/async-subagent-results/`
411
+ - persisted session/subagent artifacts under the Pi sessions tree (for example
412
+ `~/.pi/agent/sessions/.../subagent-artifacts/*_dev-loop_*_meta.json` and
413
+ `~/.pi/agent/sessions/.../subagent-artifacts/*_dev-loop_*_output.md`
414
+ plus matching `run-<n>/session.jsonl` files)
415
+ - ignores non-`dev-loop` agents, other repositories, merged PRs, and any exited run superseded by a newer matching `running` or `queued` run
416
+ - matches exited runs to open PRs only by PR number parsed from artifact text; branch names, issue numbers, or worktree paths alone are never sufficient identity
417
+ - fail-closes to `needsManualAttention` when PR identity, artifact state, or resume inputs are missing, contradictory, or ambiguous
418
+ - `--auto-resume` remains single-shot only; it does not poll, sleep, watch, or execute the resume itself
419
+
420
+ Success output shape:
421
+ - default:
422
+ - `{ "ok": true, "repo": "owner/name", "checkedAt": "...", "prCount": 2, "queueStatus": "queue_complete"|"monitoring"|"attention_needed", "needsAttentionCount": 0, "summary": { "waiting": 0, "needsAttention": 0, "blocked": 0, "done": 0 }, "prs": [...] }`
423
+ - with `--auto-resume`:
424
+ - adds
425
+ `{ "autoResumeRequested": true, "orphanedPrCount": 1, "resumePlanCount": 1, "manualAttentionCount": 0, "resumePlans": [...], "needsManualAttention": [...] }`
426
+
427
+ `resumePlans[]` fields:
428
+ - `pr`
429
+ - `runId`
430
+ - `runState` (`completed` | `failed` | `paused`)
431
+ - `artifactPath`
432
+ - `sessionPath` when known
433
+ - `parsedArtifactState`
434
+ - `parsedLoopState`
435
+ - `livePrState`
436
+ - `resumeAction`
437
+ - `handoffContract`
438
+ - `recordedHandoffContract` when the artifact explicitly records one
439
+ - `resumeMessage`
440
+ - `resumeCommandPreview`
441
+ - `staleWorktree`
442
+
443
+ If a recorded handoff contract is present, it must include explicit
444
+ `Handoff ownership`, `Stop boundary`, and `Resume policy` lines. The monitor
445
+ fails closed when those fields are partial, invalid, or conflict with the
446
+ live-derived resume plan.
447
+
448
+ `needsManualAttention[]` fields:
449
+ - `pr` when known
450
+ - `runId` when known
451
+ - `reason`
452
+ - `evidence`
453
+ - `suggestedNextStep`
454
+
455
+ Failure behavior:
456
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
457
+
458
+ ### `scripts/loop/detect-copilot-session-activity.mjs`
459
+
460
+ Detect deterministic Copilot workflow session activity on a branch.
461
+
462
+ Required:
463
+ - `--repo <owner/name>`
464
+ - `--branch <name>`
465
+
466
+ Optional:
467
+ - `--limit <positive-integer>` (default `20`)
468
+
469
+ Contract:
470
+ - uses `gh run list --branch <branch>` as the primary signal
471
+ - pattern-matches known Copilot run names (`Copilot coding for issue`, `Addressing comment on PR`, `Addressing review on PR`)
472
+ - classifies activity as:
473
+ - `active` when a matching run is currently in progress
474
+ - `concluded` when the most recent matching run is completed
475
+ - `concluded` (non-blocking observational) when a matching run is approval-gated in `action_required`; the payload still preserves the raw `runStatus` / `runConclusion` strings for debugging
476
+ - `idle` when no matching runs are found
477
+
478
+ Failure behavior:
479
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
480
+
481
+ ### `scripts/loop/copilot-pr-handoff.mjs`
482
+
483
+ Thin high-level helper for the common Copilot PR follow-up handoff path.
484
+
485
+ Required:
486
+ - `--repo <owner/name>`
487
+ - `--pr <number>`
488
+
489
+ Optional:
490
+ - `--watch-status <changed|timeout|idle>` — refresh deterministic state after a prior watcher observation; this readback mode never requests review again
491
+
492
+ Contract:
493
+ - this helper is the source of truth for normal request/re-request/watch routing on Copilot PR follow-up
494
+ - emits deterministic `action` + `nextAction` + `reviewRequestStatus` and a helper-owned `requestWatchContract` envelope for status interpretation
495
+ - enters watch only when request state is confirmed (`requested` or `already-requested`) and emits exact `watchArgs` + `watchTimeoutPolicy`
496
+ - watch refresh (`--watch-status`) is observational-only; rely on refreshed `loopDisposition` + `terminal` to decide whether to continue or stop
497
+ - explicit stop/blocked routing is machine-readable via `action: "stop"` plus `requestWatchContract.stopState`
498
+ - when `PI_SUBAGENT_RUN_ID` is set to a non-empty value, the helper enforces one-runner-per-PR ownership and returns `runnerOwnership` details when a conflicting or displaced run must stop
499
+
500
+ Success output shape:
501
+ - `{ "ok": true, "action": "watch"|"fix"|"stop", "state": "...", "allowedTransitions": [...], "nextAction": "...", "snapshot": {...}, "reviewRequestStatus"?: "...", "watchStatus"?: "...", "autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false, "loopDisposition": "...", "terminal": true|false, "requestWatchContract": { "action": "...", "nextAction": "...", "requestStatus": "requested"|"already-requested"|"unavailable"|"failed"|"none", "routingState": "copilot_request_confirmed_waiting"|"ready_state_needs_copilot_request"|"draft_reset_requires_ready_state_reentry"|"non_ready_state", "watchEntryConfirmed": true|false, "watchArgs": { ... }|null, "stopState"?: "unavailable"|"blocked"|"draft_requires_ready_state_reentry"|"no_automatic_next_step" }, "watchTimeoutPolicy"?: { "classification": "external_healthy_wait", "minimumTimeoutMs": 1800000, "defaultTimeoutMs": 1800000 }, "watchArgs"?: { ... } }`
502
+
503
+ Failure behavior:
504
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
505
+
506
+ ### `scripts/loop/pr-runner-coordination.mjs`
507
+
508
+ Durable one-runner-per-PR coordination helper.
509
+
510
+ Subcommands:
511
+ - `status` — inspect current ownership record for one PR
512
+ - `claim` — claim ownership when no active runner exists; otherwise fail closed with machine-readable conflict details
513
+ - `takeover` — explicit replacement path that records prior/new run ids
514
+ - `assert` — verify the current run still owns the PR; `--require-existing` fails closed when async pre-merge ownership proof is missing
515
+ - `release` — clear ownership when the active run is done
516
+
517
+ State path:
518
+ - `.pi/runner-coordination/<owner>/<repo>/pr-<number>.json`
519
+ - this is local coordination state under `.pi/`; keep it uncommitted like other repo-local runtime artifacts
520
+
521
+ Integration notes:
522
+ - `copilot-pr-handoff.mjs` uses this surface for async run ownership checks
523
+ - `detect-checkpoint-evidence.mjs` uses strict ownership assertion before async merge-time gate evaluation
524
+
525
+ ### `scripts/loop/run-watch-cycle.mjs`
526
+
527
+ Deterministic handoff → watch helper for one Copilot wait-cycle boundary.
528
+
529
+ Required:
530
+ - `--repo <owner/name>`
531
+ - `--pr <number>`
532
+
533
+ Optional:
534
+ - `--probe-only` — use a single immediate recheck (`timeoutMs: 0`) for explicit status probes only
535
+
536
+ Contract:
537
+ - runs `copilot-pr-handoff.mjs` first and preserves its current state / next action / watch args
538
+ - when handoff stays in watch mode, checks Copilot session activity on the PR head branch via `detect-copilot-session-activity.mjs`
539
+ - when activity is `active`, blocks on `gh run watch <run-id>` and then continues with the same emitted persistent watch budget instead of silently degrading to a zero-timeout probe
540
+ - when handoff returns `action: "watch"`, runs `probe-copilot-review.mjs` with the emitted `watchArgs`; zero-timeout probes are reserved for explicit `--probe-only` status checks
541
+ - treats `waiting_for_copilot_review` as a persistence boundary, not a completion boundary
542
+ - for explicit async loop entry/continuation, `cycleDisposition: "pending"` with `terminal: false` means stay attached and run another watch boundary rather than exiting as clean success
543
+ - after a follow-up fix / reply-resolve / re-request path returns to `waiting_for_copilot_review`, resume this helper again instead of treating the re-request handoff as completion
544
+ - handoff-only behavior must be explicitly requested; do not silently reinterpret async loop entry as one-step transition behavior
545
+ - preserves the shared Copilot-loop `loopDisposition` contract from the handoff/state-machine output (`pending`, `unresolved_feedback`, `clean_converged`, `blocked`, `action_required`, `done`)
546
+ - exposes the helper's coarser wait-cycle summary separately as `cycleDisposition`
547
+ - reports `cycleDisposition: "pending"` for quiet watch results (`timeout` or explicit probe `idle`) instead of pretending the loop concluded cleanly
548
+ - reserves zero-timeout `idle` probes for explicit status/reattach checks; normal async waiting should use the emitted non-zero watch timeout
549
+ - returns `cycleDisposition: "needs_followup"` when fresh Copilot activity appears or handoff already routed directly to `fix`
550
+ - returns `cycleDisposition: "terminal"` only when handoff routed to `stop`
551
+
552
+ Success output shape:
553
+ - `{ "ok": true, "handoffAction": "watch"|"fix"|"stop", "state": "...", "allowedTransitions": [...], "nextAction": "...", "snapshot": {...}, "reviewRequestStatus"?: "...", "watchArgs"?: { ... }, "watchTimeoutPolicy"?: { "classification": "external_healthy_wait", "minimumTimeoutMs": 1800000, "defaultTimeoutMs": 1800000 }, "watchStatus"?: "changed"|"timeout"|"idle", "watch"?: { ... }, "loopDisposition": "pending"|"unresolved_feedback"|"clean_converged"|"blocked"|"action_required"|"done", "cycleDisposition": "pending"|"needs_followup"|"terminal", "terminal": true|false }`
554
+
555
+ Failure behavior:
556
+ - malformed arguments and unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
557
+
558
+ ### `scripts/loop/detect-copilot-loop-state.mjs`
559
+
560
+ Deterministic Copilot-loop state detector. Captures current loop state from observable PR/GitHub
561
+ facts and interprets the snapshot into one explicit current state, allowed next transitions, and
562
+ a recommended next action. This script is the orchestration authority for the async Copilot
563
+ review/fix loop; see [Copilot Loop State Graph](../docs/copilot-loop-state-graph.md) for the full state-graph design.
564
+
565
+ Two modes:
566
+
567
+ - **Auto-detect**: `--repo <owner/name> --pr <number>`
568
+ Fetches PR state, Copilot review request status, review threads, and CI checks from GitHub,
569
+ builds a snapshot, and interprets it. PR CI/check normalization is owned by
570
+ [Copilot CI Status Contract](../skills/docs/copilot-ci-status-contract.md).
571
+
572
+ - **Snapshot interpretation**: `--input <path>`
573
+ Reads a pre-built snapshot JSON and interprets it without any `gh` calls. Use this mode when
574
+ the caller has already gathered facts — for example, to incorporate the `status` field from a
575
+ prior `scripts/github/request-copilot-review.mjs` run, which can report `unavailable` or
576
+ `failed` statuses that are not observable from static GitHub state alone.
577
+
578
+ Optional (auto-detect mode only):
579
+ - `--steering-state-file <path>`
580
+ Overlay the detected state with the current persisted steering contract state.
581
+ The detector stays read-only: it does not promote queued steering or write the
582
+ steering file. This is available only in `--repo/--pr` mode; snapshot `--input`
583
+ mode does not accept steering files because repo/pr target identity cannot be
584
+ proven from the snapshot alone.
585
+ - `--review-request-status <requested|already-requested|unavailable|none|failed>`
586
+ Override the Copilot review-request status with a known prior result. Skips the
587
+ `requested_reviewers` API call and injects the provided value directly into the snapshot.
588
+ Use when the caller already ran `request-copilot-review.mjs` and wants to inject its output
589
+ status without re-probing the reviewers endpoint.
590
+
591
+ Snapshot schema (`--input` mode or `snapshot` field in success output):
592
+ - `prExists` {boolean} — whether a PR was found
593
+ - `prNumber` {number|null} — PR number if prExists, otherwise null
594
+ - `prDraft` {boolean} — whether the PR is in draft state
595
+ - `prMerged` {boolean} — whether the PR has been merged
596
+ - `prClosed` {boolean} — whether the PR was closed without merge; merged PRs set `prMerged=true` instead of reusing `prClosed`
597
+ - `copilotReviewRequestStatus` {"requested"|"already-requested"|"unavailable"|"none"|"failed"} — current known Copilot review-request state
598
+ - `copilotReviewPresent` {boolean} — whether at least one Copilot review exists on the PR
599
+ - `copilotReviewOnCurrentHead` {boolean} — whether a submitted (non-PENDING) Copilot review exists for the current head commit
600
+ - `unresolvedThreadCount` {number} — total unresolved review-thread count
601
+ - `actionableThreadCount` {number} — unresolved threads with non-bot actionable comments
602
+ - `copilotReviewRoundCount` {number} — completed Copilot review rounds observed on the PR
603
+ - `ciStatus` {"success"|"failure"|"pending"|"none"} — contract-owned current-head CI/check rollup
604
+ from [Copilot CI Status Contract](../skills/docs/copilot-ci-status-contract.md); `none` means no usable readiness signal yet
605
+ - `agentFixStatus` {"applied"|null} — agent-provided: "applied" when code has been fixed
606
+
607
+ Success output shape:
608
+ - `{ "ok": true, "snapshot": { ... }, "state": "...", "allowedTransitions": [...], "nextAction": "...", "autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false, "loopDisposition": "...", "terminal": true|false }`
609
+ - `state` is one of the stable state names defined in [Copilot Loop State Graph](../docs/copilot-loop-state-graph.md)
610
+ - `allowedTransitions` is the list of states reachable from `state`
611
+ - `nextAction` is a human-readable recommended next step
612
+ - `autoRerequestEligible` is `true` only when a meaningful remediation event has made automatic re-request valid again
613
+ - `sameHeadCleanConverged` is `true` when the current head has clean convergence (a submitted Copilot review, zero unresolved and actionable threads, and CI not blocked), suppressing automatic same-head re-request
614
+ - `loopDisposition` is the high-level refreshed classification: `pending`, `unresolved_feedback`, `clean_converged`, `blocked`, `action_required`, or `done`
615
+ - `terminal` is `true` only for clean-converged, blocked, or done states; watcher timeout/idle must be treated as non-terminal until a refreshed detector output proves `terminal=true`
616
+
617
+ Failure behavior:
618
+ - Malformed arguments, unexpected `gh` failures, and review-thread detection failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
619
+
620
+ Key behavioral guarantees:
621
+ - When `unresolvedThreadCount > 0`, the state is always in the fix/reply-resolve family — never `waiting_for_copilot_review` or any wait state
622
+ - When `copilotReviewRequestStatus` is `unavailable` or `failed`, the state is a terminal stop/report state with no allowed transitions
623
+ - When `agentFixStatus` is `"applied"` and unresolved threads exist, the state is `already_fixed_needs_reply_resolve`, and `allowedTransitions` includes only `ready_to_rerequest_review`
624
+ - When the current head has clean convergence (a submitted Copilot review, zero unresolved and actionable threads, and CI not blocked), automatic same-head re-request is suppressed until a meaningful remediation event occurs
625
+ - If review-thread state cannot be determined during auto-detect, the script fails closed instead of assuming zero unresolved threads
626
+ - When `--steering-state-file` is provided, steering is surfaced as a read-only overlay;
627
+ queued steering promotion/persistence is owned explicitly by `steer-loop.mjs promote`
628
+
629
+ ### `scripts/github/upsert-checkpoint-verdict.mjs`
630
+
631
+ Creates or updates the visible gate-review PR comment for one `gate + headSha` pair.
632
+ Use this at the `draft_gate` / `pre_approval_gate` boundaries so same-head reruns
633
+ remain idempotent: the helper updates an existing same-head marker in place when
634
+ correction is needed and suppresses duplicate reposts when the visible comment
635
+ already matches the requested contract fields. The rendered visible comment uses compact readable labels (`Gate review`, `Reviewed head SHA`, `Verdict`, `Findings summary`, `Next action`). When a gate pass needed corrective changes before reaching `clean`, pass a truthful `--findings-summary` that briefly states the gap, the change, and why the current head is now acceptable instead of defaulting to `no issues found`. Verbose multiline `--findings-summary` input is compacted before posting so visible gate comments keep validation reporting bounded: raw passing output is omitted, command/count/CI signals are preferred, and any failure excerpt uses a deterministic retained-prefix limit plus a short truncation marker suffix when needed.
636
+
637
+ Required:
638
+ - `--repo <owner/name>`
639
+ - `--pr <number>`
640
+ - `--gate <draft_gate|pre_approval_gate>`
641
+ - `--head-sha <sha>` — full current head SHA or a hexadecimal prefix of it; the helper canonicalizes to the full current head before comparing/updating visible markers
642
+ - `--verdict <clean|findings_present|blocked>`
643
+ - `--findings-summary <text>`
644
+ - `--next-action <text>`
645
+
646
+ Optional:
647
+ - `--local-validation-head-sha <sha>` — reuse the bounded `crediblyGreen` exception when GitHub created zero current-head suites/statuses and local verification already passed for that exact head
648
+ - `--force --force-reason <text>` — narrow operator-authorized CI override for the helper-local gate-entry refusal only; `--force-reason` is required with `--force`, `--force-reason` without `--force` is rejected, and the reason text is whitespace-normalized for machine-readable output
649
+
650
+ Success output shape:
651
+ - standard path: `{ "ok": true, "action": "created"|"updated"|"noop", "repo": "owner/repo", "pr": 17, "gate": "draft_gate", "headSha": "abc1234", "currentHeadSha": "abc1234", "commentId": 101, "commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-101" }`
652
+ - forced CI-override path: the same shape plus `{ "forced": true, "forceReason": "CI failed due to transient infrastructure", "forceBypass": "ci_blocked_needs_user_decision" }`
653
+
654
+ Failure behavior:
655
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
656
+ - contradictory head-SHA requests or unexpected `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
657
+ - gate upserts still fail closed when `detect-pr-gate-coordination-state.mjs` reports that the requested gate action is illegal for the current head
658
+ - when that refusal is specifically `lifecycleState=blocked_needs_user_decision` plus current-head `ciStatus="failure"`, the unforced error now points operators to `--force --force-reason` as the explicit CI-only escape hatch
659
+ - `--force` does **not** bypass stale-head protection, non-draft `draft_gate` refusal, unresolved-feedback / unsettled-review `pre_approval_gate` refusal, merge-conflict handling, or closed/merged PR protection
660
+
661
+ Use `--force` only after the user explicitly authorizes ignoring the current-head CI failure for this one gate-comment upsert. Prefer the normal paths first: green current-head CI, `--local-validation-head-sha` for the bounded `crediblyGreen` case, or the draft-gate policy knob from issue #351 when the desired behavior is a durable draft-gate policy rather than a one-off override.
662
+
663
+ ### `scripts/loop/run-refinement-audit.mjs`
664
+
665
+ Runs one deterministic bounded refinement audit over explicit repo paths and emits one machine-readable planning artifact.
666
+ Use this before refiner fan-out when a local phase or GitHub issue would benefit from scoped slop-risk discovery.
667
+
668
+ Required:
669
+ - exactly one of:
670
+ - `--paths <comma-separated path list>`
671
+ - `--paths-file <file>`
672
+
673
+ Optional:
674
+ - `--root <path>` — override the repo root (defaults to `git rev-parse --show-toplevel`)
675
+ - `--max-lines <n>` — oversized-file threshold (default `1000`)
676
+ - `--duplicate-window-lines <n>` — duplicate-block window size (default `4`)
677
+ - `--branch-threshold <n>` — branching-hotspot threshold (default `25` control-flow tokens)
678
+ - `--thin-wrapper-max-lines <n>` — thin-wrapper threshold (default `40`)
679
+ - `--output <path>` — writes the same success JSON emitted on stdout
680
+
681
+ Success output shape:
682
+ - `{ "ok": true, "repoRoot": "/repo", "paths": ["AGENTS.md"], "auditedFiles": [...], "findings": [...], "highestValueFollowUpCandidates": [...], "scopeBoundary": { "mode": "bounded_paths_only", "fullRepoScan": false } }`
683
+ - `auditedFiles[]` reports per-file line count, branch-token count, duplicate-block match count, thin-wrapper classification, and deterministic skip notes for binary/unreadable files
684
+ - `findings[]` uses bounded heuristic ids: `oversized_file`, `duplicate_block_candidate`, `branching_hotspot`, `thin_wrapper_candidate`
685
+ - `highestValueFollowUpCandidates[]` is the concise planning handoff surface for refiner fan-out/fan-in; findings are planning inputs, not auto-authorized rewrites
686
+
687
+ Failure behavior:
688
+ - malformed arguments, blank paths, invalid thresholds, unreadable `--paths-file` input, or zero auditable files after tracked-file expansion emit `{ "ok": false, "error": "...", ... }` on stderr and exit non-zero
689
+ - findings do **not** fail the process; findings return exit `0`
690
+ - `--paths*` is required; the helper never silently scans the whole repo
691
+
692
+ ### `scripts/loop/detect-pr-gate-coordination-state.mjs`
693
+
694
+ Fetches the live PR facts needed to answer which gate/transition is legal next for a pull request. It combines the shared Copilot loop-state machine with visible `draft_gate` / `pre_approval_gate` evidence, GitHub `mergeStateStatus`, and local `git -c core.quotepath=false status --porcelain=v1 -z --untracked-files=no` conflict detection, then emits one explicit gate boundary, allowed/forbidden next actions, and a single recommended next step. Use this before entering `pre_approval_gate` and when deciding whether a ready PR should request Copilot review, keep waiting, stay in feedback resolution, or stop for conflict resolution.
695
+
696
+ Required:
697
+ - `--repo <owner/name>`
698
+ - `--pr <number>`
699
+
700
+ Success output shape:
701
+ - `{ "ok": true, "repo": "owner/repo", "pr": 266, "currentHeadSha": "...", "mergeStateStatus": string|null, "conflictFiles": ["path"]|[], "lifecycleState": string, "loopDisposition": string, "gateBoundary": string, "draftGate": { ... }, "preApprovalGate": { ... }, "draftGateAlreadySatisfied": true, "allowedNextActions": [ ... ], "forbiddenActions": [ ... ], "nextAction": string, "reason": "..." }`
702
+ - `draftGate` / `preApprovalGate` report both latest visible evidence (`visible`, `headSha`, `verdict`, `findingsSummary`, `nextAction`) and whether the evidence is current-head + contract-complete (`currentHead`, `contractComplete`, `currentHeadClean`)
703
+ - `mergeStateStatus` preserves the current GitHub `gh pr view` signal in helper output even when the PR is not in the conflict boundary; `DIRTY` and explicit `CONFLICTING` inputs are treated as conflict-required states
704
+ - `conflictFiles` lists unmerged local paths from `git -c core.quotepath=false status --porcelain=v1 -z --untracked-files=no` when local conflict reconciliation is already in progress
705
+ - when `mergeStateStatus` is conflicted or `conflictFiles` is non-empty, the evaluator emits `gateBoundary=conflict_resolution`, `nextAction=resolve_merge_conflicts`, and forbids gate/approval/merge progression until reconciliation completes
706
+ - `draftGateAlreadySatisfied` — true when the draft→ready transition was already recorded (non-draft + clean evidence exists); callers must skip draft gate when this is true
707
+ - `forbiddenActions` includes `run_pre_approval_gate` whenever the post-draft review cycle has not yet settled for the current head, and conflicted PRs keep it forbidden until reconciliation is complete
708
+ - non-draft PRs do not need visible `draft_gate` evidence to progress through post-draft review or `pre_approval_gate`; `draftGateAlreadySatisfied` is informational only, and downstream legality comes from `gateBoundary`, `allowedNextActions`, and `forbiddenActions`
709
+ - if the PR head changes while gate/conflict facts are loading, the helper still fails closed rather than evaluating mixed-head evidence
710
+
711
+ Failure behavior:
712
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
713
+ - `gh` failures and malformed `gh` JSON emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
714
+
715
+ ### `scripts/github/detect-checkpoint-evidence.mjs`
716
+
717
+ Fetches the live PR head SHA plus visible PR issue comments, then summarizes the
718
+ latest valid `draft_gate` and `pre_approval_gate` checkpoint verdict comments.
719
+ Use this when a fresh session needs authoritative visible gate evidence for the
720
+ current head before running `gh pr ready` or declaring final-approval readiness.
721
+ When `PI_SUBAGENT_RUN_ID` is set to a non-empty value, it also enforces async runner ownership before reading GitHub facts, so stale/displaced runs fail closed before merge.
722
+
723
+ Required:
724
+ - `--repo <owner/name>`
725
+ - `--pr <number>`
726
+
727
+ Success output shape:
728
+ - `{ "ok": true, "repo": "owner/repo", "pr": 17, "currentHeadSha": "abc1234", "draftGate": { ... }, "preApprovalGate": { ... }, "draftGateMarker": { ... }, "preApprovalGateMarker": { ... } }`
729
+ - each gate summary includes `visible`, `headSha`, `verdict`, `findingsSummary`, `nextAction`, `commentId`, `commentUrl`, and `updatedAt`
730
+ - when no valid visible comment exists for a gate, its summary is emitted with `visible=false` and the other fields set to `null`
731
+ - each marker summary includes `visible`, `headSha`, `verdict`, `findingsSummary`, `nextAction`, `contractComplete`, `commentId`, `commentUrl`, and `updatedAt`
732
+ - marker summaries track the newest visible marker for the current head (`gate + currentHeadSha`) even if contract fields are partial, enabling same-head rerun idempotency without posting duplicate visible markers
733
+
734
+ Failure behavior:
735
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
736
+ - `gh` failures and malformed `gh` JSON emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
737
+
738
+ ### `scripts/loop/detect-tracker-pr-state.mjs`
739
+
740
+ Deterministic tracker-first story-to-PR state detector. Interprets a pre-built
741
+ tracker/PR lifecycle snapshot into one explicit current state, allowed next
742
+ transitions, a recommended next action, and the canonical reverse-sync action.
743
+ This helper is intentionally snapshot-only: tracker-adapter lookups and live
744
+ GitHub discovery remain outside this CLI.
745
+
746
+ Required:
747
+ - `--input <path>`
748
+
749
+ Snapshot schema (`--input` JSON):
750
+ - `trackerItemExists` {boolean} — whether a tracker work item was found
751
+ - `trackerItemId` {string|null} — tracker item identifier if present
752
+ - `prExists` {boolean} — whether a PR exists for the tracker item
753
+ - `prNumber` {number|null} — PR number if known; `prNumber` with `prExists=false` is contradictory and blocked
754
+ - `prDraft` {boolean} — whether the PR is still draft
755
+ - `prMerged` {boolean} — whether the PR has been merged
756
+ - `prClosed` {boolean} — whether the PR is closed on GitHub (merged PRs are also closed); `pr_closed_unmerged` is derived from `prClosed && !prMerged`
757
+
758
+ Unlike the Copilot/reviewer loop snapshots, this tracker snapshot uses `prClosed` for the raw GitHub closed state. Merged PRs therefore set both `prMerged=true` and `prClosed=true`, while `pr_closed_unmerged` is derived from `prClosed && !prMerged`.
759
+
760
+ This snapshot surface is intentionally limited to tracker identity plus PR lifecycle facts. It does not encode tracker-native workflow readiness/blocking/done state; higher-level callers must combine tracker-owned state separately when deciding whether opening a PR is appropriate.
761
+
762
+ Success output shape:
763
+ - `{ "ok": true, "snapshot": { ... }, "state": "...", "allowedTransitions": [...], "nextAction": "...", "reverseSyncAction": "..." }`
764
+
765
+ Failure behavior:
766
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }`
767
+ on stderr and exit non-zero
768
+ - unreadable input files, invalid JSON, and invalid snapshot objects emit
769
+ `{ "ok": false, "error": "..." }` on stderr and exit non-zero
770
+
771
+ ### `scripts/loop/detect-reviewer-loop-state.mjs`
772
+
773
+ Deterministic reviewer-loop state detector. Captures reviewer-side PR loop state from observable
774
+ GitHub facts plus optional local reviewer-loop metadata and interprets that snapshot into one
775
+ explicit current state, allowed next transitions, and a recommended next action. See
776
+ [Reviewer Loop State Graph](../docs/reviewer-loop-state-graph.md) for the full reviewer-loop state graph and contracts.
777
+
778
+ Two modes:
779
+
780
+ - **Auto-detect**: `--repo <owner/name> --pr <number>`
781
+ Fetches PR/open-head state, review-request status, and pending/submitted review surfaces from
782
+ GitHub and interprets them into deterministic reviewer-loop state. When `--reviewer-login` is
783
+ omitted, this uses aggregate all-reviewer scope for the PR.
784
+
785
+ - **Snapshot interpretation**: `--input <path>`
786
+ Reads a pre-built snapshot JSON and interprets it without any `gh` calls.
787
+
788
+ Optional (auto-detect mode only):
789
+ - `--reviewer-login <login>`
790
+ Scope review-request and review-surface detection to a single reviewer identity. Success output
791
+ snapshots include `reviewerScope` (`"all_reviewers"` or `"single_reviewer"`) plus
792
+ `reviewerLogin` (`string|null`) so callers can tell whether the detector used aggregate or
793
+ single-reviewer scope.
794
+ - `--review-requested <true|false>`
795
+ Override review-request detection with a known prior result.
796
+ - `--local-state <path>`
797
+ Inject local reviewer-loop metadata (planning/run/merge/draft-notification status) used for
798
+ deterministic planning/running/merge-ready and draft lifecycle transitions.
799
+
800
+ Success output shape:
801
+ - `{ "ok": true, "snapshot": { ... }, "state": "...", "allowedTransitions": [...], "nextAction": "..." }`
802
+ - reviewer snapshots preserve the latest submitted review metadata via `submittedReviewPresent`, `submittedReviewCommitSha`, and `submittedReviewState`
803
+ - together those fields let read-only inspection UIs distinguish submitted-verdict handoff boundaries from active reviewer-pass states
804
+
805
+ Failure behavior:
806
+ - malformed arguments, unexpected `gh` failures, and invalid input/local-state JSON emit
807
+ `{ "ok": false, "error": "..." }` on stderr and exit non-zero
808
+
809
+ ### `scripts/loop/outer-loop.mjs`
810
+
811
+ Thin deterministic outer-loop wrapper for the Copilot PR remediation path. It combines
812
+ the existing Copilot and reviewer inner-loop detectors into one machine-readable outer
813
+ action so bounded external waits remain owned by the same remediation family instead of
814
+ looking like terminal run endpoints.
815
+
816
+ Required:
817
+ - `--repo <owner/name>`
818
+ - `--pr <number>`
819
+
820
+ Optional:
821
+ - `--reviewer-login <login>`
822
+ - `--checkpoint-dir <path>`
823
+ - `--copilot-input <path>`
824
+ - `--reviewer-input <path>`
825
+
826
+ Reviewer-scope contract:
827
+ - omitting `--reviewer-login` means aggregate all-reviewer scope for the PR
828
+ - providing `--reviewer-login` means single-reviewer scope for that login
829
+ - `--reviewer-input` cannot be combined with `--reviewer-login`
830
+
831
+ Contract:
832
+ - auto-detect mode calls both inner detectors, interprets their current states, and emits one
833
+ outer action: `continue_wait`, `reenter_copilot_loop`, `reenter_reviewer_loop`, `stop`, or `done`
834
+ - treats draft PRs as a re-entry point into owned draft-stage follow-up rather than a terminal stop
835
+ - treats `waiting_for_copilot_review`, `waiting_for_ci`, and reviewer `submitted_review`
836
+ as outer-loop-owned `continue_wait` states at explicit external/handoff boundaries
837
+ - preserves compatibility for reviewer `waiting_for_author_followup` and `waiting_for_re_request`
838
+ as legacy named external-wait boundaries
839
+ - when the next step needs local execution or mutation and the checkout is dirty or detached, preserves the loop-family handoff and marks `conductorRouting.handoffEnvelope.requiresLocalIsolation=true` so callers can continue from an isolated checkout/worktree instead of treating the boundary as terminal
840
+ - for PR-local re-entry actions, verifies local branch/HEAD identity against the active PR head;
841
+ when an isolation-managed handoff is already in effect, it enriches the handoff with `headRefName` / `headRefOid` for the target PR head instead of failing the handoff on the parent checkout's expected mismatch
842
+ - otherwise stops with `unsafe_local_branch_mismatch_requires_reconcile` or
843
+ `unsafe_local_head_mismatch_requires_reconcile` when checkout identity is not aligned
844
+ - when that PR-local identity gate trips, the emitted `conductorRouting` result is also fail-closed
845
+ to a stop outcome with no handoff entrypoint, so consumers cannot keep following a stale handoff envelope
846
+ - persists bounded checkpoint state to `tmp/copilot-loop/<owner>/<repo>/pr-<n>/outer-loop-state.json` for
847
+ async continuation and false-positive wakeup detection
848
+ - emits an additive `conductorRouting` field with the conductor-owned routing outcome, derived
849
+ outer action, stop reason when relevant, and any machine-readable handoff envelope
850
+ - supports snapshot-input mode for deterministic gh-free testing
851
+
852
+ Success output shape:
853
+ - `{ "ok": true, "outerAction": "...", "copilotState": "...", "reviewerState": "...", "reviewerScope": { "mode": "all_reviewers"|"single_reviewer", "reviewerLogin": "..."|null }, "reason"?: "...", "branchIdentity"?: { ... }, "conductorRouting": { "routingOutcome": "...", "outerAction": "...", "stopReason": null|"...", "handoffEnvelope": { ... } }, "checkpoint": { ... } }`
854
+
855
+ Failure behavior:
856
+ - malformed arguments emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
857
+ - unexpected `gh` or `git` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
858
+
859
+ ### `scripts/loop/inspect-run.mjs`
860
+
861
+ Read-only inspection entrypoint for one explicit Copilot PR outer-loop target.
862
+ It composes current inner-loop facts into one JSON snapshot without attaching to
863
+ an active worker or mutating local/runtime state.
864
+
865
+ Required:
866
+ - `--repo <owner/name>`
867
+ - `--pr <number>`
868
+
869
+ Optional:
870
+ - `--steering-state-file <path>`
871
+ - `--reviewer-login <login>` — narrows live reviewer detection to one reviewer identity; when omitted, inspection uses aggregate all-reviewer scope for the PR
872
+ - `--copilot-input <path>`
873
+ - `--reviewer-input <path>` — cannot be combined with `--reviewer-login`
874
+
875
+ Contract:
876
+ - is strictly read-only: it does not write checkpoints, mutate GitHub state, or create local artifacts
877
+ - returns a stable top-level inspection shape with target identity, derived `runId`, authoritative `outerState`, conditional top-level `allowedTransitions`, compatibility `outerAction`, active family state,
878
+ status class, trust/source semantics, evidence, markers, and best-effort drill-down layers
879
+ - only derives top-level `outerState` / `allowedTransitions` / `outerAction` / `activeFamilyState` / `statusClass` when inspection has a complete current inner-loop picture, whether from live detectors and/or caller-supplied snapshot inputs
880
+ - when inspection falls back to checkpoint-only data or mixed live + checkpoint evidence, checkpoint-backed drill-down layers and checkpoint evidence paths remain available as advisory context while the top-level state stays `"unknown"`
881
+ - reports not-found or unavailable targets as structured success output with `statusClass: "unknown"`
882
+ rather than by throwing a synthetic blocked-run error
883
+ - looks for checkpoints at the repo-qualified default path `tmp/copilot-loop/<owner>/<repo>/pr-<n>/outer-loop-state.json`
884
+ - during transition, may read the legacy default path `tmp/copilot-loop/pr-<n>/outer-loop-state.json` only when the checkpoint file's embedded `repo` and `pr` match the explicit target
885
+ - surfaces steering as a best-effort drill-down layer when `--steering-state-file` is provided,
886
+ including latest acknowledgement plus queued/effective stop summaries for the current run,
887
+ without exposing full steering history/detail or raw steering-file locator paths
888
+ - when live GitHub PR facts are available, surfaces a deterministic `loopIterations` summary for the
889
+ remote Copilot review/fix loop (completed rounds, pending round indicator, Copilot review comments,
890
+ current resolved/unresolved review-thread counts, and fix commits after feedback)
891
+ - keeps `loopIterations` unavailable in snapshot-only / non-live inspection paths instead of
892
+ inventing local phase-loop iteration semantics
893
+ - rejects mismatched steering-state files from the targeted repo/pr instead of projecting their state onto the inspected run
894
+
895
+ Success output shape:
896
+ - `{ "ok": true, "schemaVersion": 1, "target": { "repo": "...", "pr": 17 }, "runId": "pr-17", "inspectedAt": "...", "activeStateFamily": "copilot-pr-outer-loop", "outerState": "...", "allowedTransitions"?: [...], "outerAction": "...", "activeFamilyState": "...", "statusClass": "...", "needsAttention": false, "sourceMode": "...", "trust": "...", "evidence": { ... }, "markers": { ... }, "loopIterations": { "available": true|false, ... }, "layers": { "reviewer": { "currentState": "...", "scope": { "mode": "all_reviewers"|"single_reviewer", "reviewerLogin": "..."|null }, ... }, ... } }`
897
+
898
+ Failure behavior:
899
+ - malformed arguments emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
900
+ - unexpected runtime failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
901
+
902
+ ### `scripts/loop/inspect-run-viewer.mjs`
903
+
904
+ Owned read-only local/operator inspection dashboard layered on `inspect-run`.
905
+ `inspect-run` remains authoritative for inspection/status state; the viewer owns local inbox discovery and read-only presentation/prioritization.
906
+
907
+ Primary local lifecycle UX now lives in the Pi extension under:
908
+ - `/dev-loops inspect open [--repo <owner/name>]`
909
+ - `/dev-loops inspect resume [--repo <owner/name>]`
910
+ - `/dev-loops inspect status [--repo <owner/name>]`
911
+ - `/dev-loops inspect stop [--repo <owner/name>]`
912
+ - `/dev-loops inspect restart [--repo <owner/name>]`
913
+
914
+ The extension-managed seam stores one narrow repo-local managed-instance record at:
915
+ - `.pi/ui-servers/inspect-run-viewer.json`
916
+
917
+ Ownership split for this slice:
918
+ - extension owns lifecycle UX, URL discovery, liveness checks, reattach logic, stop/restart, and best-effort browser opening
919
+ - when a repo-scoped command reuses an inbox-first managed viewer, the surfaced URL may include `?scope=<owner/name>` to pre-scope the inbox without replacing the managed instance
920
+ - viewer script still owns HTTP server behavior, rendering, inbox/query behavior, and snapshot loading
921
+
922
+ Optional:
923
+ - `--repo <owner/name>` (repo-scope the inbox; otherwise the viewer starts in inbox-first mode)
924
+ - `--host <host>` (default: `127.0.0.1`; non-loopback binds require `--allow-non-localhost`)
925
+ - `--port <port>` (default: `4311`)
926
+ - `--allow-non-localhost` (explicit opt-in for non-loopback binds such as `0.0.0.0` or LAN IPs)
927
+ - `--restart` (manual/debug convenience only; requires `lsof` / POSIX support and sends `SIGTERM` to every listener already bound to that port)
928
+ - `--steering-state-file <path>` (pass-through to `inspect-run`)
929
+ - `--reviewer-login <login>` (pass-through to `inspect-run`)
930
+ - `--copilot-input <path>` (pass-through to `inspect-run`)
931
+ - `--reviewer-input <path>` (pass-through to `inspect-run`; cannot be combined with `--reviewer-login`)
932
+
933
+ Contract:
934
+ - current-slice posture: kept/promoted as an explicitly owned local/operator inspection dashboard (not a second public workflow entrypoint)
935
+ - read-only: no GitHub mutations, no checkpoint writes, no steering writes, no worker attachment
936
+ - ownership boundary: `inspect-run` owns authoritative inspection/status state; viewer owns local inbox discovery plus read-only operator presentation/prioritization
937
+ - extension-managed lifecycle remains loopback-first and local-only; no remote/public hosting support
938
+ - the script fallback still requires explicit `--allow-non-localhost` opt-in for non-loopback binds; do not expose inspection state on the network by default
939
+ - GitHub-first launch boundary: repo scope is optional and PR selection happens through the viewer URL/query state, not a CLI `--pr` flag
940
+ - uses one adapter module (`scripts/loop/_inspect-run-viewer-adapter.mjs`) to load the normalized inspection snapshot
941
+ - adapter is the only viewer integration seam that calls the existing `inspect-run` contract in this source-loaded workspace
942
+ - serves two explicit read-only endpoints:
943
+ - `/` → operator-facing HTML with an assigned-PR inbox shell and, when a PR is selected via URL or sidebar, the Mermaid-first graph plus current-PR-state banner and supporting textual summary/evidence
944
+ - `/snapshot.json` → the full authoritative inspection snapshot JSON for the currently selected PR/query target
945
+ - HTML includes a visible link to `/snapshot.json` so machine-readable state no longer depends on an inline full-snapshot dump in the page itself
946
+ - `/snapshot.json` returns `application/json; charset=utf-8` on success and deterministic JSON error output with non-2xx status when snapshot loading throws or yields no snapshot
947
+ - unsupported paths return deterministic `404` without loading a snapshot (even for unsupported methods on unknown paths); `/favicon.ico` returns deterministic `204`; unsupported methods on supported routes return `405 Allow: GET`
948
+ - both primary endpoints send `Cache-Control: no-store` to match the manual-reload workflow
949
+ - the script-local `--restart` flag remains a manual/debug fallback only; the extension-managed path must not depend on killing unknown listeners
950
+ - manual reload only (`window.location.reload()`); no polling/watch/timeout/control semantics
951
+
952
+ Local manual verification path:
953
+ 1. Preferred extension-managed path:
954
+ - `/dev-loops inspect open`
955
+ - `/dev-loops inspect status`
956
+ - `/dev-loops inspect resume`
957
+ - `/dev-loops inspect stop`
958
+ 2. Script fallback for manual/debug verification:
959
+ - `node scripts/loop/inspect-run-viewer.mjs`
960
+ - `node scripts/loop/inspect-run-viewer.mjs --repo <owner/name>`
961
+ 3. Open the printed/resolved URL in a local browser and verify the human-oriented `/` page
962
+ 4. Select a PR via the sidebar or by adding `?repo=<owner/name>&pr=<number>` to the viewer URL
963
+ 5. Open `/snapshot.json` for that selected/query-targeted PR and verify it returns the matching full inspection snapshot JSON
964
+ 6. Use browser refresh or the reload button for point-in-time re-inspection
965
+
966
+ Local WebKit/Playwright smoke path:
967
+ 1. Install the Safari/WebKit browser runtime once:
968
+ - `npx playwright install webkit`
969
+ 2. Run the viewer smoke suite:
970
+ - `npm run test:playwright:viewer`
971
+ 3. Review screenshots/traces under `test-results/` and the HTML report under `playwright-report/ui-smoke/inspect-run-viewer/`
972
+ 4. Optionally hit `/favicon.ico` or an unsupported path to confirm those paths stay deterministic and do not perform snapshot rendering
973
+ 5. For deterministic/local test mode, pass `--copilot-input` and `--reviewer-input` fixtures to viewer; these are forwarded to `inspect-run`
974
+
975
+ ### `scripts/loop/steer-loop.mjs`
976
+
977
+ Mid-flight operator steering CLI for active dev loops.
978
+
979
+ Subcommands:
980
+ - `submit` — submit a steering directive to a specific run
981
+ - `promote` — explicitly promote queued steering for a specific run at a known loop state
982
+ - `status` — inspect the current steering state for a run
983
+
984
+ Contract:
985
+ - persists steering state to a JSON file (default: `.pi/steering/<owner>/<repo>/pr-<n>.json` for operator-facing `--repo/--pr` mode; `.pi/steering/<run-id>.json` for low-level `--run-id` mode)
986
+ - operator-facing `submit` resolves one explicit `repo` + `pr` target through the read-only
987
+ inspection surface and derives `runId: pr-<number>` from that target while persisting repo-qualified target metadata alongside the steering state
988
+ - explicit queued-steering promotion/persistence belongs to `promote`; detector-shaped helpers stay read-only
989
+ - operator-facing `submit` is intentionally limited to `stop_at_next_safe_gate`; other directive
990
+ kinds remain low-level/internal and are rejected on the external submit path
991
+ - operator-facing `submit` fails closed when inspection is partial, checkpoint-only, unavailable,
992
+ stale, or conflicting
993
+ - low-level/testing mode may still accept injected loop-state inputs for deterministic tests
994
+ - returns deterministic acknowledgement/result payloads for `submit` and deterministic state
995
+ readback for `status`
996
+ - rejected operator-facing submits leave any trusted durable steering file unchanged; when the
997
+ persisted file is malformed or target-mismatched, the response may include a fresh synthetic
998
+ target-scoped `steeringState` for deterministic readback without trusting broken persisted data
999
+
1000
+ Success output shape:
1001
+ - `submit`: `{ "ok": true, "acknowledgement": { ... }, "result": { ... }, "steeringState": { ... } }`
1002
+ - `promote`: `{ "ok": true, "promotedCount": <n>, "promoted": [ ... ], "steeringState": { ... } }`
1003
+ - `status`: `{ "ok": true, "status": { ... } }`
1004
+
1005
+ Failure behavior:
1006
+ - argument/usage errors emit `{ "ok": false, "error": "...", "usage": "..." }` on stderr and exit non-zero
1007
+ - runtime failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero