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,841 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
STEERING_KIND,
|
|
8
|
+
STEERING_RESULT,
|
|
9
|
+
classifySafePoint,
|
|
10
|
+
normalizeSteeringEvent,
|
|
11
|
+
normalizeSteeringState,
|
|
12
|
+
createSteeringState,
|
|
13
|
+
promoteQueuedSteering,
|
|
14
|
+
submitSteering,
|
|
15
|
+
getSteeringStatus,
|
|
16
|
+
} from "@dev-loops/core/loop/steering";
|
|
17
|
+
import { STATE } from "@dev-loops/core/loop/copilot-loop-state";
|
|
18
|
+
import {
|
|
19
|
+
ACTIVE_STATE_FAMILY,
|
|
20
|
+
deriveRunIdForInspectionTarget,
|
|
21
|
+
SOURCE_MODE,
|
|
22
|
+
TRUST,
|
|
23
|
+
} from "@dev-loops/core/loop/run-inspection";
|
|
24
|
+
import { inspectRun } from "./inspect-run.mjs";
|
|
25
|
+
import {
|
|
26
|
+
defaultStateFilePath,
|
|
27
|
+
defaultStateFilePathForTarget,
|
|
28
|
+
loadStateFile,
|
|
29
|
+
saveStateFile,
|
|
30
|
+
validateSteeringStateTarget,
|
|
31
|
+
withStateFileLock,
|
|
32
|
+
} from "./_steering-state-file.mjs";
|
|
33
|
+
import { formatCliError } from "../_core-helpers.mjs";
|
|
34
|
+
import { requireOptionValue as readSharedOptionValue } from "../_cli-primitives.mjs";
|
|
35
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
36
|
+
const SUBMIT_USAGE = `Usage:
|
|
37
|
+
steer-loop.mjs submit --repo <owner/name> --pr <number>
|
|
38
|
+
--kind stop_at_next_safe_gate --directive <text> --seq <n>
|
|
39
|
+
[--state-file <path>] [--copilot-input <path>] [--reviewer-input <path>]
|
|
40
|
+
[--run-id <id>] [--event-id <id>]
|
|
41
|
+
# Internal/testing mode only:
|
|
42
|
+
steer-loop.mjs submit --run-id <id> --kind <kind> --directive <text> --seq <n>
|
|
43
|
+
[--state-file <path>] [--loop-state <loop-state>] [--apply-mode <mode>]
|
|
44
|
+
[--event-id <id>]
|
|
45
|
+
Submit a mid-flight steering directive to an active dev loop run.
|
|
46
|
+
Required:
|
|
47
|
+
--kind <kind> Steering kind
|
|
48
|
+
--directive <text> Operator payload / directive text
|
|
49
|
+
--seq <n> Positive integer sequence number (monotonically increasing per run)
|
|
50
|
+
--run-id <id> Target run identifier (required in low-level mode)
|
|
51
|
+
--repo <owner/name> Repository slug (required with --pr in operator-facing mode)
|
|
52
|
+
--pr <number> Pull request number (required with --repo in operator-facing mode)
|
|
53
|
+
Optional:
|
|
54
|
+
--state-file <path> Path to steering state JSON file (default: repo/pr mode => .pi/steering/<owner>/<repo>/pr-<n>.json; run-id mode => .pi/steering/<run-id>.json)
|
|
55
|
+
--loop-state <state> Current copilot loop state (low-level/testing mode only)
|
|
56
|
+
--apply-mode <mode> Application mode: immediate | next_safe_point (low-level/testing mode only)
|
|
57
|
+
--event-id <id> Unique event ID (default: auto-generated)
|
|
58
|
+
--copilot-input <path> Pre-built copilot snapshot JSON (operator-facing test mode)
|
|
59
|
+
--reviewer-input <path> Pre-built reviewer snapshot JSON (operator-facing test mode)
|
|
60
|
+
Output (stdout, JSON):
|
|
61
|
+
{ "ok": true, "acknowledgement": { ... }, "result": { ... }, "steeringState": { ... } }
|
|
62
|
+
Error output (stderr, JSON):
|
|
63
|
+
{ "ok": false, "error": "...", "usage": "..." }`.trim();
|
|
64
|
+
const STATUS_USAGE = `Usage:
|
|
65
|
+
steer-loop.mjs status --run-id <id> [--state-file <path>]
|
|
66
|
+
steer-loop.mjs status --repo <owner/name> --pr <number> [--state-file <path>]
|
|
67
|
+
Inspect the steering state for a run.
|
|
68
|
+
Choose exactly one target mode:
|
|
69
|
+
--run-id <id> Target run identifier
|
|
70
|
+
--repo <owner/name> Repository slug (required with --pr)
|
|
71
|
+
--pr <number> Pull request number (required with --repo)
|
|
72
|
+
Optional:
|
|
73
|
+
--state-file <path> Path to steering state JSON file (default: repo/pr mode => .pi/steering/<owner>/<repo>/pr-<n>.json; run-id mode => .pi/steering/<run-id>.json)
|
|
74
|
+
Output (stdout, JSON):
|
|
75
|
+
{ "ok": true, "status": { ... } }
|
|
76
|
+
Error output (stderr, JSON):
|
|
77
|
+
{ "ok": false, "error": "...", "usage": "..." }`.trim();
|
|
78
|
+
const PROMOTE_USAGE = `Usage:
|
|
79
|
+
steer-loop.mjs promote --run-id <id> --loop-state <state> [--state-file <path>]
|
|
80
|
+
steer-loop.mjs promote --repo <owner/name> --pr <number>
|
|
81
|
+
--loop-state <state> [--state-file <path>]
|
|
82
|
+
Explicitly promote queued steering to the effective stack when the caller knows
|
|
83
|
+
the loop has reached a safe point.
|
|
84
|
+
Choose exactly one target mode:
|
|
85
|
+
--run-id <id> Target run identifier
|
|
86
|
+
--repo <owner/name> Repository slug (required with --pr)
|
|
87
|
+
--pr <number> Pull request number (required with --repo)
|
|
88
|
+
Required:
|
|
89
|
+
--loop-state <state> Current copilot loop state
|
|
90
|
+
Optional:
|
|
91
|
+
--state-file <path> Path to steering state JSON file (default: repo/pr mode => .pi/steering/<owner>/<repo>/pr-<n>.json; run-id mode => .pi/steering/<run-id>.json)
|
|
92
|
+
Output (stdout, JSON):
|
|
93
|
+
{ "ok": true, "promotedCount": <n>, "promoted": [ ... ], "steeringState": { ... } }
|
|
94
|
+
Error output (stderr, JSON):
|
|
95
|
+
{ "ok": false, "error": "...", "usage": "..." }`.trim();
|
|
96
|
+
const TOP_USAGE = `Usage:
|
|
97
|
+
steer-loop.mjs <subcommand> [options]
|
|
98
|
+
Subcommands:
|
|
99
|
+
submit Submit a steering directive to an active dev loop run
|
|
100
|
+
promote Explicitly promote queued steering at a known loop state
|
|
101
|
+
status Inspect the steering state for a run
|
|
102
|
+
Run steer-loop.mjs <subcommand> --help for subcommand-specific help.`.trim();
|
|
103
|
+
const VALID_KINDS = new Set(Object.values(STEERING_KIND));
|
|
104
|
+
const VALID_APPLY_MODES = new Set(["immediate", "next_safe_point"]);
|
|
105
|
+
const VALID_LOOP_STATES = new Set(Object.values(STATE));
|
|
106
|
+
const SAFE_RUN_ID_RE = /^[A-Za-z0-9._-]+$/;
|
|
107
|
+
function usageError(message, usage) {
|
|
108
|
+
return Object.assign(new Error(message), { usage });
|
|
109
|
+
}
|
|
110
|
+
function runIdMismatchError(persistedRunId, requestedRunId) {
|
|
111
|
+
return new Error(
|
|
112
|
+
`run-id mismatch: --state-file contains run ${JSON.stringify(persistedRunId)} but --run-id is ${JSON.stringify(requestedRunId)}. Use the correct --run-id or point --state-file at the right file.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function readRequiredOptionValue(args, flag, usage, { allowFlagLike = false } = {}) {
|
|
116
|
+
return readSharedOptionValue(
|
|
117
|
+
args,
|
|
118
|
+
flag,
|
|
119
|
+
(message) => usageError(message, usage),
|
|
120
|
+
{ flagPattern: allowFlagLike ? /$^/u : /^--/u },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function validateSafeRunId(runId, usage) {
|
|
124
|
+
if (!SAFE_RUN_ID_RE.test(runId)) {
|
|
125
|
+
throw usageError("--run-id must contain only letters, numbers, dot, underscore, or hyphen", usage);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function parseRepoSlugOption(rawRepo, usage) {
|
|
129
|
+
try {
|
|
130
|
+
parseRepoSlug(rawRepo);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw usageError(error instanceof Error ? error.message : String(error), usage);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function parsePositiveIntegerOption(raw, flag, usage) {
|
|
136
|
+
if (!/^\d+$/.test(raw) || Number(raw) === 0) {
|
|
137
|
+
throw usageError(`${flag} must be a positive integer`, usage);
|
|
138
|
+
}
|
|
139
|
+
return Number(raw);
|
|
140
|
+
}
|
|
141
|
+
export function parseSubmitCliArgs(argv) {
|
|
142
|
+
const args = [...argv];
|
|
143
|
+
const options = {
|
|
144
|
+
help: false,
|
|
145
|
+
repo: undefined,
|
|
146
|
+
pr: undefined,
|
|
147
|
+
runId: undefined,
|
|
148
|
+
kind: undefined,
|
|
149
|
+
directive: undefined,
|
|
150
|
+
seq: undefined,
|
|
151
|
+
stateFile: undefined,
|
|
152
|
+
loopState: "ready_to_rerequest_review",
|
|
153
|
+
loopStateExplicit: false,
|
|
154
|
+
applyMode: "immediate",
|
|
155
|
+
eventId: undefined,
|
|
156
|
+
copilotInputPath: undefined,
|
|
157
|
+
reviewerInputPath: undefined,
|
|
158
|
+
};
|
|
159
|
+
while (args.length > 0) {
|
|
160
|
+
const token = args.shift();
|
|
161
|
+
if (token === "--help" || token === "-h") {
|
|
162
|
+
options.help = true;
|
|
163
|
+
return options;
|
|
164
|
+
}
|
|
165
|
+
if (token === "--run-id") {
|
|
166
|
+
options.runId = readRequiredOptionValue(args, "--run-id", SUBMIT_USAGE).trim();
|
|
167
|
+
validateSafeRunId(options.runId, SUBMIT_USAGE);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (token === "--repo") {
|
|
171
|
+
options.repo = readRequiredOptionValue(args, "--repo", SUBMIT_USAGE).trim();
|
|
172
|
+
parseRepoSlugOption(options.repo, SUBMIT_USAGE);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (token === "--pr") {
|
|
176
|
+
options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", SUBMIT_USAGE), "--pr", SUBMIT_USAGE);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (token === "--kind") {
|
|
180
|
+
const val = readRequiredOptionValue(args, "--kind", SUBMIT_USAGE);
|
|
181
|
+
if (!VALID_KINDS.has(val)) {
|
|
182
|
+
throw usageError(`--kind must be one of: ${[...VALID_KINDS].join(", ")}`, SUBMIT_USAGE);
|
|
183
|
+
}
|
|
184
|
+
options.kind = val;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (token === "--directive") {
|
|
188
|
+
options.directive = readRequiredOptionValue(args, "--directive", SUBMIT_USAGE, { allowFlagLike: true }).trim();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (token === "--seq") {
|
|
192
|
+
options.seq = parsePositiveIntegerOption(readRequiredOptionValue(args, "--seq", SUBMIT_USAGE), "--seq", SUBMIT_USAGE);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (token === "--state-file") {
|
|
196
|
+
options.stateFile = readRequiredOptionValue(args, "--state-file", SUBMIT_USAGE);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (token === "--loop-state") {
|
|
200
|
+
const val = readRequiredOptionValue(args, "--loop-state", SUBMIT_USAGE);
|
|
201
|
+
if (!VALID_LOOP_STATES.has(val)) {
|
|
202
|
+
throw usageError(`--loop-state must be one of: ${[...VALID_LOOP_STATES].join(", ")}`, SUBMIT_USAGE);
|
|
203
|
+
}
|
|
204
|
+
options.loopState = val;
|
|
205
|
+
options.loopStateExplicit = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (token === "--apply-mode") {
|
|
209
|
+
const val = readRequiredOptionValue(args, "--apply-mode", SUBMIT_USAGE);
|
|
210
|
+
if (!VALID_APPLY_MODES.has(val)) {
|
|
211
|
+
throw usageError(`--apply-mode must be one of: ${[...VALID_APPLY_MODES].join(", ")}`, SUBMIT_USAGE);
|
|
212
|
+
}
|
|
213
|
+
options.applyMode = val;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (token === "--event-id") {
|
|
217
|
+
options.eventId = readRequiredOptionValue(args, "--event-id", SUBMIT_USAGE);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (token === "--copilot-input") {
|
|
221
|
+
options.copilotInputPath = readRequiredOptionValue(args, "--copilot-input", SUBMIT_USAGE);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (token === "--reviewer-input") {
|
|
225
|
+
options.reviewerInputPath = readRequiredOptionValue(args, "--reviewer-input", SUBMIT_USAGE);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
throw usageError(`Unknown argument: ${token}`, SUBMIT_USAGE);
|
|
229
|
+
}
|
|
230
|
+
if (!options.help) {
|
|
231
|
+
if ((options.repo === undefined) !== (options.pr === undefined)) {
|
|
232
|
+
throw usageError("--repo and --pr must be provided together", SUBMIT_USAGE);
|
|
233
|
+
}
|
|
234
|
+
if (!options.runId && options.repo === undefined) {
|
|
235
|
+
throw usageError("--run-id is required, or both --repo and --pr must be provided together", SUBMIT_USAGE);
|
|
236
|
+
}
|
|
237
|
+
if (options.repo !== undefined && options.loopStateExplicit) {
|
|
238
|
+
throw usageError("--loop-state is low-level/testing mode only; omit it when using --repo/--pr operator mode", SUBMIT_USAGE);
|
|
239
|
+
}
|
|
240
|
+
if (!options.kind) {
|
|
241
|
+
throw usageError("--kind is required", SUBMIT_USAGE);
|
|
242
|
+
}
|
|
243
|
+
if (!options.directive || options.directive.length === 0) {
|
|
244
|
+
throw usageError("--directive is required and must be non-empty", SUBMIT_USAGE);
|
|
245
|
+
}
|
|
246
|
+
if (options.seq === undefined) {
|
|
247
|
+
throw usageError("--seq is required", SUBMIT_USAGE);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return options;
|
|
251
|
+
}
|
|
252
|
+
export function parseStatusCliArgs(argv) {
|
|
253
|
+
const args = [...argv];
|
|
254
|
+
const options = {
|
|
255
|
+
help: false,
|
|
256
|
+
repo: undefined,
|
|
257
|
+
pr: undefined,
|
|
258
|
+
runId: undefined,
|
|
259
|
+
stateFile: undefined,
|
|
260
|
+
};
|
|
261
|
+
while (args.length > 0) {
|
|
262
|
+
const token = args.shift();
|
|
263
|
+
if (token === "--help" || token === "-h") {
|
|
264
|
+
options.help = true;
|
|
265
|
+
return options;
|
|
266
|
+
}
|
|
267
|
+
if (token === "--run-id") {
|
|
268
|
+
options.runId = readRequiredOptionValue(args, "--run-id", STATUS_USAGE).trim();
|
|
269
|
+
validateSafeRunId(options.runId, STATUS_USAGE);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (token === "--repo") {
|
|
273
|
+
options.repo = readRequiredOptionValue(args, "--repo", STATUS_USAGE).trim();
|
|
274
|
+
parseRepoSlugOption(options.repo, STATUS_USAGE);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (token === "--pr") {
|
|
278
|
+
options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", STATUS_USAGE), "--pr", STATUS_USAGE);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (token === "--state-file") {
|
|
282
|
+
options.stateFile = readRequiredOptionValue(args, "--state-file", STATUS_USAGE);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
throw usageError(`Unknown argument: ${token}`, STATUS_USAGE);
|
|
286
|
+
}
|
|
287
|
+
if (!options.help) {
|
|
288
|
+
if ((options.repo === undefined) !== (options.pr === undefined)) {
|
|
289
|
+
throw usageError("--repo and --pr must be provided together", STATUS_USAGE);
|
|
290
|
+
}
|
|
291
|
+
if (options.runId && options.repo !== undefined) {
|
|
292
|
+
throw usageError("Choose exactly one target mode: either --run-id or --repo/--pr", STATUS_USAGE);
|
|
293
|
+
}
|
|
294
|
+
if (!options.runId && options.repo === undefined) {
|
|
295
|
+
throw usageError("--run-id is required, or both --repo and --pr must be provided together", STATUS_USAGE);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return options;
|
|
299
|
+
}
|
|
300
|
+
export function parsePromoteCliArgs(argv) {
|
|
301
|
+
const args = [...argv];
|
|
302
|
+
const options = {
|
|
303
|
+
help: false,
|
|
304
|
+
repo: undefined,
|
|
305
|
+
pr: undefined,
|
|
306
|
+
runId: undefined,
|
|
307
|
+
stateFile: undefined,
|
|
308
|
+
loopState: undefined,
|
|
309
|
+
};
|
|
310
|
+
while (args.length > 0) {
|
|
311
|
+
const token = args.shift();
|
|
312
|
+
if (token === "--help" || token === "-h") {
|
|
313
|
+
options.help = true;
|
|
314
|
+
return options;
|
|
315
|
+
}
|
|
316
|
+
if (token === "--run-id") {
|
|
317
|
+
options.runId = readRequiredOptionValue(args, "--run-id", PROMOTE_USAGE).trim();
|
|
318
|
+
validateSafeRunId(options.runId, PROMOTE_USAGE);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (token === "--repo") {
|
|
322
|
+
options.repo = readRequiredOptionValue(args, "--repo", PROMOTE_USAGE).trim();
|
|
323
|
+
parseRepoSlugOption(options.repo, PROMOTE_USAGE);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (token === "--pr") {
|
|
327
|
+
options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", PROMOTE_USAGE), "--pr", PROMOTE_USAGE);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (token === "--state-file") {
|
|
331
|
+
options.stateFile = readRequiredOptionValue(args, "--state-file", PROMOTE_USAGE);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (token === "--loop-state") {
|
|
335
|
+
const val = readRequiredOptionValue(args, "--loop-state", PROMOTE_USAGE);
|
|
336
|
+
if (!VALID_LOOP_STATES.has(val)) {
|
|
337
|
+
throw usageError(`--loop-state must be one of: ${[...VALID_LOOP_STATES].join(", ")}`, PROMOTE_USAGE);
|
|
338
|
+
}
|
|
339
|
+
options.loopState = val;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
throw usageError(`Unknown argument: ${token}`, PROMOTE_USAGE);
|
|
343
|
+
}
|
|
344
|
+
if (!options.help) {
|
|
345
|
+
if ((options.repo === undefined) !== (options.pr === undefined)) {
|
|
346
|
+
throw usageError("--repo and --pr must be provided together", PROMOTE_USAGE);
|
|
347
|
+
}
|
|
348
|
+
if (options.runId && options.repo !== undefined) {
|
|
349
|
+
throw usageError("Choose exactly one target mode: either --run-id or --repo/--pr", PROMOTE_USAGE);
|
|
350
|
+
}
|
|
351
|
+
if (!options.runId && options.repo === undefined) {
|
|
352
|
+
throw usageError("--run-id is required, or both --repo and --pr must be provided together", PROMOTE_USAGE);
|
|
353
|
+
}
|
|
354
|
+
if (options.loopState === undefined) {
|
|
355
|
+
throw usageError("--loop-state is required", PROMOTE_USAGE);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return options;
|
|
359
|
+
}
|
|
360
|
+
function deriveTargetRunId(options) {
|
|
361
|
+
if (options.repo !== undefined && options.pr !== undefined) {
|
|
362
|
+
return deriveRunIdForInspectionTarget({ repo: options.repo, pr: options.pr });
|
|
363
|
+
}
|
|
364
|
+
return options.runId;
|
|
365
|
+
}
|
|
366
|
+
function quoteCliValue(value) {
|
|
367
|
+
return JSON.stringify(String(value));
|
|
368
|
+
}
|
|
369
|
+
function resolveRequestedRunId(options, usage) {
|
|
370
|
+
const derivedRunId = deriveTargetRunId(options);
|
|
371
|
+
if (options.runId && options.repo !== undefined && options.pr !== undefined && options.runId !== derivedRunId) {
|
|
372
|
+
throw usageError(
|
|
373
|
+
`run-id mismatch: explicit --run-id ${JSON.stringify(options.runId)} does not match derived run ${JSON.stringify(derivedRunId)} for --repo/--pr target`,
|
|
374
|
+
usage,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return derivedRunId;
|
|
378
|
+
}
|
|
379
|
+
function mapDisposition(resultCode) {
|
|
380
|
+
switch (resultCode) {
|
|
381
|
+
case STEERING_RESULT.APPLIED_NOW:
|
|
382
|
+
return "applied_now";
|
|
383
|
+
case STEERING_RESULT.QUEUED_FOR_SAFE_POINT:
|
|
384
|
+
return "queued_for_safe_point";
|
|
385
|
+
default:
|
|
386
|
+
return "rejected";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function buildReadbackPath({ repo, pr, runId, stateFilePath }) {
|
|
390
|
+
const inspectionStateFileFlag = stateFilePath ? ` --steering-state-file ${quoteCliValue(stateFilePath)}` : "";
|
|
391
|
+
const statusStateFileFlag = stateFilePath ? ` --state-file ${quoteCliValue(stateFilePath)}` : "";
|
|
392
|
+
const quotedRepo = repo ? quoteCliValue(repo) : null;
|
|
393
|
+
const quotedPr = pr !== undefined && pr !== null ? quoteCliValue(pr) : null;
|
|
394
|
+
const inspection = quotedRepo && quotedPr
|
|
395
|
+
? `node scripts/loop/inspect-run.mjs --repo ${quotedRepo} --pr ${quotedPr}${inspectionStateFileFlag}`
|
|
396
|
+
: null;
|
|
397
|
+
const steeringStatus = quotedRepo && quotedPr
|
|
398
|
+
? `node scripts/loop/steer-loop.mjs status --repo ${quotedRepo} --pr ${quotedPr}${statusStateFileFlag}`
|
|
399
|
+
: `node scripts/loop/steer-loop.mjs status --run-id ${quoteCliValue(runId)}${statusStateFileFlag}`;
|
|
400
|
+
return {
|
|
401
|
+
inspection,
|
|
402
|
+
steeringStatus,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function buildAcknowledgement({
|
|
406
|
+
repo,
|
|
407
|
+
pr,
|
|
408
|
+
runId,
|
|
409
|
+
directiveKind,
|
|
410
|
+
directiveText,
|
|
411
|
+
resultCode,
|
|
412
|
+
reason,
|
|
413
|
+
reasonCode = null,
|
|
414
|
+
inspectedState,
|
|
415
|
+
safePointCategory,
|
|
416
|
+
readbackPath,
|
|
417
|
+
}) {
|
|
418
|
+
return {
|
|
419
|
+
runId,
|
|
420
|
+
directiveKind,
|
|
421
|
+
directive: directiveText,
|
|
422
|
+
disposition: mapDisposition(resultCode),
|
|
423
|
+
resultCode,
|
|
424
|
+
reason,
|
|
425
|
+
...(reasonCode ? { reasonCode } : {}),
|
|
426
|
+
inspectedState,
|
|
427
|
+
safePointCategory,
|
|
428
|
+
effectiveNow: resultCode === STEERING_RESULT.APPLIED_NOW,
|
|
429
|
+
readbackPath,
|
|
430
|
+
...(repo && pr ? { target: { repo, pr } } : {}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function buildLowLevelResult({
|
|
434
|
+
eventId,
|
|
435
|
+
seq,
|
|
436
|
+
resultCode,
|
|
437
|
+
reason,
|
|
438
|
+
reasonCode = null,
|
|
439
|
+
acknowledgedAt = new Date().toISOString(),
|
|
440
|
+
}) {
|
|
441
|
+
return {
|
|
442
|
+
eventId,
|
|
443
|
+
seq,
|
|
444
|
+
result: resultCode,
|
|
445
|
+
reason,
|
|
446
|
+
...(reasonCode ? { reasonCode } : {}),
|
|
447
|
+
acknowledgedAt,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function loadOrCreateSteeringState(filePath, runId, target = null) {
|
|
451
|
+
const raw = await loadStateFile(filePath);
|
|
452
|
+
const steeringState = raw !== null
|
|
453
|
+
? normalizeSteeringState(raw)
|
|
454
|
+
: createSteeringState(runId, target);
|
|
455
|
+
if (raw !== null && steeringState.runId !== runId) {
|
|
456
|
+
throw runIdMismatchError(steeringState.runId, runId);
|
|
457
|
+
}
|
|
458
|
+
if (target !== null) {
|
|
459
|
+
const validation = validateSteeringStateTarget(steeringState, {
|
|
460
|
+
repo: target.repo,
|
|
461
|
+
pr: target.pr,
|
|
462
|
+
runId,
|
|
463
|
+
});
|
|
464
|
+
if (!validation.ok) {
|
|
465
|
+
throw new Error(`state-file target mismatch: ${validation.reason}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return steeringState;
|
|
469
|
+
}
|
|
470
|
+
function rejectUnsteerableInspection(inspection, { runId, eventId, seq, directiveKind, directiveText, readbackPath }) {
|
|
471
|
+
if (inspection.activeStateFamily !== ACTIVE_STATE_FAMILY) {
|
|
472
|
+
const reasonCode = "inspection_unsupported_state_family";
|
|
473
|
+
const result = buildLowLevelResult({
|
|
474
|
+
eventId,
|
|
475
|
+
seq,
|
|
476
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
477
|
+
reasonCode,
|
|
478
|
+
reason: `inspection target family '${inspection.activeStateFamily}' is unsupported for operator-facing steering`,
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
acknowledgement: buildAcknowledgement({
|
|
482
|
+
repo: inspection.target?.repo,
|
|
483
|
+
pr: inspection.target?.pr,
|
|
484
|
+
runId,
|
|
485
|
+
directiveKind,
|
|
486
|
+
directiveText,
|
|
487
|
+
resultCode: result.result,
|
|
488
|
+
reason: result.reason,
|
|
489
|
+
reasonCode: result.reasonCode,
|
|
490
|
+
inspectedState: inspection.layers?.copilot?.currentState ?? "unknown",
|
|
491
|
+
safePointCategory: null,
|
|
492
|
+
readbackPath,
|
|
493
|
+
}),
|
|
494
|
+
result,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (inspection.runId !== runId) {
|
|
498
|
+
const reasonCode = "inspection_run_mismatch";
|
|
499
|
+
const result = buildLowLevelResult({
|
|
500
|
+
eventId,
|
|
501
|
+
seq,
|
|
502
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
503
|
+
reasonCode,
|
|
504
|
+
reason: `inspection run mismatch: expected ${JSON.stringify(runId)} but inspected ${JSON.stringify(inspection.runId)}`,
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
acknowledgement: buildAcknowledgement({
|
|
508
|
+
repo: inspection.target?.repo,
|
|
509
|
+
pr: inspection.target?.pr,
|
|
510
|
+
runId,
|
|
511
|
+
directiveKind,
|
|
512
|
+
directiveText,
|
|
513
|
+
resultCode: result.result,
|
|
514
|
+
reason: result.reason,
|
|
515
|
+
reasonCode: result.reasonCode,
|
|
516
|
+
inspectedState: inspection.layers?.copilot?.currentState ?? "unknown",
|
|
517
|
+
safePointCategory: null,
|
|
518
|
+
readbackPath,
|
|
519
|
+
}),
|
|
520
|
+
result,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const inspectedState = inspection.layers?.copilot?.currentState;
|
|
524
|
+
const safePointCategory = typeof inspectedState === "string" ? classifySafePoint(inspectedState) : null;
|
|
525
|
+
if (typeof inspectedState !== "string" || inspection.statusClass === "unknown") {
|
|
526
|
+
const reasonCode = "inspection_target_unidentifiable";
|
|
527
|
+
const result = buildLowLevelResult({
|
|
528
|
+
eventId,
|
|
529
|
+
seq,
|
|
530
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
531
|
+
reasonCode,
|
|
532
|
+
reason: "target run could not be confidently identified from the inspection snapshot",
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
acknowledgement: buildAcknowledgement({
|
|
536
|
+
repo: inspection.target?.repo,
|
|
537
|
+
pr: inspection.target?.pr,
|
|
538
|
+
runId,
|
|
539
|
+
directiveKind,
|
|
540
|
+
directiveText,
|
|
541
|
+
resultCode: result.result,
|
|
542
|
+
reason: result.reason,
|
|
543
|
+
reasonCode: result.reasonCode,
|
|
544
|
+
inspectedState: inspectedState ?? "unknown",
|
|
545
|
+
safePointCategory,
|
|
546
|
+
readbackPath,
|
|
547
|
+
}),
|
|
548
|
+
result,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
inspection.sourceMode !== SOURCE_MODE.LIVE_DETECTOR_BACKED
|
|
553
|
+
|| inspection.trust !== TRUST.AUTHORITATIVE
|
|
554
|
+
|| inspection.markers.missing.length > 0
|
|
555
|
+
|| inspection.markers.stale.length > 0
|
|
556
|
+
|| inspection.markers.conflicts.length > 0
|
|
557
|
+
) {
|
|
558
|
+
const detail = [
|
|
559
|
+
`sourceMode=${inspection.sourceMode}`,
|
|
560
|
+
`trust=${inspection.trust}`,
|
|
561
|
+
`missing=${inspection.markers.missing.length}`,
|
|
562
|
+
`stale=${inspection.markers.stale.length}`,
|
|
563
|
+
`conflicts=${inspection.markers.conflicts.length}`,
|
|
564
|
+
].join(", ");
|
|
565
|
+
const reasonCode = "inspection_not_authoritative";
|
|
566
|
+
const result = buildLowLevelResult({
|
|
567
|
+
eventId,
|
|
568
|
+
seq,
|
|
569
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
570
|
+
reasonCode,
|
|
571
|
+
reason: `inspection snapshot is degraded or stale and cannot be steered safely (${detail})`,
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
acknowledgement: buildAcknowledgement({
|
|
575
|
+
repo: inspection.target?.repo,
|
|
576
|
+
pr: inspection.target?.pr,
|
|
577
|
+
runId,
|
|
578
|
+
directiveKind,
|
|
579
|
+
directiveText,
|
|
580
|
+
resultCode: result.result,
|
|
581
|
+
reason: result.reason,
|
|
582
|
+
reasonCode: result.reasonCode,
|
|
583
|
+
inspectedState,
|
|
584
|
+
safePointCategory,
|
|
585
|
+
readbackPath,
|
|
586
|
+
}),
|
|
587
|
+
result,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
export async function runSubmit(
|
|
593
|
+
argv = [],
|
|
594
|
+
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
595
|
+
) {
|
|
596
|
+
const options = parseSubmitCliArgs(argv);
|
|
597
|
+
if (options.help) {
|
|
598
|
+
stdout.write(`${SUBMIT_USAGE}\n`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const runId = resolveRequestedRunId(options, SUBMIT_USAGE);
|
|
602
|
+
const target = options.repo !== undefined && options.pr !== undefined
|
|
603
|
+
? { repo: options.repo, pr: options.pr }
|
|
604
|
+
: null;
|
|
605
|
+
const defaultTargetStateFilePath = target ? defaultStateFilePathForTarget(target, cwd) : defaultStateFilePath(runId, cwd);
|
|
606
|
+
const stateFilePath = options.stateFile ?? defaultTargetStateFilePath;
|
|
607
|
+
const readbackPath = buildReadbackPath({
|
|
608
|
+
repo: options.repo,
|
|
609
|
+
pr: options.pr,
|
|
610
|
+
runId,
|
|
611
|
+
stateFilePath,
|
|
612
|
+
});
|
|
613
|
+
const eventId = options.eventId ?? `evt-${randomUUID()}`;
|
|
614
|
+
let persistedTargetMismatch = null;
|
|
615
|
+
if (target !== null) {
|
|
616
|
+
try {
|
|
617
|
+
const rawExistingState = await loadStateFile(stateFilePath);
|
|
618
|
+
if (rawExistingState !== null) {
|
|
619
|
+
const normalizedExistingState = normalizeSteeringState(rawExistingState);
|
|
620
|
+
const validation = validateSteeringStateTarget(normalizedExistingState, {
|
|
621
|
+
repo: target.repo,
|
|
622
|
+
pr: target.pr,
|
|
623
|
+
runId,
|
|
624
|
+
});
|
|
625
|
+
if (!validation.ok) {
|
|
626
|
+
persistedTargetMismatch = validation.reason;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} catch (error) {
|
|
630
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
631
|
+
persistedTargetMismatch = `existing steering state is invalid: ${detail}`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
let inspectedState = options.loopState;
|
|
635
|
+
let safePointCategory = classifySafePoint(options.loopState);
|
|
636
|
+
let validationRejection = null;
|
|
637
|
+
if (options.repo !== undefined && options.pr !== undefined) {
|
|
638
|
+
if (persistedTargetMismatch !== null) {
|
|
639
|
+
const safeReadbackPath = buildReadbackPath({
|
|
640
|
+
repo: options.repo,
|
|
641
|
+
pr: options.pr,
|
|
642
|
+
runId,
|
|
643
|
+
stateFilePath: defaultTargetStateFilePath,
|
|
644
|
+
});
|
|
645
|
+
validationRejection = {
|
|
646
|
+
acknowledgement: buildAcknowledgement({
|
|
647
|
+
repo: options.repo,
|
|
648
|
+
pr: options.pr,
|
|
649
|
+
runId,
|
|
650
|
+
directiveKind: options.kind,
|
|
651
|
+
directiveText: options.directive,
|
|
652
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
653
|
+
reason: `steering state file does not match the requested target (${persistedTargetMismatch})`,
|
|
654
|
+
inspectedState: "unknown",
|
|
655
|
+
safePointCategory: null,
|
|
656
|
+
readbackPath: safeReadbackPath,
|
|
657
|
+
}),
|
|
658
|
+
result: buildLowLevelResult({
|
|
659
|
+
eventId,
|
|
660
|
+
seq: options.seq,
|
|
661
|
+
resultCode: STEERING_RESULT.REJECTED_UNSAFE_NOW,
|
|
662
|
+
reason: `steering state file does not match the requested target (${persistedTargetMismatch})`,
|
|
663
|
+
}),
|
|
664
|
+
};
|
|
665
|
+
} else {
|
|
666
|
+
const inspection = await inspectRun({
|
|
667
|
+
repo: options.repo,
|
|
668
|
+
pr: options.pr,
|
|
669
|
+
steeringStateFile: stateFilePath,
|
|
670
|
+
copilotInputPath: options.copilotInputPath,
|
|
671
|
+
reviewerInputPath: options.reviewerInputPath,
|
|
672
|
+
}, { env, ghCommand });
|
|
673
|
+
inspectedState = inspection.layers?.copilot?.currentState;
|
|
674
|
+
safePointCategory = inspectedState ? classifySafePoint(inspectedState) : null;
|
|
675
|
+
if (options.applyMode !== "immediate") {
|
|
676
|
+
validationRejection = {
|
|
677
|
+
acknowledgement: buildAcknowledgement({
|
|
678
|
+
repo: options.repo,
|
|
679
|
+
pr: options.pr,
|
|
680
|
+
runId,
|
|
681
|
+
directiveKind: options.kind,
|
|
682
|
+
directiveText: options.directive,
|
|
683
|
+
resultCode: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
684
|
+
reason: "external operator submit does not accept --apply-mode overrides in this first slice",
|
|
685
|
+
inspectedState: inspectedState ?? "unknown",
|
|
686
|
+
safePointCategory,
|
|
687
|
+
readbackPath,
|
|
688
|
+
}),
|
|
689
|
+
result: buildLowLevelResult({
|
|
690
|
+
eventId,
|
|
691
|
+
seq: options.seq,
|
|
692
|
+
resultCode: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
693
|
+
reason: "external operator submit does not accept --apply-mode overrides in this first slice",
|
|
694
|
+
}),
|
|
695
|
+
};
|
|
696
|
+
} else if (options.kind !== STEERING_KIND.STOP_AT_NEXT_SAFE_GATE) {
|
|
697
|
+
validationRejection = {
|
|
698
|
+
acknowledgement: buildAcknowledgement({
|
|
699
|
+
repo: options.repo,
|
|
700
|
+
pr: options.pr,
|
|
701
|
+
runId,
|
|
702
|
+
directiveKind: options.kind,
|
|
703
|
+
directiveText: options.directive,
|
|
704
|
+
resultCode: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
705
|
+
reason: "external operator submit accepts only stop_at_next_safe_gate in this first slice",
|
|
706
|
+
inspectedState: inspectedState ?? "unknown",
|
|
707
|
+
safePointCategory,
|
|
708
|
+
readbackPath,
|
|
709
|
+
}),
|
|
710
|
+
result: buildLowLevelResult({
|
|
711
|
+
eventId,
|
|
712
|
+
seq: options.seq,
|
|
713
|
+
resultCode: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
714
|
+
reason: "external operator submit accepts only stop_at_next_safe_gate in this first slice",
|
|
715
|
+
}),
|
|
716
|
+
};
|
|
717
|
+
} else {
|
|
718
|
+
validationRejection = rejectUnsteerableInspection(inspection, {
|
|
719
|
+
runId,
|
|
720
|
+
eventId,
|
|
721
|
+
seq: options.seq,
|
|
722
|
+
directiveKind: options.kind,
|
|
723
|
+
directiveText: options.directive,
|
|
724
|
+
readbackPath,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (validationRejection !== null) {
|
|
730
|
+
let steeringState;
|
|
731
|
+
try {
|
|
732
|
+
steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
733
|
+
} catch {
|
|
734
|
+
steeringState = createSteeringState(runId, target);
|
|
735
|
+
}
|
|
736
|
+
stdout.write(`${JSON.stringify({
|
|
737
|
+
ok: true,
|
|
738
|
+
acknowledgement: validationRejection.acknowledgement,
|
|
739
|
+
result: validationRejection.result,
|
|
740
|
+
steeringState,
|
|
741
|
+
})}\n`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const { steeringState: newState, result } = await withStateFileLock(stateFilePath, async () => {
|
|
745
|
+
const steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
746
|
+
const event = normalizeSteeringEvent({
|
|
747
|
+
eventId,
|
|
748
|
+
runId,
|
|
749
|
+
kind: options.kind,
|
|
750
|
+
directive: options.directive,
|
|
751
|
+
seq: options.seq,
|
|
752
|
+
applyMode: options.applyMode,
|
|
753
|
+
submittedAt: new Date().toISOString(),
|
|
754
|
+
});
|
|
755
|
+
const submission = submitSteering(event, steeringState, inspectedState);
|
|
756
|
+
await saveStateFile(stateFilePath, submission.steeringState);
|
|
757
|
+
return submission;
|
|
758
|
+
});
|
|
759
|
+
const acknowledgement = buildAcknowledgement({
|
|
760
|
+
repo: options.repo,
|
|
761
|
+
pr: options.pr,
|
|
762
|
+
runId,
|
|
763
|
+
directiveKind: options.kind,
|
|
764
|
+
directiveText: options.directive,
|
|
765
|
+
resultCode: result.result,
|
|
766
|
+
reason: result.reason,
|
|
767
|
+
inspectedState,
|
|
768
|
+
safePointCategory,
|
|
769
|
+
readbackPath,
|
|
770
|
+
});
|
|
771
|
+
stdout.write(`${JSON.stringify({ ok: true, acknowledgement, result, steeringState: newState })}\n`);
|
|
772
|
+
}
|
|
773
|
+
export async function runStatus(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
774
|
+
const options = parseStatusCliArgs(argv);
|
|
775
|
+
if (options.help) {
|
|
776
|
+
stdout.write(`${STATUS_USAGE}\n`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const runId = resolveRequestedRunId(options, STATUS_USAGE);
|
|
780
|
+
const target = options.repo !== undefined && options.pr !== undefined
|
|
781
|
+
? { repo: options.repo, pr: options.pr }
|
|
782
|
+
: null;
|
|
783
|
+
const stateFilePath = options.stateFile ?? (target ? defaultStateFilePathForTarget(target, cwd) : defaultStateFilePath(runId, cwd));
|
|
784
|
+
const steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
785
|
+
const status = getSteeringStatus(steeringState);
|
|
786
|
+
stdout.write(`${JSON.stringify({ ok: true, status })}\n`);
|
|
787
|
+
}
|
|
788
|
+
export async function runPromote(argv = [], { stdout = process.stdout, cwd = process.cwd() } = {}) {
|
|
789
|
+
const options = parsePromoteCliArgs(argv);
|
|
790
|
+
if (options.help) {
|
|
791
|
+
stdout.write(`${PROMOTE_USAGE}\n`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const runId = resolveRequestedRunId(options, PROMOTE_USAGE);
|
|
795
|
+
const target = options.repo !== undefined && options.pr !== undefined
|
|
796
|
+
? { repo: options.repo, pr: options.pr }
|
|
797
|
+
: null;
|
|
798
|
+
const stateFilePath = options.stateFile ?? (target ? defaultStateFilePathForTarget(target, cwd) : defaultStateFilePath(runId, cwd));
|
|
799
|
+
const promotedState = await withStateFileLock(stateFilePath, async () => {
|
|
800
|
+
const steeringState = await loadOrCreateSteeringState(stateFilePath, runId, target);
|
|
801
|
+
const nextState = promoteQueuedSteering(steeringState, options.loopState);
|
|
802
|
+
if (nextState.promoted.length > 0) {
|
|
803
|
+
await saveStateFile(stateFilePath, nextState.steeringState);
|
|
804
|
+
}
|
|
805
|
+
return nextState;
|
|
806
|
+
});
|
|
807
|
+
stdout.write(`${JSON.stringify({
|
|
808
|
+
ok: true,
|
|
809
|
+
promotedCount: promotedState.promoted.length,
|
|
810
|
+
promoted: promotedState.promoted,
|
|
811
|
+
steeringState: promotedState.steeringState,
|
|
812
|
+
})}\n`);
|
|
813
|
+
}
|
|
814
|
+
export async function runCli(
|
|
815
|
+
argv = process.argv.slice(2),
|
|
816
|
+
{ stdout = process.stdout, cwd = process.cwd(), env = process.env, ghCommand = "gh" } = {},
|
|
817
|
+
) {
|
|
818
|
+
const [subcommand, ...rest] = argv;
|
|
819
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
820
|
+
stdout.write(`${TOP_USAGE}\n`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (subcommand === "submit") {
|
|
824
|
+
return runSubmit(rest, { stdout, cwd, env, ghCommand });
|
|
825
|
+
}
|
|
826
|
+
if (subcommand === "promote") {
|
|
827
|
+
return runPromote(rest, { stdout, cwd });
|
|
828
|
+
}
|
|
829
|
+
if (subcommand === "status") {
|
|
830
|
+
return runStatus(rest, { stdout, cwd });
|
|
831
|
+
}
|
|
832
|
+
const error = usageError(`Unknown subcommand: ${subcommand}`, TOP_USAGE);
|
|
833
|
+
throw error;
|
|
834
|
+
}
|
|
835
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
836
|
+
if (isDirectRun) {
|
|
837
|
+
runCli().catch((error) => {
|
|
838
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
839
|
+
process.exitCode = 1;
|
|
840
|
+
});
|
|
841
|
+
}
|