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,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
4
|
+
export const INSPECT_RUN_VIEWER_RELEVANT_EXACT_PATHS = Object.freeze([
|
|
5
|
+
".github/workflows/ci.yml",
|
|
6
|
+
"package.json",
|
|
7
|
+
"package-lock.json",
|
|
8
|
+
"playwright.inspect-run-viewer.config.mjs",
|
|
9
|
+
"scripts/loop/_inspect-run-viewer-adapter.mjs",
|
|
10
|
+
"scripts/loop/inspect-run-viewer.mjs",
|
|
11
|
+
"scripts/loop/inspect-run-viewer-ci-changes.mjs",
|
|
12
|
+
"test/playwright/harness/webkit-smoke-harness.mjs",
|
|
13
|
+
"test/playwright/inspect-run-viewer.spec.mjs",
|
|
14
|
+
]);
|
|
15
|
+
export const INSPECT_RUN_VIEWER_RELEVANT_PREFIXES = Object.freeze([
|
|
16
|
+
"scripts/loop/inspect-run-viewer/",
|
|
17
|
+
"test/playwright/fixtures/",
|
|
18
|
+
]);
|
|
19
|
+
const USAGE = "Usage: inspect-run-viewer-ci-changes.mjs <changed-files-path>";
|
|
20
|
+
const HELP = `Usage: inspect-run-viewer-ci-changes.mjs <changed-files-path>
|
|
21
|
+
Classify changed files to determine if inspect-run-viewer tests should run.
|
|
22
|
+
Reads a newline-delimited file list and checks against known relevant paths.
|
|
23
|
+
Options:
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
Exit codes:
|
|
26
|
+
0 Success
|
|
27
|
+
1 Error
|
|
28
|
+
`;
|
|
29
|
+
const parseError = buildParseError(USAGE);
|
|
30
|
+
export function normalizeInspectRunViewerPath(filePath) {
|
|
31
|
+
return String(filePath ?? "")
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^\.\/+/u, "");
|
|
34
|
+
}
|
|
35
|
+
function isInspectRunViewerRelevantNormalizedPath(normalizedPath) {
|
|
36
|
+
return normalizedPath.length > 0 && (
|
|
37
|
+
INSPECT_RUN_VIEWER_RELEVANT_EXACT_PATHS.includes(normalizedPath)
|
|
38
|
+
|| INSPECT_RUN_VIEWER_RELEVANT_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export function isInspectRunViewerRelevantPath(filePath) {
|
|
42
|
+
return isInspectRunViewerRelevantNormalizedPath(normalizeInspectRunViewerPath(filePath));
|
|
43
|
+
}
|
|
44
|
+
export function classifyInspectRunViewerCiChanges(changedPaths = []) {
|
|
45
|
+
const relevantPaths = [...new Set(changedPaths
|
|
46
|
+
.map((entry) => normalizeInspectRunViewerPath(entry))
|
|
47
|
+
.filter((entry) => isInspectRunViewerRelevantNormalizedPath(entry)))].sort();
|
|
48
|
+
return {
|
|
49
|
+
shouldRun: relevantPaths.length > 0,
|
|
50
|
+
relevantPaths,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function runCli(
|
|
54
|
+
argv = process.argv.slice(2),
|
|
55
|
+
{ env = process.env, stdout = process.stdout } = {},
|
|
56
|
+
) {
|
|
57
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
58
|
+
stdout.write(HELP);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (argv.length !== 1) {
|
|
62
|
+
throw parseError("inspect-run-viewer-ci-changes requires exactly one changed-files path argument");
|
|
63
|
+
}
|
|
64
|
+
const rawPaths = await readFile(argv[0], "utf8");
|
|
65
|
+
const result = classifyInspectRunViewerCiChanges(rawPaths.split(/\r?\n/u));
|
|
66
|
+
if (env.GITHUB_OUTPUT) {
|
|
67
|
+
await appendFile(env.GITHUB_OUTPUT, `inspect_run_viewer=${result.shouldRun}\n`, "utf8");
|
|
68
|
+
}
|
|
69
|
+
stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
73
|
+
runCli().catch((error) => {
|
|
74
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { formatCliError } from "../_core-helpers.mjs";
|
|
5
|
+
import { parseInspectRunViewerCliArgs, parseInspectRunViewerCliError, USAGE } from "./inspect-run-viewer/cli.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createInspectRunViewerServer,
|
|
8
|
+
formatInspectRunViewerUrl,
|
|
9
|
+
listListeningPidsForPort,
|
|
10
|
+
restartExistingPortListener,
|
|
11
|
+
} from "./inspect-run-viewer/server.mjs";
|
|
12
|
+
import {
|
|
13
|
+
buildInspectionMermaidGraph,
|
|
14
|
+
loadMermaidBrowserScript,
|
|
15
|
+
renderInspectRunViewerHtml,
|
|
16
|
+
resetMermaidBrowserScriptCache,
|
|
17
|
+
} from "./inspect-run-viewer/rendering.mjs";
|
|
18
|
+
function normalizeRestartCapabilityError(error) {
|
|
19
|
+
const missingLsof = error?.code === "ENOENT"
|
|
20
|
+
&& (error?.path === "lsof" || /(^|\b)lsof(\b|$)/i.test(String(error?.message ?? "")));
|
|
21
|
+
if (!missingLsof) {
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
const parseFriendlyError = parseInspectRunViewerCliError(
|
|
25
|
+
"--restart requires lsof/POSIX support; install lsof or rerun without --restart",
|
|
26
|
+
);
|
|
27
|
+
parseFriendlyError.cause = error;
|
|
28
|
+
return parseFriendlyError;
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
buildInspectionMermaidGraph,
|
|
32
|
+
createInspectRunViewerServer,
|
|
33
|
+
formatInspectRunViewerUrl,
|
|
34
|
+
listListeningPidsForPort,
|
|
35
|
+
loadMermaidBrowserScript,
|
|
36
|
+
parseInspectRunViewerCliArgs,
|
|
37
|
+
renderInspectRunViewerHtml,
|
|
38
|
+
resetMermaidBrowserScriptCache,
|
|
39
|
+
restartExistingPortListener,
|
|
40
|
+
};
|
|
41
|
+
export async function runCli(
|
|
42
|
+
argv = process.argv.slice(2),
|
|
43
|
+
{
|
|
44
|
+
stdout = process.stdout,
|
|
45
|
+
restartExistingPortListenerImpl = restartExistingPortListener,
|
|
46
|
+
} = {},
|
|
47
|
+
) {
|
|
48
|
+
const options = parseInspectRunViewerCliArgs(argv);
|
|
49
|
+
if (options.help) {
|
|
50
|
+
stdout.write(`${USAGE}\n`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (options.restart) {
|
|
54
|
+
try {
|
|
55
|
+
await restartExistingPortListenerImpl(options.port);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw normalizeRestartCapabilityError(error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const server = createInspectRunViewerServer(options);
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
server.once("error", reject);
|
|
63
|
+
server.listen(options.port, options.host, resolve);
|
|
64
|
+
});
|
|
65
|
+
stdout.write(
|
|
66
|
+
`${JSON.stringify({
|
|
67
|
+
ok: true,
|
|
68
|
+
message: "read-only inspect-run dashboard started",
|
|
69
|
+
scope: { repo: options.repo },
|
|
70
|
+
url: formatInspectRunViewerUrl(options.host, options.port),
|
|
71
|
+
reload: "manual",
|
|
72
|
+
})}\n`,
|
|
73
|
+
);
|
|
74
|
+
return server;
|
|
75
|
+
}
|
|
76
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
77
|
+
if (isDirectRun) {
|
|
78
|
+
runCli().catch((error) => {
|
|
79
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { buildParseError, formatCliError, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
|
|
7
|
+
import { fetchGithubReviewThreadsPayload } from "../github/capture-review-threads.mjs";
|
|
8
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
9
|
+
import { readExistingCheckpoint } from "./_checkpoint-io.mjs";
|
|
10
|
+
import { loadCopilotEvidence, loadReviewerEvidence } from "./_loop-evidence.mjs";
|
|
11
|
+
import { interpretOuterLoopState } from "@dev-loops/core/loop/conductor-routing";
|
|
12
|
+
import {
|
|
13
|
+
composeRunInspectionSnapshot,
|
|
14
|
+
deriveRunIdForInspectionTarget,
|
|
15
|
+
} from "@dev-loops/core/loop/run-inspection";
|
|
16
|
+
import { summarizeCopilotLoopIterations } from "@dev-loops/core/loop/copilot-loop-iterations";
|
|
17
|
+
import {
|
|
18
|
+
classifySafePoint,
|
|
19
|
+
getSteeringStatus,
|
|
20
|
+
normalizeSteeringState,
|
|
21
|
+
resolveEffectiveLoopState,
|
|
22
|
+
STEERING_KIND,
|
|
23
|
+
} from "@dev-loops/core/loop/steering";
|
|
24
|
+
import { validateSteeringStateTarget } from "./_steering-state-file.mjs";
|
|
25
|
+
const USAGE = `Usage: inspect-run.mjs --repo <owner/name> --pr <number>
|
|
26
|
+
Read-only run inspection for the Copilot PR outer-loop family.
|
|
27
|
+
Produces a single JSON snapshot describing the current state of one
|
|
28
|
+
explicitly targeted run without attaching to a live worker process or
|
|
29
|
+
rewriting any local artifacts.
|
|
30
|
+
Required:
|
|
31
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
32
|
+
--pr <number> Pull request number
|
|
33
|
+
Optional:
|
|
34
|
+
--steering-state-file <path> Path to a durable steering state JSON
|
|
35
|
+
file (as written by steer-loop.mjs).
|
|
36
|
+
When absent, steering is reported as
|
|
37
|
+
unavailable (no_steering_locator).
|
|
38
|
+
Test / snapshot-mode flags:
|
|
39
|
+
--copilot-input <path> Pre-built copilot snapshot JSON
|
|
40
|
+
(skips live copilot detection)
|
|
41
|
+
--reviewer-input <path> Pre-built reviewer snapshot JSON
|
|
42
|
+
(skips live reviewer detection)
|
|
43
|
+
Output (stdout, JSON):
|
|
44
|
+
Always-present fields:
|
|
45
|
+
ok, schemaVersion, target, inspectedAt, activeStateFamily,
|
|
46
|
+
outerState, outerAction, activeFamilyState, statusClass, needsAttention,
|
|
47
|
+
sourceMode, trust, evidence, markers, loopIterations
|
|
48
|
+
Best-effort fields:
|
|
49
|
+
allowedTransitions (when authoritative outerState is available),
|
|
50
|
+
layers (copilot, reviewer, steering drill-down)
|
|
51
|
+
statusClass values:
|
|
52
|
+
active Orchestrator needs to re-enter an inner loop
|
|
53
|
+
waiting Orchestrator is waiting on an external event
|
|
54
|
+
blocked Orchestrator is stopped and requires attention
|
|
55
|
+
done PR is merged or closed; loop complete
|
|
56
|
+
unknown Cannot determine state from available evidence
|
|
57
|
+
sourceMode values:
|
|
58
|
+
live-detector-backed All facts from live detectors (authoritative)
|
|
59
|
+
checkpoint-only Checkpoint drill-down only; top-level state stays unknown
|
|
60
|
+
partial Degraded mode. Mixed live + checkpoint fallback keeps top-level
|
|
61
|
+
state unknown; complete current-state input supplied by the
|
|
62
|
+
caller (including mixed live + input coverage) can still derive
|
|
63
|
+
a top-level state.
|
|
64
|
+
unavailable No usable evidence available
|
|
65
|
+
Error output (stderr, JSON):
|
|
66
|
+
Argument/usage errors:
|
|
67
|
+
{ "ok": false, "error": "...", "usage": "..." }
|
|
68
|
+
Runtime failures:
|
|
69
|
+
{ "ok": false, "error": "..." }
|
|
70
|
+
Exit codes:
|
|
71
|
+
0 Success
|
|
72
|
+
1 Argument error or unexpected runtime failure`.trim();
|
|
73
|
+
const parseError = buildParseError(USAGE);
|
|
74
|
+
export function parseInspectRunCliArgs(argv) {
|
|
75
|
+
const args = [...argv];
|
|
76
|
+
const options = {
|
|
77
|
+
help: false,
|
|
78
|
+
repo: undefined,
|
|
79
|
+
pr: undefined,
|
|
80
|
+
steeringStateFile: undefined,
|
|
81
|
+
copilotInputPath: undefined,
|
|
82
|
+
reviewerInputPath: undefined,
|
|
83
|
+
};
|
|
84
|
+
while (args.length > 0) {
|
|
85
|
+
const token = args.shift();
|
|
86
|
+
if (token === "--help" || token === "-h") {
|
|
87
|
+
options.help = true;
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
if (token === "--repo") {
|
|
91
|
+
options.repo = requireOptionValue(args, "--repo", parseError).trim();
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (token === "--pr") {
|
|
95
|
+
options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token === "--steering-state-file") {
|
|
99
|
+
options.steeringStateFile = requireOptionValue(args, "--steering-state-file", parseError);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (token === "--copilot-input") {
|
|
103
|
+
options.copilotInputPath = requireOptionValue(args, "--copilot-input", parseError);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (token === "--reviewer-input") {
|
|
107
|
+
options.reviewerInputPath = requireOptionValue(args, "--reviewer-input", parseError);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
throw parseError(`Unknown argument: ${token}`);
|
|
111
|
+
}
|
|
112
|
+
if (!options.help) {
|
|
113
|
+
if (options.repo === undefined || options.pr === undefined) {
|
|
114
|
+
throw parseError("inspect-run requires both --repo <owner/name> and --pr <number>");
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
parseRepoSlug(options.repo);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return options;
|
|
123
|
+
}
|
|
124
|
+
async function runGhJson(args, { env, ghCommand }) {
|
|
125
|
+
const result = await runChild(ghCommand, args, env);
|
|
126
|
+
if (result.code !== 0) {
|
|
127
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
128
|
+
throw new Error(`gh command failed: ${detail}`);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(result.stdout);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error(`Invalid JSON from gh: ${result.stdout.trim() || "<empty>"}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function normalizeTimelineReviewRequestEvents(payload) {
|
|
137
|
+
const events = Array.isArray(payload) ? payload : [];
|
|
138
|
+
return events
|
|
139
|
+
.filter((event) => event?.event === "review_requested")
|
|
140
|
+
.map((event) => ({
|
|
141
|
+
createdAt: event?.created_at,
|
|
142
|
+
requestedReviewerLogin: event?.requested_reviewer?.login,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
function normalizeReviewPayload(payload) {
|
|
146
|
+
const reviews = Array.isArray(payload) ? payload : [];
|
|
147
|
+
return reviews.map((review) => ({
|
|
148
|
+
state: review?.state,
|
|
149
|
+
submittedAt: review?.submitted_at ?? review?.created_at,
|
|
150
|
+
authorLogin: review?.user?.login ?? review?.author?.login,
|
|
151
|
+
commitSha: review?.commit_id ?? review?.commit?.oid,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
function normalizeReviewCommentsPayload(payload) {
|
|
155
|
+
const comments = Array.isArray(payload) ? payload : [];
|
|
156
|
+
return comments.map((comment) => ({
|
|
157
|
+
createdAt: comment?.created_at ?? comment?.createdAt,
|
|
158
|
+
authorLogin: comment?.user?.login ?? comment?.author?.login,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function normalizeCommitsPayload(payload) {
|
|
162
|
+
const commits = Array.isArray(payload) ? payload : [];
|
|
163
|
+
return commits.map((item) => ({
|
|
164
|
+
sha: item?.sha ?? item?.commit?.oid,
|
|
165
|
+
committedAt: item?.commit?.committer?.date ?? item?.commit?.author?.date ?? item?.committed_at,
|
|
166
|
+
authorLogin: item?.author?.login ?? item?.committer?.login ?? "",
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
async function fetchCopilotLoopIterations({ repo, pr, snapshot }, { env, ghCommand }) {
|
|
170
|
+
const [prViewPayload, timelinePayload, reviewsPayload, reviewCommentsPayload, commitsPayload, reviewThreadsPayload] = await Promise.all([
|
|
171
|
+
runGhJson(["pr", "view", String(pr), "--repo", repo, "--json", "headRefOid"], { env, ghCommand }),
|
|
172
|
+
runGhJson(
|
|
173
|
+
["api", "-H", "Accept: application/vnd.github+json", `repos/${repo}/issues/${pr}/timeline?per_page=100`],
|
|
174
|
+
{ env, ghCommand },
|
|
175
|
+
),
|
|
176
|
+
runGhJson(["api", `repos/${repo}/pulls/${pr}/reviews?per_page=100`], { env, ghCommand }),
|
|
177
|
+
runGhJson(["api", `repos/${repo}/pulls/${pr}/comments?per_page=100`], { env, ghCommand }),
|
|
178
|
+
runGhJson(["api", `repos/${repo}/pulls/${pr}/commits?per_page=100`], { env, ghCommand }),
|
|
179
|
+
fetchGithubReviewThreadsPayload({ repo, pr }, { env, ghCommand }),
|
|
180
|
+
]);
|
|
181
|
+
const reviewThreads = parseReviewThreads(reviewThreadsPayload);
|
|
182
|
+
const degradedReasons = [];
|
|
183
|
+
if (Array.isArray(timelinePayload) && timelinePayload.length >= 100) degradedReasons.push("timeline_page_cap");
|
|
184
|
+
if (Array.isArray(reviewsPayload) && reviewsPayload.length >= 100) degradedReasons.push("reviews_page_cap");
|
|
185
|
+
if (Array.isArray(reviewCommentsPayload) && reviewCommentsPayload.length >= 100) degradedReasons.push("review_comments_page_cap");
|
|
186
|
+
if (Array.isArray(commitsPayload) && commitsPayload.length >= 100) degradedReasons.push("commits_page_cap");
|
|
187
|
+
if (reviewThreadsPayload?.data?.repository?.pullRequest?.reviewThreads?.pageInfo?.hasNextPage) {
|
|
188
|
+
degradedReasons.push("review_threads_has_next_page");
|
|
189
|
+
}
|
|
190
|
+
return summarizeCopilotLoopIterations({
|
|
191
|
+
reviewRequestEvents: normalizeTimelineReviewRequestEvents(timelinePayload),
|
|
192
|
+
reviews: normalizeReviewPayload(reviewsPayload),
|
|
193
|
+
reviewComments: normalizeReviewCommentsPayload(reviewCommentsPayload),
|
|
194
|
+
commits: normalizeCommitsPayload(commitsPayload),
|
|
195
|
+
reviewThreadSummary: reviewThreads.summary,
|
|
196
|
+
currentHeadSha: typeof prViewPayload?.headRefOid === "string" ? prViewPayload.headRefOid : null,
|
|
197
|
+
currentReviewRequestStatus: snapshot?.copilotReviewRequestStatus ?? "none",
|
|
198
|
+
degraded: degradedReasons.length > 0,
|
|
199
|
+
degradedReasons,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function loadSteeringState(steeringStateFile) {
|
|
203
|
+
try {
|
|
204
|
+
const text = await readFile(steeringStateFile, "utf8");
|
|
205
|
+
const raw = parseJsonText(text);
|
|
206
|
+
return { state: normalizeSteeringState(raw), loadFailed: false };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error && error.code === "ENOENT") {
|
|
209
|
+
return { state: null, loadFailed: false };
|
|
210
|
+
}
|
|
211
|
+
return { state: null, loadFailed: true };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export async function inspectRun(options, { env = process.env, ghCommand = "gh" } = {}) {
|
|
215
|
+
const { repo, pr, steeringStateFile, copilotInputPath, reviewerInputPath } = options;
|
|
216
|
+
parseRepoSlug(repo);
|
|
217
|
+
const inspectedAt = new Date().toISOString();
|
|
218
|
+
const evidenceSourceKinds = {
|
|
219
|
+
copilot: copilotInputPath !== undefined ? "input" : "live",
|
|
220
|
+
reviewer: reviewerInputPath !== undefined ? "input" : "live",
|
|
221
|
+
};
|
|
222
|
+
let copilotEvidence = null;
|
|
223
|
+
let copilotLiveStatus = "failed";
|
|
224
|
+
try {
|
|
225
|
+
copilotEvidence = await loadCopilotEvidence({ repo, pr, copilotInputPath }, { env, ghCommand });
|
|
226
|
+
copilotLiveStatus = "ok";
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
let reviewerEvidence = null;
|
|
230
|
+
let reviewerLiveStatus = "failed";
|
|
231
|
+
try {
|
|
232
|
+
reviewerEvidence = await loadReviewerEvidence({ repo, pr, reviewerInputPath }, { env, ghCommand });
|
|
233
|
+
reviewerLiveStatus = "ok";
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
let loopIterations = {
|
|
237
|
+
available: false,
|
|
238
|
+
source: "github_pr_timeline",
|
|
239
|
+
reason: "requires_live_github_facts",
|
|
240
|
+
};
|
|
241
|
+
if (copilotEvidence?.snapshot?.prExists === false) {
|
|
242
|
+
loopIterations = {
|
|
243
|
+
available: false,
|
|
244
|
+
source: "github_pr_timeline",
|
|
245
|
+
reason: "no_pr",
|
|
246
|
+
};
|
|
247
|
+
} else if (copilotInputPath === undefined && copilotEvidence !== null) {
|
|
248
|
+
try {
|
|
249
|
+
loopIterations = await fetchCopilotLoopIterations(
|
|
250
|
+
{ repo, pr, snapshot: copilotEvidence.snapshot },
|
|
251
|
+
{ env, ghCommand },
|
|
252
|
+
);
|
|
253
|
+
} catch {
|
|
254
|
+
loopIterations = {
|
|
255
|
+
available: false,
|
|
256
|
+
source: "github_pr_timeline",
|
|
257
|
+
reason: "github_fact_capture_failed",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const { checkpoint: existingCheckpoint, filePath: checkpointEvidencePath } = await readExistingCheckpoint(repo, pr, { failSilently: true });
|
|
262
|
+
let outerState;
|
|
263
|
+
let outerAllowedTransitions;
|
|
264
|
+
let outerAction;
|
|
265
|
+
let outerReason;
|
|
266
|
+
const explicitTargetMissing =
|
|
267
|
+
copilotEvidence?.snapshot?.prExists === false
|
|
268
|
+
|| reviewerEvidence?.snapshot?.prExists === false;
|
|
269
|
+
const hasCompleteCurrentInnerLoopState =
|
|
270
|
+
!explicitTargetMissing
|
|
271
|
+
&& copilotLiveStatus === "ok"
|
|
272
|
+
&& reviewerLiveStatus === "ok"
|
|
273
|
+
&& copilotEvidence !== null
|
|
274
|
+
&& reviewerEvidence !== null;
|
|
275
|
+
if (hasCompleteCurrentInnerLoopState) {
|
|
276
|
+
const outerInterpretation = interpretOuterLoopState({
|
|
277
|
+
target: { repo, pr },
|
|
278
|
+
copilotState: copilotEvidence.interpretation.state,
|
|
279
|
+
reviewerState: reviewerEvidence.interpretation.state,
|
|
280
|
+
sourceMode: evidenceSourceKinds.copilot === "live" && evidenceSourceKinds.reviewer === "live"
|
|
281
|
+
? "authoritative"
|
|
282
|
+
: "snapshot",
|
|
283
|
+
requiresLocalIsolation: false,
|
|
284
|
+
});
|
|
285
|
+
outerState = outerInterpretation.state;
|
|
286
|
+
outerAllowedTransitions = outerInterpretation.allowedTransitions;
|
|
287
|
+
outerAction = outerInterpretation.outerAction;
|
|
288
|
+
outerReason = outerInterpretation.stopReason;
|
|
289
|
+
}
|
|
290
|
+
let steeringEvidence = null;
|
|
291
|
+
let steeringLoadFailed = false;
|
|
292
|
+
let steeringUnavailableReason = null;
|
|
293
|
+
let steeringReadback = null;
|
|
294
|
+
const steeringLocatorPath = steeringStateFile ?? null;
|
|
295
|
+
if (steeringLocatorPath !== null) {
|
|
296
|
+
const result = await loadSteeringState(steeringLocatorPath);
|
|
297
|
+
steeringEvidence = result.state;
|
|
298
|
+
steeringLoadFailed = result.loadFailed;
|
|
299
|
+
if (steeringEvidence !== null) {
|
|
300
|
+
const validation = validateSteeringStateTarget(steeringEvidence, {
|
|
301
|
+
repo,
|
|
302
|
+
pr,
|
|
303
|
+
runId: deriveRunIdForInspectionTarget({ repo, pr }),
|
|
304
|
+
});
|
|
305
|
+
if (!validation.ok) {
|
|
306
|
+
steeringEvidence = null;
|
|
307
|
+
steeringUnavailableReason = "mismatched_steering_target";
|
|
308
|
+
} else {
|
|
309
|
+
const steeringStatus = getSteeringStatus(steeringEvidence);
|
|
310
|
+
const resolved = copilotEvidence !== null
|
|
311
|
+
? resolveEffectiveLoopState(copilotEvidence.snapshot, steeringEvidence)
|
|
312
|
+
: null;
|
|
313
|
+
const queuedStopAtNextSafeGate = steeringEvidence.queuedEvents.some(
|
|
314
|
+
(event) => event.kind === STEERING_KIND.STOP_AT_NEXT_SAFE_GATE,
|
|
315
|
+
);
|
|
316
|
+
const effectiveConstraints = resolved?.effectiveConstraints ?? steeringStatus.effectiveConstraints;
|
|
317
|
+
const safePointCategory = copilotEvidence !== null
|
|
318
|
+
? classifySafePoint(copilotEvidence.interpretation.state)
|
|
319
|
+
: null;
|
|
320
|
+
steeringReadback = {
|
|
321
|
+
latestAcknowledgement: steeringStatus.latestResult,
|
|
322
|
+
effectiveConstraints,
|
|
323
|
+
pendingSummary: {
|
|
324
|
+
queuedCount: steeringStatus.queuedCount,
|
|
325
|
+
queuedKinds: [...new Set(steeringEvidence.queuedEvents.map((event) => event.kind))],
|
|
326
|
+
stopAtNextSafeGateQueued: queuedStopAtNextSafeGate,
|
|
327
|
+
},
|
|
328
|
+
stopAtNextSafeGate: {
|
|
329
|
+
effective: effectiveConstraints.stopAtNextSafeGate,
|
|
330
|
+
queued: queuedStopAtNextSafeGate,
|
|
331
|
+
terminal: resolved?.terminalStopAtNextSafeGate ?? false,
|
|
332
|
+
safePointCategory,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return composeRunInspectionSnapshot({
|
|
339
|
+
target: { repo, pr },
|
|
340
|
+
inspectedAt,
|
|
341
|
+
outerState,
|
|
342
|
+
outerAllowedTransitions,
|
|
343
|
+
outerAction,
|
|
344
|
+
outerReason,
|
|
345
|
+
copilotEvidence,
|
|
346
|
+
reviewerEvidence,
|
|
347
|
+
existingCheckpoint,
|
|
348
|
+
checkpointEvidencePath,
|
|
349
|
+
liveAvailability: { copilot: copilotLiveStatus, reviewer: reviewerLiveStatus },
|
|
350
|
+
evidenceSourceKinds,
|
|
351
|
+
explicitTargetMissing,
|
|
352
|
+
steeringLocatorPath,
|
|
353
|
+
steeringEvidence,
|
|
354
|
+
steeringLoadFailed,
|
|
355
|
+
steeringUnavailableReason,
|
|
356
|
+
steeringReadback,
|
|
357
|
+
loopIterations,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
export async function runCli(
|
|
361
|
+
argv = process.argv.slice(2),
|
|
362
|
+
{
|
|
363
|
+
stdout = process.stdout,
|
|
364
|
+
env = process.env,
|
|
365
|
+
ghCommand = "gh",
|
|
366
|
+
} = {},
|
|
367
|
+
) {
|
|
368
|
+
const options = parseInspectRunCliArgs(argv);
|
|
369
|
+
if (options.help) {
|
|
370
|
+
stdout.write(`${USAGE}\n`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const snapshot = await inspectRun(options, { env, ghCommand });
|
|
374
|
+
stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
375
|
+
}
|
|
376
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
377
|
+
if (isDirectRun) {
|
|
378
|
+
runCli().catch((error) => {
|
|
379
|
+
process.stderr.write(`${formatCliError(error)}\n`);
|
|
380
|
+
process.exitCode = 1;
|
|
381
|
+
});
|
|
382
|
+
}
|