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.
- package/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp } from "../_core-helpers.mjs";
|
|
3
|
+
import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadDevLoopConfig, resolveRefinement } from "@dev-loops/core/config";
|
|
7
|
+
import { autoDetectSnapshot } from "./detect-copilot-loop-state.mjs";
|
|
8
|
+
import { performCopilotReviewRequest } from "../github/request-copilot-review.mjs";
|
|
9
|
+
import { detectInternalOnly as detectPrInternalOnly } from "./detect-internal-only-pr.mjs";
|
|
10
|
+
import { applyConfirmedReviewRequest, interpretLoopState, NEXT_ACTIONS, STATE, summarizeLoopInterpretation, TRANSITIONS } from "@dev-loops/core/loop/copilot-loop-state";
|
|
11
|
+
import { ensureAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
|
|
16
|
+
enforceExternalHealthyWaitTimeout,
|
|
17
|
+
} from "@dev-loops/core/loop/timeout-policy";
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
20
|
+
COPILOT_REVIEW_WAIT_TIMEOUT_MS,
|
|
21
|
+
} from "@dev-loops/core/loop/policy-constants";
|
|
22
|
+
const VALID_WATCH_STATUSES = new Set(["changed", "timeout", "idle"]);
|
|
23
|
+
const REMOVED_FLAGS = new Set([
|
|
24
|
+
"--force-rerequest-review",
|
|
25
|
+
]);
|
|
26
|
+
const USAGE = `Usage: copilot-pr-handoff.mjs --pr <number> [--repo <owner/name>] [--watch-status <changed|timeout|idle>]
|
|
27
|
+
Detect the Copilot-loop state for a PR, request Copilot review only when
|
|
28
|
+
a new request is still needed, and emit the recommended next action with
|
|
29
|
+
exact parameters.
|
|
30
|
+
Required:
|
|
31
|
+
--pr <number> Pull request number
|
|
32
|
+
Optional:
|
|
33
|
+
--repo <owner/name> Repository slug (e.g. owner/repo). Auto-detected from git remote when omitted.
|
|
34
|
+
--watch-status <status> Refresh deterministic loop state after a prior
|
|
35
|
+
watcher result (changed|timeout|idle). This mode
|
|
36
|
+
never requests review; it only re-detects state.
|
|
37
|
+
Output (stdout, JSON):
|
|
38
|
+
{ "ok": true, "action": "watch"|"fix"|"stop", "state": "...",
|
|
39
|
+
"allowedTransitions": [...], "nextAction": "...", "snapshot": {...},
|
|
40
|
+
"reviewRequestStatus"?: "...", "watchStatus"?: "...",
|
|
41
|
+
"autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false,
|
|
42
|
+
"roundCapCleanEligible": true|false, "loopDisposition": "...", "terminal": true|false,
|
|
43
|
+
"requestWatchContract": {
|
|
44
|
+
"action": "watch"|"fix"|"stop",
|
|
45
|
+
"nextAction": "...",
|
|
46
|
+
"requestStatus": "requested"|"already-requested"|"unavailable"|"failed"|"none",
|
|
47
|
+
"routingState": "copilot_request_confirmed_waiting"|"ready_state_needs_copilot_request"|"draft_reset_requires_ready_state_reentry"|"non_ready_state",
|
|
48
|
+
"watchEntryConfirmed": true|false,
|
|
49
|
+
"watchArgs": { ... }|null,
|
|
50
|
+
"stopState"?: "unavailable"|"blocked"|"draft_requires_ready_state_reentry"|"no_automatic_next_step"
|
|
51
|
+
},
|
|
52
|
+
"watchTimeoutPolicy"?: { "classification": "...", "minimumTimeoutMs": N, "defaultTimeoutMs": N },
|
|
53
|
+
"watchArgs"?: { "repo": "...", "pr": N, "pollIntervalMs": N, "timeoutMs": N } }
|
|
54
|
+
Actions:
|
|
55
|
+
watch Copilot review was requested; use watchArgs with probe-copilot-review.mjs
|
|
56
|
+
fix Unresolved feedback exists; address it before re-requesting review
|
|
57
|
+
stop No automatic next step; report the current state (terminal, blocked, or operator-decision-required) and do not proceed
|
|
58
|
+
Watch refresh rule:
|
|
59
|
+
watcher timeout/idle is observational only. Re-run this helper with
|
|
60
|
+
--watch-status and stop only when terminal=true. Pending or unresolved
|
|
61
|
+
states remain non-terminal even after a timeout.
|
|
62
|
+
Watch defaults:
|
|
63
|
+
pollIntervalMs 60000 (1 minute)
|
|
64
|
+
timeoutMs 1800000 (30 minutes)
|
|
65
|
+
Error output (stderr, JSON):
|
|
66
|
+
Argument/usage errors:
|
|
67
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
68
|
+
gh/runtime failures:
|
|
69
|
+
{ "ok": false, "error": "..." }
|
|
70
|
+
Exit codes:
|
|
71
|
+
0 Success
|
|
72
|
+
1 Argument error or gh failure`.trim();
|
|
73
|
+
const WATCH_STATES = new Set([
|
|
74
|
+
STATE.WAITING_FOR_COPILOT_REVIEW,
|
|
75
|
+
]);
|
|
76
|
+
const FIX_STATES = new Set([
|
|
77
|
+
STATE.UNRESOLVED_FEEDBACK_PRESENT,
|
|
78
|
+
STATE.ALREADY_FIXED_NEEDS_REPLY_RESOLVE,
|
|
79
|
+
STATE.INTERNAL_TOOLING_DIRECT_GATE,
|
|
80
|
+
]);
|
|
81
|
+
function summarizeRequestWatchContract({
|
|
82
|
+
interpretation,
|
|
83
|
+
action,
|
|
84
|
+
requestStatus,
|
|
85
|
+
watchArgs,
|
|
86
|
+
}) {
|
|
87
|
+
let routingState = "non_ready_state";
|
|
88
|
+
if (action === "watch" && (requestStatus === "requested" || requestStatus === "already-requested")) {
|
|
89
|
+
routingState = "copilot_request_confirmed_waiting";
|
|
90
|
+
} else if (interpretation.state === STATE.PR_DRAFT) {
|
|
91
|
+
routingState = "draft_reset_requires_ready_state_reentry";
|
|
92
|
+
} else if (
|
|
93
|
+
interpretation.state === STATE.PR_READY_NO_FEEDBACK
|
|
94
|
+
|| interpretation.state === STATE.READY_TO_REREQUEST_REVIEW
|
|
95
|
+
&& interpretation.sameHeadCleanConverged !== true
|
|
96
|
+
) {
|
|
97
|
+
routingState = "ready_state_needs_copilot_request";
|
|
98
|
+
} else if (interpretation.state === STATE.INTERNAL_TOOLING_DIRECT_GATE) {
|
|
99
|
+
routingState = "internal_tooling_skip_copilot";
|
|
100
|
+
}
|
|
101
|
+
let stopState;
|
|
102
|
+
if (action === "stop") {
|
|
103
|
+
if (interpretation.state === STATE.REVIEW_REQUEST_UNAVAILABLE) {
|
|
104
|
+
stopState = "unavailable";
|
|
105
|
+
} else if (interpretation.state === STATE.BLOCKED_NEEDS_USER_DECISION) {
|
|
106
|
+
stopState = "blocked";
|
|
107
|
+
} else if (interpretation.state === STATE.PR_DRAFT) {
|
|
108
|
+
stopState = "draft_requires_ready_state_reentry";
|
|
109
|
+
} else {
|
|
110
|
+
stopState = "no_automatic_next_step";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
action,
|
|
115
|
+
nextAction: interpretation.nextAction,
|
|
116
|
+
requestStatus,
|
|
117
|
+
routingState,
|
|
118
|
+
watchEntryConfirmed: action === "watch" && watchArgs !== undefined,
|
|
119
|
+
watchArgs: watchArgs ?? null,
|
|
120
|
+
stopState,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const parseError = buildParseError(USAGE);
|
|
124
|
+
function rejectRemovedFlag(token) {
|
|
125
|
+
throw parseError(
|
|
126
|
+
`${token} has been removed. Copilot re-requests are managed internally. Omit the flag.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
|
|
130
|
+
const args = [...argv];
|
|
131
|
+
const options = {
|
|
132
|
+
help: false,
|
|
133
|
+
repo: undefined,
|
|
134
|
+
pr: undefined,
|
|
135
|
+
watchStatus: undefined,
|
|
136
|
+
};
|
|
137
|
+
while (args.length > 0) {
|
|
138
|
+
const token = args.shift();
|
|
139
|
+
if (token === "--help" || token === "-h") {
|
|
140
|
+
options.help = true;
|
|
141
|
+
return options;
|
|
142
|
+
}
|
|
143
|
+
if (REMOVED_FLAGS.has(token)) {
|
|
144
|
+
rejectRemovedFlag(token);
|
|
145
|
+
}
|
|
146
|
+
if (token === "--repo") {
|
|
147
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (token === "--pr") {
|
|
151
|
+
options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (token === "--watch-status") {
|
|
155
|
+
const watchStatus = requireOptionValue(args, "--watch-status", parseError).trim().toLowerCase();
|
|
156
|
+
if (!VALID_WATCH_STATUSES.has(watchStatus)) {
|
|
157
|
+
throw parseError(`--watch-status must be one of: ${[...VALID_WATCH_STATUSES].join(", ")}`);
|
|
158
|
+
}
|
|
159
|
+
options.watchStatus = watchStatus;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
163
|
+
}
|
|
164
|
+
if (options.pr === undefined) {
|
|
165
|
+
throw parseError("copilot-pr-handoff requires --pr <number>");
|
|
166
|
+
}
|
|
167
|
+
if (options.repo === undefined) {
|
|
168
|
+
options.repo = detectRepoSlug(cwd);
|
|
169
|
+
if (!options.repo) {
|
|
170
|
+
throw parseError(
|
|
171
|
+
"Repo auto-detection failed. " +
|
|
172
|
+
"Run from a git repo checkout or provide --repo <owner/name>."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
parseRepoSlug(options.repo);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
180
|
+
}
|
|
181
|
+
return options;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Detect recent human (non-bot) comments on a PR since the last subagent action.
|
|
185
|
+
* Determines "last subagent action" by finding the most recent bot/Copilot comment
|
|
186
|
+
* on the PR issue comments. If a human comment exists after that timestamp, the
|
|
187
|
+
* loop should pause for operator review.
|
|
188
|
+
* Returns { paused: true, humanComments: [...] } when human comments need attention.
|
|
189
|
+
*/
|
|
190
|
+
export async function detectRecentHumanComments({ repo, pr, claimedAtMs }, { env = process.env, ghCommand = "gh" } = {}) {
|
|
191
|
+
try {
|
|
192
|
+
const result = await runChild(
|
|
193
|
+
ghCommand,
|
|
194
|
+
["api", `repos/${repo}/issues/${pr}/comments`, "--paginate", "--jq", ".[]"],
|
|
195
|
+
env,
|
|
196
|
+
);
|
|
197
|
+
if (result.code !== 0) {
|
|
198
|
+
return { paused: false, error: "comment_fetch_failed", detail: "Failed to fetch PR comments; human comment detection unavailable." };
|
|
199
|
+
}
|
|
200
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
201
|
+
if (lines.length === 0) {
|
|
202
|
+
return { paused: false };
|
|
203
|
+
}
|
|
204
|
+
let comments;
|
|
205
|
+
try {
|
|
206
|
+
comments = lines.map((line) => JSON.parse(line));
|
|
207
|
+
} catch {
|
|
208
|
+
return { paused: false, error: "comment_parse_failed", detail: "Failed to parse PR comments JSON; human comment detection unavailable." };
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(comments)) {
|
|
211
|
+
return { paused: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Find the most recent bot/Copilot comment timestamp (last subagent action)
|
|
215
|
+
let lastBotActionMs = null;
|
|
216
|
+
for (const comment of comments) {
|
|
217
|
+
const authorLogin = comment?.user?.login ?? "";
|
|
218
|
+
if (!isCopilotLogin(authorLogin)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const createdAt = normalizeTimestamp(comment?.created_at);
|
|
222
|
+
if (createdAt !== null && (lastBotActionMs === null || createdAt > lastBotActionMs)) {
|
|
223
|
+
lastBotActionMs = createdAt;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If no Copilot comments found, use the run's claim time as baseline when available
|
|
228
|
+
if (lastBotActionMs === null) {
|
|
229
|
+
if (typeof claimedAtMs === "number" && claimedAtMs > 0) {
|
|
230
|
+
lastBotActionMs = claimedAtMs;
|
|
231
|
+
} else {
|
|
232
|
+
return { paused: false };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find human comments after the last bot action
|
|
237
|
+
const humanComments = [];
|
|
238
|
+
for (const comment of comments) {
|
|
239
|
+
const authorLogin = comment?.user?.login ?? "";
|
|
240
|
+
const authorType = comment?.user?.type ?? "";
|
|
241
|
+
// Skip bot authors (GitHub type "Bot") and Copilot
|
|
242
|
+
if (authorType === "Bot" || isCopilotLogin(authorLogin)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Skip if comment body is a gate verdict comment (system action, not operator input)
|
|
246
|
+
const body = typeof comment?.body === "string" ? comment.body : "";
|
|
247
|
+
if (body.includes("Gate review:") || body.includes("**draft_gate**") || body.includes("**pre_approval_gate**")) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const createdAt = normalizeTimestamp(comment?.created_at);
|
|
251
|
+
if (createdAt !== null && createdAt > lastBotActionMs) {
|
|
252
|
+
humanComments.push({
|
|
253
|
+
id: comment.id,
|
|
254
|
+
author: authorLogin,
|
|
255
|
+
createdAt: comment.created_at,
|
|
256
|
+
bodyPreview: body.slice(0, 200),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
paused: humanComments.length > 0,
|
|
263
|
+
humanComments: humanComments.length > 0 ? humanComments : undefined,
|
|
264
|
+
lastBotCommentAt: lastBotActionMs !== null ? new Date(lastBotActionMs).toISOString() : undefined,
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return { paused: false, error: "unexpected_error", detail: "Unexpected error during human comment detection." };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function runHandoff(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
272
|
+
const runnerOwnership = await ensureAsyncRunnerOwnership({
|
|
273
|
+
repo: options.repo,
|
|
274
|
+
pr: options.pr,
|
|
275
|
+
env,
|
|
276
|
+
cwd: path.resolve(process.cwd()),
|
|
277
|
+
claimIfMissing: true,
|
|
278
|
+
});
|
|
279
|
+
if (!runnerOwnership.ok) {
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
action: "stop",
|
|
283
|
+
state: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
284
|
+
allowedTransitions: [],
|
|
285
|
+
nextAction: runnerOwnership.message,
|
|
286
|
+
autoRerequestEligible: false,
|
|
287
|
+
sameHeadCleanConverged: false,
|
|
288
|
+
roundCapCleanEligible: false,
|
|
289
|
+
loopDisposition: "blocked",
|
|
290
|
+
terminal: true,
|
|
291
|
+
snapshot: { repo: options.repo, pr: options.pr },
|
|
292
|
+
runnerOwnership,
|
|
293
|
+
requestWatchContract: {
|
|
294
|
+
action: "stop",
|
|
295
|
+
nextAction: runnerOwnership.message,
|
|
296
|
+
requestStatus: "none",
|
|
297
|
+
routingState: "non_ready_state",
|
|
298
|
+
watchEntryConfirmed: false,
|
|
299
|
+
watchArgs: null,
|
|
300
|
+
stopState: "blocked",
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
let snapshot = await autoDetectSnapshot(
|
|
305
|
+
{ repo: options.repo, pr: options.pr },
|
|
306
|
+
{ env, ghCommand },
|
|
307
|
+
);
|
|
308
|
+
const config = await loadDevLoopConfig({ repoRoot: path.resolve(process.cwd()) });
|
|
309
|
+
if (config.errors?.length > 0) {
|
|
310
|
+
console.error("[copilot-pr-handoff] config warnings:", JSON.stringify(config.errors));
|
|
311
|
+
}
|
|
312
|
+
const refinementConfig = config.errors?.length > 0
|
|
313
|
+
? resolveRefinement({ version: 1 })
|
|
314
|
+
: resolveRefinement(config.config);
|
|
315
|
+
let interpretation = interpretLoopState(snapshot, refinementConfig);
|
|
316
|
+
|
|
317
|
+
// Check for human comments since last subagent action
|
|
318
|
+
// Only active in async subagent context (PI_SUBAGENT_RUN_ID set)
|
|
319
|
+
let humanCommentCheck = { paused: false };
|
|
320
|
+
if (env.PI_SUBAGENT_RUN_ID) {
|
|
321
|
+
humanCommentCheck = await detectRecentHumanComments(
|
|
322
|
+
{ repo: options.repo, pr: options.pr, claimedAtMs: runnerOwnership?.activeRun?.claimedAt ? new Date(runnerOwnership.activeRun.claimedAt).getTime() : undefined },
|
|
323
|
+
{ env, ghCommand },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const TERMINAL_STATES = new Set([STATE.NO_PR, STATE.DONE, STATE.BLOCKED_NEEDS_USER_DECISION]);
|
|
327
|
+
const humanCommentUnavailable = humanCommentCheck.error && !humanCommentCheck.paused;
|
|
328
|
+
if ((humanCommentCheck.paused || humanCommentUnavailable) && !TERMINAL_STATES.has(interpretation.state)) {
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
action: "stop",
|
|
332
|
+
state: STATE.BLOCKED_NEEDS_USER_DECISION,
|
|
333
|
+
allowedTransitions: [],
|
|
334
|
+
nextAction: humanCommentCheck.paused
|
|
335
|
+
? "Human comment detected on PR since last subagent action; review the comment(s) before continuing the automated loop."
|
|
336
|
+
: `Human comment detection unavailable (${humanCommentCheck.error}); review PR comments manually before continuing.`,
|
|
337
|
+
autoRerequestEligible: false,
|
|
338
|
+
sameHeadCleanConverged: false,
|
|
339
|
+
roundCapCleanEligible: false,
|
|
340
|
+
loopDisposition: "blocked",
|
|
341
|
+
terminal: true,
|
|
342
|
+
snapshot,
|
|
343
|
+
runnerOwnership,
|
|
344
|
+
humanCommentPause: {
|
|
345
|
+
reason: humanCommentCheck.paused ? "human_comment_detected" : "human_comment_check_unavailable",
|
|
346
|
+
error: humanCommentCheck.error || undefined,
|
|
347
|
+
humanComments: humanCommentCheck.humanComments,
|
|
348
|
+
lastBotCommentAt: humanCommentCheck.lastBotCommentAt,
|
|
349
|
+
},
|
|
350
|
+
requestWatchContract: {
|
|
351
|
+
action: "stop",
|
|
352
|
+
nextAction: humanCommentCheck.paused
|
|
353
|
+
? "Human comment detected on PR since last subagent action; review the comment(s) before continuing the automated loop."
|
|
354
|
+
: `Human comment detection unavailable (${humanCommentCheck.error}); review PR comments manually before continuing.`,
|
|
355
|
+
requestStatus: "none",
|
|
356
|
+
routingState: "non_ready_state",
|
|
357
|
+
watchEntryConfirmed: false,
|
|
358
|
+
watchArgs: null,
|
|
359
|
+
stopState: "blocked",
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
// Detect internal tooling PRs — suppress Copilot review request step entirely.
|
|
366
|
+
// Internal-only PRs (scripts/docs/tests/config) skip the request, not just the wait.
|
|
367
|
+
let internalOnlySkipCopilot = false;
|
|
368
|
+
// Skip internal detection in sequential stub/test mode to avoid consuming stub entries.
|
|
369
|
+
// Claims-mode stubs handle interleaved calls; detection runs normally.
|
|
370
|
+
if (!env.GH_SEQUENCE_PATH || env.GH_STUB_MODE === "claims") {
|
|
371
|
+
if (options.watchStatus === undefined &&
|
|
372
|
+
(interpretation.state === STATE.PR_READY_NO_FEEDBACK ||
|
|
373
|
+
interpretation.state === STATE.READY_TO_REREQUEST_REVIEW)) {
|
|
374
|
+
try {
|
|
375
|
+
const internalCheck = await detectPrInternalOnly(options, { env, ghCommand });
|
|
376
|
+
if (internalCheck.ok && internalCheck.internalOnly) {
|
|
377
|
+
internalOnlySkipCopilot = true;
|
|
378
|
+
interpretation = {
|
|
379
|
+
...interpretation,
|
|
380
|
+
state: STATE.INTERNAL_TOOLING_DIRECT_GATE,
|
|
381
|
+
nextAction: NEXT_ACTIONS[STATE.INTERNAL_TOOLING_DIRECT_GATE],
|
|
382
|
+
allowedTransitions: TRANSITIONS[STATE.INTERNAL_TOOLING_DIRECT_GATE] || [STATE.DONE],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// Best-effort: if detection fails, fall through to normal request behavior
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let reviewRequestStatus;
|
|
392
|
+
const shouldRequestReview = !internalOnlySkipCopilot && options.watchStatus === undefined
|
|
393
|
+
&& (interpretation.state === STATE.PR_READY_NO_FEEDBACK
|
|
394
|
+
|| interpretation.state === STATE.READY_TO_REREQUEST_REVIEW
|
|
395
|
+
&& interpretation.autoRerequestEligible);
|
|
396
|
+
if (shouldRequestReview) {
|
|
397
|
+
const requestResult = await performCopilotReviewRequest(
|
|
398
|
+
{
|
|
399
|
+
repo: options.repo,
|
|
400
|
+
pr: options.pr,
|
|
401
|
+
sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
|
|
402
|
+
},
|
|
403
|
+
{ env, ghCommand },
|
|
404
|
+
);
|
|
405
|
+
reviewRequestStatus = requestResult.status;
|
|
406
|
+
snapshot = applyConfirmedReviewRequest(snapshot, reviewRequestStatus);
|
|
407
|
+
interpretation = interpretLoopState(snapshot, refinementConfig);
|
|
408
|
+
}
|
|
409
|
+
const interpretationSummary = summarizeLoopInterpretation(interpretation, refinementConfig);
|
|
410
|
+
const effectiveReviewRequestStatus = reviewRequestStatus
|
|
411
|
+
?? (snapshot.copilotReviewRequestStatus === "requested" || snapshot.copilotReviewRequestStatus === "already-requested"
|
|
412
|
+
? snapshot.copilotReviewRequestStatus
|
|
413
|
+
: undefined);
|
|
414
|
+
let action;
|
|
415
|
+
if (reviewRequestStatus === "requested" || reviewRequestStatus === "already-requested") {
|
|
416
|
+
action = "watch";
|
|
417
|
+
} else if (WATCH_STATES.has(interpretation.state)) {
|
|
418
|
+
action = "watch";
|
|
419
|
+
} else if (FIX_STATES.has(interpretation.state)) {
|
|
420
|
+
action = "fix";
|
|
421
|
+
} else {
|
|
422
|
+
action = "stop";
|
|
423
|
+
}
|
|
424
|
+
const result = {
|
|
425
|
+
ok: true,
|
|
426
|
+
action,
|
|
427
|
+
state: interpretation.state,
|
|
428
|
+
allowedTransitions: interpretation.allowedTransitions,
|
|
429
|
+
nextAction: interpretation.nextAction,
|
|
430
|
+
autoRerequestEligible: interpretation.autoRerequestEligible,
|
|
431
|
+
sameHeadCleanConverged: interpretation.sameHeadCleanConverged,
|
|
432
|
+
roundCapCleanEligible: interpretation.roundCapCleanEligible ?? false,
|
|
433
|
+
loopDisposition: interpretationSummary.loopDisposition,
|
|
434
|
+
terminal: interpretationSummary.terminal,
|
|
435
|
+
snapshot,
|
|
436
|
+
internalOnlySkipCopilot: internalOnlySkipCopilot || undefined,
|
|
437
|
+
};
|
|
438
|
+
if (runnerOwnership.status !== "skipped_no_async_run_id") {
|
|
439
|
+
result.runnerOwnership = runnerOwnership;
|
|
440
|
+
}
|
|
441
|
+
if (effectiveReviewRequestStatus !== undefined) {
|
|
442
|
+
result.reviewRequestStatus = effectiveReviewRequestStatus;
|
|
443
|
+
}
|
|
444
|
+
if (options.watchStatus !== undefined) {
|
|
445
|
+
result.watchStatus = options.watchStatus;
|
|
446
|
+
}
|
|
447
|
+
if (action === "watch") {
|
|
448
|
+
result.watchTimeoutPolicy = EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY;
|
|
449
|
+
result.watchArgs = {
|
|
450
|
+
repo: options.repo,
|
|
451
|
+
pr: options.pr,
|
|
452
|
+
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
453
|
+
timeoutMs: enforceExternalHealthyWaitTimeout({
|
|
454
|
+
timeoutMs: COPILOT_REVIEW_WAIT_TIMEOUT_MS,
|
|
455
|
+
contextLabel: "Copilot review wait",
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const normalizedRequestStatus = effectiveReviewRequestStatus
|
|
460
|
+
?? (snapshot.copilotReviewRequestStatus === "unavailable"
|
|
461
|
+
|| snapshot.copilotReviewRequestStatus === "failed"
|
|
462
|
+
? snapshot.copilotReviewRequestStatus
|
|
463
|
+
: "none");
|
|
464
|
+
result.requestWatchContract = summarizeRequestWatchContract({
|
|
465
|
+
interpretation,
|
|
466
|
+
action,
|
|
467
|
+
requestStatus: normalizedRequestStatus,
|
|
468
|
+
watchArgs: result.watchArgs,
|
|
469
|
+
});
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
export async function runCli(
|
|
473
|
+
argv = process.argv.slice(2),
|
|
474
|
+
{
|
|
475
|
+
stdout = process.stdout,
|
|
476
|
+
env = process.env,
|
|
477
|
+
ghCommand = "gh",
|
|
478
|
+
} = {},
|
|
479
|
+
) {
|
|
480
|
+
const options = parseHandoffCliArgs(argv);
|
|
481
|
+
if (options.help) {
|
|
482
|
+
stdout.write(`${USAGE}\n`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const result = await runHandoff(options, { env, ghCommand });
|
|
486
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
487
|
+
}
|
|
488
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
489
|
+
runCli().catch((error) => {
|
|
490
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
491
|
+
process.exitCode = 1;
|
|
492
|
+
});
|
|
493
|
+
}
|