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,715 @@
|
|
|
1
|
+
import { OUTER_STATE } from "@dev-loops/core/loop/conductor-routing";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
escapeHtml,
|
|
5
|
+
formatStateToken,
|
|
6
|
+
humanizeStateToken,
|
|
7
|
+
titleCaseWords,
|
|
8
|
+
} from "./shared.mjs";
|
|
9
|
+
|
|
10
|
+
const SNAPSHOT_BADGE_VARIANTS = {
|
|
11
|
+
authoritative: "success",
|
|
12
|
+
conflicting: "danger",
|
|
13
|
+
degraded: "warning",
|
|
14
|
+
"checkpoint-only": "warning",
|
|
15
|
+
unavailable: "muted",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const INBOX_SIGNAL_BADGE_VARIANTS = {
|
|
19
|
+
attention: "danger",
|
|
20
|
+
pending: "warning",
|
|
21
|
+
gate: "info",
|
|
22
|
+
ready: "success",
|
|
23
|
+
closed: "muted",
|
|
24
|
+
unknown: "muted",
|
|
25
|
+
waiting: "info",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function renderBadge(label, variant = "muted") {
|
|
29
|
+
return `<span class="handoff-badge handoff-badge-${escapeHtml(variant)}">${escapeHtml(label)}</span>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderCodeValue(value, fallback = "not present") {
|
|
33
|
+
return `<code>${escapeHtml(formatStateToken(value, fallback))}</code>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderCard({ kicker, title, body, className = "", dataField = null }) {
|
|
37
|
+
return `<article class="handoff-card viewer-card${className ? ` ${escapeHtml(className)}` : ""}"${dataField ? ` data-field="${escapeHtml(dataField)}"` : ""}>
|
|
38
|
+
${kicker ? `<p class="handoff-card-kicker">${escapeHtml(kicker)}</p>` : ""}
|
|
39
|
+
${title ? `<h3>${escapeHtml(title)}</h3>` : ""}
|
|
40
|
+
<div class="viewer-card-body">${body}</div>
|
|
41
|
+
</article>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderCardEmptyState() {
|
|
45
|
+
return '<p class="handoff-empty-copy">not present / unavailable</p>';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderKeyValueRows(entries, { compact = false } = {}) {
|
|
49
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
50
|
+
return renderCardEmptyState();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `<dl class="handoff-kv${compact ? " handoff-kv-compact" : ""}">
|
|
54
|
+
${entries.map(([label, value]) => `<div class="handoff-kv-row"><dt>${escapeHtml(label)}</dt><dd>${value}</dd></div>`).join("")}
|
|
55
|
+
</dl>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderStatGrid(stats, { columns = "" } = {}) {
|
|
59
|
+
const normalizedStats = Array.isArray(stats) ? stats.filter(Boolean) : [];
|
|
60
|
+
if (normalizedStats.length === 0) {
|
|
61
|
+
return renderCardEmptyState();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `<div class="handoff-stat-grid viewer-stat-grid${columns ? ` ${escapeHtml(columns)}` : ""}">
|
|
65
|
+
${normalizedStats.map(({ label, value }) => `<div class="handoff-stat"><span class="handoff-stat-label">${escapeHtml(label)}</span><span class="handoff-stat-value">${value}</span></div>`).join("")}
|
|
66
|
+
</div>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderCardList(items, { emptyText = "none" } = {}) {
|
|
70
|
+
if (!Array.isArray(items)) {
|
|
71
|
+
return `<p class="handoff-empty-copy">not present / unavailable</p>`;
|
|
72
|
+
}
|
|
73
|
+
if (items.length === 0) {
|
|
74
|
+
return `<p class="handoff-empty-copy">${escapeHtml(emptyText)}</p>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `<ul class="viewer-card-list">${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderCardListBlock(title, items, options = {}) {
|
|
81
|
+
return `<div class="viewer-card-list-block">
|
|
82
|
+
<h4>${escapeHtml(title)}</h4>
|
|
83
|
+
${renderCardList(items, options)}
|
|
84
|
+
</div>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderCurrentStateBadge(stateLabel) {
|
|
88
|
+
return renderBadge(stateLabel, SNAPSHOT_BADGE_VARIANTS[stateLabel] ?? "muted");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderInboxSignalBadge(snapshot) {
|
|
92
|
+
const signal = deriveInboxSignalFromSnapshot(snapshot);
|
|
93
|
+
return renderBadge(signal.replaceAll("_", " "), INBOX_SIGNAL_BADGE_VARIANTS[signal] ?? "muted");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderBooleanBadge(value, { positive = false } = {}) {
|
|
97
|
+
if (value === true) {
|
|
98
|
+
return renderBadge("true", positive ? "success" : "danger");
|
|
99
|
+
}
|
|
100
|
+
if (value === false) {
|
|
101
|
+
return renderBadge("false", positive ? "muted" : "success");
|
|
102
|
+
}
|
|
103
|
+
return renderBadge("not present", "muted");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildCopilotLoopIterationEntries(snapshot) {
|
|
107
|
+
const loopIterations = snapshot?.loopIterations;
|
|
108
|
+
if (loopIterations === null || loopIterations === undefined) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const humanSummary = loopIterations.available
|
|
113
|
+
? [
|
|
114
|
+
`state: ${snapshot?.layers?.copilot?.currentState ?? "not present"}`,
|
|
115
|
+
`iterations: ${loopIterations.completedCopilotReviewRounds} completed, ${loopIterations.pendingCopilotReviewRounds} pending`,
|
|
116
|
+
`comments: ${loopIterations.copilotReviewComments} produced, ${loopIterations.unresolvedReviewThreads} unresolved`,
|
|
117
|
+
`fix commits: ${loopIterations.fixCommitsAfterFeedback}`,
|
|
118
|
+
].join("; ")
|
|
119
|
+
: "not present / unavailable";
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
["available", escapeHtml(String(loopIterations.available))],
|
|
123
|
+
["source", escapeHtml(loopIterations.source ?? "not present")],
|
|
124
|
+
["reason", escapeHtml(loopIterations.reason ?? "not present")],
|
|
125
|
+
["completedCopilotReviewRounds", escapeHtml(String(loopIterations.completedCopilotReviewRounds ?? "not present"))],
|
|
126
|
+
["pendingCopilotReviewRounds", escapeHtml(String(loopIterations.pendingCopilotReviewRounds ?? "not present"))],
|
|
127
|
+
["copilotReviewRequests", escapeHtml(String(loopIterations.copilotReviewRequests ?? "not present"))],
|
|
128
|
+
["copilotReviewComments", escapeHtml(String(loopIterations.copilotReviewComments ?? "not present"))],
|
|
129
|
+
["resolvedReviewThreads", escapeHtml(String(loopIterations.resolvedReviewThreads ?? "not present"))],
|
|
130
|
+
["unresolvedReviewThreads", escapeHtml(String(loopIterations.unresolvedReviewThreads ?? "not present"))],
|
|
131
|
+
["fixCommitsAfterFeedback", escapeHtml(String(loopIterations.fixCommitsAfterFeedback ?? "not present"))],
|
|
132
|
+
["humanSummary", escapeHtml(humanSummary)],
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderOverviewSection(snapshot) {
|
|
137
|
+
const summary = summarizeCurrentPrStatus(snapshot);
|
|
138
|
+
const loopIterations = snapshot?.loopIterations;
|
|
139
|
+
|
|
140
|
+
const stateBody = `${renderStatGrid([
|
|
141
|
+
{ label: "status class", value: renderCodeValue(snapshot?.statusClass) },
|
|
142
|
+
{ label: "outer state", value: renderCodeValue(snapshot?.outerState) },
|
|
143
|
+
{ label: "outerAction", value: renderCodeValue(snapshot?.outerAction) },
|
|
144
|
+
{ label: "Copilot state", value: renderCodeValue(snapshot?.layers?.copilot?.currentState) },
|
|
145
|
+
{ label: "reviewer state", value: renderCodeValue(snapshot?.layers?.reviewer?.currentState) },
|
|
146
|
+
{ label: "reviewer verdict", value: escapeHtml(renderReviewerVerdict(snapshot)) },
|
|
147
|
+
{ label: "needs attention", value: renderBooleanBadge(snapshot?.needsAttention) },
|
|
148
|
+
{ label: "sourceMode", value: escapeHtml(formatStateToken(snapshot?.sourceMode)) },
|
|
149
|
+
], { columns: "viewer-stat-grid-2" })}`;
|
|
150
|
+
|
|
151
|
+
const metricsBody = `<div class="handoff-next-action viewer-next-action">
|
|
152
|
+
<p><strong>${escapeHtml(summary.headline)}.</strong> ${escapeHtml(summary.nextAction)}</p>
|
|
153
|
+
</div>
|
|
154
|
+
${loopIterations
|
|
155
|
+
? renderStatGrid([
|
|
156
|
+
{ label: "completed rounds", value: escapeHtml(String(loopIterations.completedCopilotReviewRounds ?? "not present")) },
|
|
157
|
+
{ label: "pending rounds", value: escapeHtml(String(loopIterations.pendingCopilotReviewRounds ?? "not present")) },
|
|
158
|
+
{ label: "Copilot comments", value: escapeHtml(String(loopIterations.copilotReviewComments ?? "not present")) },
|
|
159
|
+
{ label: "unresolved threads", value: escapeHtml(String(loopIterations.unresolvedReviewThreads ?? "not present")) },
|
|
160
|
+
{ label: "resolved threads", value: escapeHtml(String(loopIterations.resolvedReviewThreads ?? "not present")) },
|
|
161
|
+
{ label: "fix commits", value: escapeHtml(String(loopIterations.fixCommitsAfterFeedback ?? "not present")) },
|
|
162
|
+
], { columns: "viewer-stat-grid-3" })
|
|
163
|
+
: renderCardEmptyState()}
|
|
164
|
+
<div class="viewer-card-list-grid">
|
|
165
|
+
${renderCardListBlock("markers.missing", snapshot?.markers?.missing)}
|
|
166
|
+
${renderCardListBlock("markers.stale", snapshot?.markers?.stale)}
|
|
167
|
+
${renderCardListBlock("markers.conflicts", snapshot?.markers?.conflicts)}
|
|
168
|
+
</div>`;
|
|
169
|
+
|
|
170
|
+
return `<section class="viewer-tab-section" aria-label="Overview">
|
|
171
|
+
<div class="viewer-card-grid viewer-card-grid-overview">
|
|
172
|
+
${renderCard({ kicker: "Overview", title: "Current state", body: stateBody, className: "handoff-card-tight handoff-card-emphasis", dataField: "overview-state" })}
|
|
173
|
+
${renderCard({ kicker: "Overview", title: "Next action and key metrics", body: metricsBody, className: "handoff-card-tight", dataField: "overview-metrics" })}
|
|
174
|
+
</div>
|
|
175
|
+
</section>`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function renderOuterLoopSummarySection(snapshot) {
|
|
179
|
+
if (snapshot === null || snapshot === undefined) {
|
|
180
|
+
return renderCard({ kicker: "Layers", title: "Outer-loop", body: renderCardEmptyState(), className: "handoff-card-tight", dataField: "outer-loop-summary" });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return renderCard({
|
|
184
|
+
kicker: "Layers",
|
|
185
|
+
title: "Outer-loop",
|
|
186
|
+
className: "handoff-card-tight",
|
|
187
|
+
dataField: "outer-loop-summary",
|
|
188
|
+
body: `${renderStatGrid([
|
|
189
|
+
{ label: "activeStateFamily", value: escapeHtml(snapshot.activeStateFamily ?? "not present") },
|
|
190
|
+
{ label: "outerState", value: renderCodeValue(snapshot.outerState) },
|
|
191
|
+
{ label: "outerAction (compatibility)", value: renderCodeValue(snapshot.outerAction) },
|
|
192
|
+
{ label: "activeFamilyState", value: escapeHtml(snapshot.activeFamilyState ?? "not present") },
|
|
193
|
+
{ label: "statusClass", value: escapeHtml(snapshot.statusClass ?? "not present") },
|
|
194
|
+
{ label: "needsAttention", value: renderBooleanBadge(snapshot.needsAttention) },
|
|
195
|
+
{ label: "sourceMode", value: escapeHtml(snapshot.sourceMode ?? "not present") },
|
|
196
|
+
{ label: "trust", value: escapeHtml(snapshot.trust ?? "not present") },
|
|
197
|
+
{ label: "evidence.summary", value: escapeHtml(snapshot.evidence?.summary ?? "not present") },
|
|
198
|
+
], { columns: "viewer-stat-grid-3" })}
|
|
199
|
+
<div class="viewer-card-list-grid">
|
|
200
|
+
${renderCardListBlock("evidence.authoritative", snapshot.evidence?.authoritative)}
|
|
201
|
+
${renderCardListBlock("evidence.checkpoint", snapshot.evidence?.checkpoint)}
|
|
202
|
+
</div>`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function renderCopilotLayerSection(layer, snapshot = null) {
|
|
207
|
+
|
|
208
|
+
const loopIterationEntries = buildCopilotLoopIterationEntries(snapshot);
|
|
209
|
+
|
|
210
|
+
if (layer === null || layer === undefined) {
|
|
211
|
+
return renderCard({
|
|
212
|
+
kicker: "Layers",
|
|
213
|
+
title: "Copilot",
|
|
214
|
+
className: "handoff-card-tight",
|
|
215
|
+
dataField: "copilot-layer",
|
|
216
|
+
body: `${renderCardEmptyState()}
|
|
217
|
+
<div class="viewer-card-subsection">
|
|
218
|
+
<h4>Copilot loop iterations</h4>
|
|
219
|
+
${loopIterationEntries ? renderKeyValueRows(loopIterationEntries, { compact: true }) : renderCardEmptyState()}
|
|
220
|
+
</div>`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return renderCard({
|
|
225
|
+
kicker: "Layers",
|
|
226
|
+
title: "Copilot",
|
|
227
|
+
className: "handoff-card-tight",
|
|
228
|
+
dataField: "copilot-layer",
|
|
229
|
+
body: `${renderStatGrid([
|
|
230
|
+
{ label: "currentState", value: renderCodeValue(layer.currentState) },
|
|
231
|
+
{ label: "loopDisposition", value: renderCodeValue(layer.loopDisposition) },
|
|
232
|
+
{ label: "sameHeadCleanConverged", value: renderBooleanBadge(layer.sameHeadCleanConverged, { positive: true }) },
|
|
233
|
+
{ label: "terminal", value: renderBooleanBadge(layer.terminal, { positive: true }) },
|
|
234
|
+
], { columns: "viewer-stat-grid-2" })}
|
|
235
|
+
${renderCardListBlock("allowedTransitions", layer.allowedTransitions)}
|
|
236
|
+
<div class="viewer-card-subsection">
|
|
237
|
+
<h4>Copilot loop iterations</h4>
|
|
238
|
+
${loopIterationEntries ? renderKeyValueRows(loopIterationEntries, { compact: true }) : renderCardEmptyState()}
|
|
239
|
+
</div>`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
export function renderReviewerLayerSection(layer) {
|
|
245
|
+
if (layer === null || layer === undefined) {
|
|
246
|
+
return renderCard({ kicker: "Layers", title: "Reviewer", body: renderCardEmptyState(), className: "handoff-card-tight", dataField: "reviewer-layer" });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return renderCard({
|
|
250
|
+
kicker: "Layers",
|
|
251
|
+
title: "Reviewer",
|
|
252
|
+
className: "handoff-card-tight",
|
|
253
|
+
dataField: "reviewer-layer",
|
|
254
|
+
body: `${renderStatGrid([
|
|
255
|
+
{ label: "currentState", value: renderCodeValue(layer.currentState) },
|
|
256
|
+
{ label: "scope.mode", value: escapeHtml(layer.scope?.mode ?? "not present") },
|
|
257
|
+
{ label: "scope.reviewerLogin", value: escapeHtml(layer.scope?.reviewerLogin ?? "not present") },
|
|
258
|
+
{ label: "submittedReviewState", value: renderCodeValue(layer.submittedReviewState) },
|
|
259
|
+
{ label: "approvedOnCurrentHead", value: renderBooleanBadge(layer.approvedOnCurrentHead, { positive: true }) },
|
|
260
|
+
], { columns: "viewer-stat-grid-2" })}
|
|
261
|
+
${renderCardListBlock("allowedTransitions", layer.allowedTransitions)}`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function renderSteeringSummarySection(layer) {
|
|
266
|
+
if (layer === null || layer === undefined) {
|
|
267
|
+
return renderCard({ kicker: "Layers", title: "Steering", body: renderCardEmptyState(), className: "handoff-card-tight", dataField: "steering-summary" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return renderCard({
|
|
271
|
+
kicker: "Layers",
|
|
272
|
+
title: "Steering",
|
|
273
|
+
className: "handoff-card-tight",
|
|
274
|
+
dataField: "steering-summary",
|
|
275
|
+
body: renderStatGrid([
|
|
276
|
+
{ label: "status", value: escapeHtml(layer.status ?? "not present") },
|
|
277
|
+
{ label: "reason", value: escapeHtml(layer.reason ?? "not present") },
|
|
278
|
+
], { columns: "viewer-stat-grid-2" }),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderReviewerVerdict(snapshot) {
|
|
283
|
+
if (!snapshot) {
|
|
284
|
+
return "not present";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (snapshot.layers?.reviewer?.approvedOnCurrentHead === true) {
|
|
288
|
+
return "approved on current head";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return formatStateToken(snapshot.layers?.reviewer?.submittedReviewState);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function summarizeCurrentPrStatus(snapshot) {
|
|
295
|
+
if (!snapshot) {
|
|
296
|
+
return {
|
|
297
|
+
headline: "Snapshot unavailable",
|
|
298
|
+
detail: "Unable to determine the current PR state yet.",
|
|
299
|
+
nextAction: "Reload the snapshot or open /snapshot.json for the raw error payload.",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const copilotState = formatStateToken(snapshot.layers?.copilot?.currentState);
|
|
304
|
+
const reviewerState = formatStateToken(snapshot.layers?.reviewer?.currentState);
|
|
305
|
+
const statusClass = formatStateToken(snapshot.statusClass, "unknown");
|
|
306
|
+
const outerState = formatStateToken(snapshot.outerState, "unknown");
|
|
307
|
+
const outerAction = formatStateToken(snapshot.outerAction, "unknown");
|
|
308
|
+
const sameHeadCleanConverged = snapshot.layers?.copilot?.sameHeadCleanConverged === true;
|
|
309
|
+
const copilotLoopDisposition = formatStateToken(snapshot.layers?.copilot?.loopDisposition);
|
|
310
|
+
const copilotTerminal = snapshot.layers?.copilot?.terminal === true;
|
|
311
|
+
const reviewerApprovedOnCurrentHead = snapshot.layers?.reviewer?.approvedOnCurrentHead === true;
|
|
312
|
+
|
|
313
|
+
if (outerState === OUTER_STATE.NEEDS_RECONCILE) {
|
|
314
|
+
return {
|
|
315
|
+
headline: "Needs reconcile",
|
|
316
|
+
detail: "The authoritative outer state is needs_reconcile, which means the current inputs are ambiguous, conflicting, or insufficient.",
|
|
317
|
+
nextAction: "Reconcile the conflicting state before trusting the current routing result.",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (outerState === OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER) {
|
|
322
|
+
return {
|
|
323
|
+
headline: "Live owner already active",
|
|
324
|
+
detail: "The authoritative outer state is stay_with_current_live_owner, so the loop should not issue a new handoff yet.",
|
|
325
|
+
nextAction: "Wait for the live owner to progress the run, then refresh the inspection.",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (outerState === OUTER_STATE.STOP_NEEDS_HUMAN) {
|
|
330
|
+
return {
|
|
331
|
+
headline: "Needs attention",
|
|
332
|
+
detail: "The authoritative outer state is stop_needs_human, so automated progress should stop until a human resolves the blocking condition.",
|
|
333
|
+
nextAction: "Read the stop reason, trust markers, and layer summaries before proceeding.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (outerState === OUTER_STATE.DONE_TERMINAL) {
|
|
338
|
+
return {
|
|
339
|
+
headline: "PR complete",
|
|
340
|
+
detail: "The current inspection says this PR is in a terminal done state.",
|
|
341
|
+
nextAction: "Confirm merge/readiness context or inspect the raw snapshot for terminal evidence.",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (copilotState === "round_cap_clean_fallback") {
|
|
346
|
+
return {
|
|
347
|
+
headline: "Round cap reached",
|
|
348
|
+
detail: "Copilot review rounds are exhausted for this clean PR head, so the loop should move to pre_approval_gate instead of requesting another Copilot review.",
|
|
349
|
+
nextAction: "Run or confirm the current-head pre_approval_gate rather than re-requesting Copilot review.",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (copilotState === "round_cap_reached") {
|
|
354
|
+
return {
|
|
355
|
+
headline: "Round cap reached",
|
|
356
|
+
detail: "Copilot review rounds are exhausted for this PR head while unresolved feedback or CI problems still block convergence, so no further Copilot re-requests are possible.",
|
|
357
|
+
nextAction: "Resolve the remaining feedback or CI blockers without requesting another Copilot review.",
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (statusClass === "done" || outerAction === "done" || copilotState === "done") {
|
|
362
|
+
return {
|
|
363
|
+
headline: "PR complete",
|
|
364
|
+
detail: "The current inspection says this PR is in a terminal done state.",
|
|
365
|
+
nextAction: "Confirm merge/readiness context or inspect the raw snapshot for terminal evidence.",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (copilotState === "unresolved_feedback_present") {
|
|
370
|
+
return {
|
|
371
|
+
headline: "Needs author fixes",
|
|
372
|
+
detail: "Copilot has unresolved feedback on the current PR head.",
|
|
373
|
+
nextAction: "Address the feedback, then reply to and resolve each addressed thread.",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (copilotState === "already_fixed_needs_reply_resolve") {
|
|
378
|
+
return {
|
|
379
|
+
headline: "Fixes applied; threads still need resolution",
|
|
380
|
+
detail: "Local fixes appear applied, but GitHub review threads still need reply/resolve follow-up.",
|
|
381
|
+
nextAction: "Reply to and resolve the addressed review threads before requesting another Copilot pass.",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (copilotState === "waiting_for_copilot_review") {
|
|
386
|
+
return {
|
|
387
|
+
headline: "Waiting for Copilot review",
|
|
388
|
+
detail: "Copilot review has been requested and the PR is waiting for new review activity.",
|
|
389
|
+
nextAction: "Wait for Copilot review or refresh the snapshot after review activity lands.",
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (copilotState === "ready_to_rerequest_review" && reviewerApprovedOnCurrentHead && (sameHeadCleanConverged || copilotLoopDisposition === "clean_converged" || copilotTerminal)) {
|
|
394
|
+
return {
|
|
395
|
+
headline: "Clean reviews present; gate evidence still required",
|
|
396
|
+
detail: "The current head has both a clean submitted Copilot review and an approved human review, but approval or merge suggestions still require explicit current-head pre_approval_gate evidence.",
|
|
397
|
+
nextAction: "Confirm or rerun the current-head pre_approval_gate before any approval or merge recommendation.",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (copilotState === "ready_to_rerequest_review" && (sameHeadCleanConverged || copilotLoopDisposition === "clean_converged" || copilotTerminal)) {
|
|
402
|
+
return {
|
|
403
|
+
headline: "Copilot pass complete; gate evidence still required",
|
|
404
|
+
detail: "The current head already has a clean submitted Copilot review with no unresolved feedback, but that alone is not enough for an approval or merge suggestion.",
|
|
405
|
+
nextAction: "Confirm or rerun the current-head pre_approval_gate before any approval or merge recommendation, or wait for a meaningful remediation event before requesting another Copilot pass.",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (copilotState === "ready_to_rerequest_review") {
|
|
410
|
+
return {
|
|
411
|
+
headline: "Ready to re-request Copilot review",
|
|
412
|
+
detail: "The current head looks clean enough for another Copilot pass or final confirmation.",
|
|
413
|
+
nextAction: "Re-request Copilot review only after the smallest honest local validation is green, or confirm the PR is done.",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (reviewerState === "waiting_for_author_followup") {
|
|
418
|
+
return {
|
|
419
|
+
headline: "Waiting for author follow-up",
|
|
420
|
+
detail: "Reviewer work is done for this round and the PR is waiting on author-side changes.",
|
|
421
|
+
nextAction: "Wait for author commits or refresh after follow-up lands.",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (reviewerState === "waiting_for_re_request") {
|
|
426
|
+
return {
|
|
427
|
+
headline: "Waiting for reviewer re-request",
|
|
428
|
+
detail: "Reviewer work is paused until a new explicit review request arrives.",
|
|
429
|
+
nextAction: "Wait for a reviewer re-request after follow-up commits.",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (reviewerState === "review_requested" || reviewerState === "determine_review_plan" || reviewerState === "reviews_running" || reviewerState === "merge_results" || reviewerState === "draft_review_ready" || reviewerState === "draft_review_posted" || reviewerState === "waiting_for_user_submit" || reviewerState === "submitted_review" || reviewerState === "review_invalidated") {
|
|
434
|
+
return {
|
|
435
|
+
headline: "Reviewer loop active",
|
|
436
|
+
detail: `Reviewer lane is currently at ${humanizeStateToken(reviewerState)}.`,
|
|
437
|
+
nextAction: "Follow the reviewer lane details below and refresh after the next review event.",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (copilotState === "waiting_for_ci") {
|
|
442
|
+
return {
|
|
443
|
+
headline: "Waiting for CI",
|
|
444
|
+
detail: "The current head has progressed past review but is still waiting on CI readiness.",
|
|
445
|
+
nextAction: "Wait for CI to complete or become available.",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (outerState === "unknown" && snapshot.needsAttention) {
|
|
450
|
+
return {
|
|
451
|
+
headline: "Needs attention",
|
|
452
|
+
detail: "The current snapshot is not authoritative enough to collapse to one trusted outer state.",
|
|
453
|
+
nextAction: "Check trust markers and layer summaries before acting on this snapshot.",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (outerAction === "stop" || statusClass === "blocked") {
|
|
458
|
+
return {
|
|
459
|
+
headline: "Needs attention",
|
|
460
|
+
detail: "The inspection found a blocked or stop-like state, but the authoritative outer state was not specific enough to classify it more narrowly here.",
|
|
461
|
+
nextAction: "Read the stop reason, trust markers, and layer summaries before proceeding.",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (outerState === OUTER_STATE.CONTINUE_CURRENT_WAIT || outerAction === "continue_wait") {
|
|
466
|
+
return {
|
|
467
|
+
headline: "Waiting for follow-up",
|
|
468
|
+
detail: "The authoritative outer state is continue_current_wait, so the loop should remain in its durable wait path for now.",
|
|
469
|
+
nextAction: "Refresh after new review, CI, or author activity lands.",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (outerState === OUTER_STATE.HANDOFF_TO_COPILOT_LOOP || outerAction === "reenter_copilot_loop") {
|
|
474
|
+
return {
|
|
475
|
+
headline: "Copilot loop needs action",
|
|
476
|
+
detail: "The authoritative outer state is handoff_to_copilot_loop, so the next meaningful work is in the Copilot lane.",
|
|
477
|
+
nextAction: "Inspect the Copilot state and act on the requested follow-up.",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (outerState === OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP || outerAction === "reenter_reviewer_loop") {
|
|
482
|
+
return {
|
|
483
|
+
headline: "Reviewer loop needs action",
|
|
484
|
+
detail: "The authoritative outer state is handoff_to_reviewer_loop, so the next meaningful work is in the reviewer lane.",
|
|
485
|
+
nextAction: "Inspect the reviewer state and act on the requested follow-up.",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
headline: titleCaseWords(humanizeStateToken(copilotState === "not present" ? (outerState === "unknown" ? outerAction : outerState) : copilotState)),
|
|
491
|
+
detail: "The viewer could not collapse this to a narrower plain-English status than the current exported loop states.",
|
|
492
|
+
nextAction: "Use the current-state banner fields plus the graph and summaries below.",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function buildPullRequestHref(target) {
|
|
497
|
+
if (!target?.repo || target?.pr === null || target?.pr === undefined) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
return `https://github.com/${encodeURIComponent(target.repo).replaceAll("%2F", "/")}/pull/${encodeURIComponent(String(target.pr))}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function summarizeCurrentPrMode(snapshot) {
|
|
504
|
+
if (!snapshot) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const copilotState = formatStateToken(snapshot.layers?.copilot?.currentState);
|
|
509
|
+
const reviewerState = formatStateToken(snapshot.layers?.reviewer?.currentState);
|
|
510
|
+
const outerState = formatStateToken(snapshot.outerState, "unknown");
|
|
511
|
+
const outerAction = formatStateToken(snapshot.outerAction, "unknown");
|
|
512
|
+
|
|
513
|
+
if (deriveInboxSignalFromSnapshot(snapshot) === "ready") {
|
|
514
|
+
return { emoji: "✅", label: "Approved" };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (copilotState === "waiting_for_copilot_review"
|
|
518
|
+
|| copilotState === "waiting_for_ci"
|
|
519
|
+
|| reviewerState === "waiting_for_author_followup"
|
|
520
|
+
|| reviewerState === "waiting_for_re_request"
|
|
521
|
+
|| outerState === OUTER_STATE.CONTINUE_CURRENT_WAIT
|
|
522
|
+
|| outerState === OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER
|
|
523
|
+
|| outerAction === "continue_wait") {
|
|
524
|
+
return { emoji: "⏳", label: "Waiting state" };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (outerState === OUTER_STATE.HANDOFF_TO_COPILOT_LOOP
|
|
528
|
+
|| outerState === OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP
|
|
529
|
+
|| outerAction === "reenter_copilot_loop"
|
|
530
|
+
|| outerAction === "reenter_reviewer_loop"
|
|
531
|
+
|| reviewerState === "review_requested"
|
|
532
|
+
|| reviewerState === "determine_review_plan"
|
|
533
|
+
|| reviewerState === "reviews_running"
|
|
534
|
+
|| reviewerState === "merge_results"
|
|
535
|
+
|| reviewerState === "draft_review_ready"
|
|
536
|
+
|| reviewerState === "draft_review_posted"
|
|
537
|
+
|| reviewerState === "waiting_for_user_submit"
|
|
538
|
+
|| reviewerState === "submitted_review"
|
|
539
|
+
|| reviewerState === "review_invalidated") {
|
|
540
|
+
return { emoji: "🔁", label: "Active loop" };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function renderCurrentStateBanner(snapshot, target, stateLabel, selectedTitle = null) {
|
|
547
|
+
const summary = summarizeCurrentPrStatus(snapshot);
|
|
548
|
+
const pullRequestHref = buildPullRequestHref(target);
|
|
549
|
+
const mode = summarizeCurrentPrMode(snapshot);
|
|
550
|
+
const heading = typeof selectedTitle === "string" && selectedTitle.trim().length > 0
|
|
551
|
+
? selectedTitle.trim()
|
|
552
|
+
: `PR #${target.pr}`;
|
|
553
|
+
const targetLabel = target?.repo && target?.pr !== null && target?.pr !== undefined
|
|
554
|
+
? `${target.repo}#${target.pr}`
|
|
555
|
+
: `PR #${target?.pr ?? "unknown"}`;
|
|
556
|
+
const autoReloadOptions = [
|
|
557
|
+
{ value: "off", label: "Off" },
|
|
558
|
+
{ value: "60000", label: "1 minute" },
|
|
559
|
+
{ value: "300000", label: "5 minutes" },
|
|
560
|
+
{ value: "900000", label: "15 minutes" },
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
const metaLine = [
|
|
564
|
+
snapshot?.runId ? `run ${escapeHtml(snapshot.runId)}` : null,
|
|
565
|
+
snapshot?.inspectedAt ? escapeHtml(snapshot.inspectedAt) : null,
|
|
566
|
+
snapshot?.sourceMode ? escapeHtml(`source: ${snapshot.sourceMode}`) : null,
|
|
567
|
+
].filter(Boolean).join(" · ");
|
|
568
|
+
|
|
569
|
+
return `<section class="current-pr-state-banner" aria-label="PR #${escapeHtml(target.pr)}">
|
|
570
|
+
<div class="current-pr-state-heading-row">
|
|
571
|
+
<div class="current-pr-state-heading-copy">
|
|
572
|
+
<p class="current-pr-state-kicker">${pullRequestHref
|
|
573
|
+
? `<a href="${escapeHtml(pullRequestHref)}">${escapeHtml(targetLabel)}</a>`
|
|
574
|
+
: escapeHtml(targetLabel)}</p>
|
|
575
|
+
<h1>${escapeHtml(heading)}</h1>
|
|
576
|
+
</div>
|
|
577
|
+
${mode ? `<span class="current-pr-state-mode-indicator" title="${escapeHtml(mode.label)}" aria-label="${escapeHtml(mode.label)}">${escapeHtml(mode.emoji)}</span>` : ""}
|
|
578
|
+
</div>
|
|
579
|
+
<div class="current-pr-state-copy-flow">
|
|
580
|
+
${metaLine ? `<p class="current-pr-state-meta">${metaLine}</p>` : ""}
|
|
581
|
+
<div class="viewer-badge-row current-pr-state-badge-row">
|
|
582
|
+
${renderCurrentStateBadge(stateLabel)}
|
|
583
|
+
${renderInboxSignalBadge(snapshot)}
|
|
584
|
+
${mode ? renderBadge(mode.label, "info") : ""}
|
|
585
|
+
</div>
|
|
586
|
+
<p class="current-pr-state-summary-headline"><strong>${escapeHtml(summary.headline)}</strong></p>
|
|
587
|
+
<p class="current-pr-state-detail">${escapeHtml(summary.detail)}</p>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="current-pr-state-controls" data-auto-reload-controls>
|
|
590
|
+
<label class="current-pr-state-auto-reload-label" for="current-pr-state-auto-reload">
|
|
591
|
+
<span aria-hidden="true">⏱</span>
|
|
592
|
+
<span>Auto-reload:</span>
|
|
593
|
+
</label>
|
|
594
|
+
<select id="current-pr-state-auto-reload" class="current-pr-state-auto-reload-select" data-auto-reload-select aria-label="Auto-reload period">
|
|
595
|
+
${autoReloadOptions.map((option) => `<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>`).join("")}
|
|
596
|
+
</select>
|
|
597
|
+
<button type="button" class="viewer-action-button current-pr-state-reload" data-auto-reload-manual onclick="window.location.reload()" title="Reload snapshot" aria-label="Reload snapshot">🔄 Reload</button>
|
|
598
|
+
</div>
|
|
599
|
+
</section>
|
|
600
|
+
<script>
|
|
601
|
+
(() => {
|
|
602
|
+
const AUTO_RELOAD_STORAGE_KEY = "inspect-run-viewer:auto-reload-ms";
|
|
603
|
+
const select = document.querySelector("[data-auto-reload-select]");
|
|
604
|
+
const manualButton = document.querySelector("[data-auto-reload-manual]");
|
|
605
|
+
if (!select || !manualButton) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let autoReloadTimer = null;
|
|
610
|
+
const clearAutoReloadTimer = () => {
|
|
611
|
+
if (autoReloadTimer !== null) {
|
|
612
|
+
window.clearInterval(autoReloadTimer);
|
|
613
|
+
autoReloadTimer = null;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const applyAutoReloadValue = (rawValue) => {
|
|
617
|
+
const nextValue = Array.from(select.options).some((option) => option.value === rawValue)
|
|
618
|
+
? rawValue
|
|
619
|
+
: "off";
|
|
620
|
+
select.value = nextValue;
|
|
621
|
+
const autoReloadEnabled = nextValue !== "off";
|
|
622
|
+
manualButton.hidden = autoReloadEnabled;
|
|
623
|
+
clearAutoReloadTimer();
|
|
624
|
+
if (!autoReloadEnabled) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const intervalMs = Number.parseInt(nextValue, 10);
|
|
628
|
+
if (Number.isFinite(intervalMs) && intervalMs > 0) {
|
|
629
|
+
autoReloadTimer = window.setInterval(() => {
|
|
630
|
+
window.location.reload();
|
|
631
|
+
}, intervalMs);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
let storedValue = "off";
|
|
636
|
+
try {
|
|
637
|
+
storedValue = window.localStorage.getItem(AUTO_RELOAD_STORAGE_KEY) ?? "off";
|
|
638
|
+
} catch {
|
|
639
|
+
storedValue = "off";
|
|
640
|
+
}
|
|
641
|
+
applyAutoReloadValue(storedValue);
|
|
642
|
+
|
|
643
|
+
select.addEventListener("change", () => {
|
|
644
|
+
const nextValue = select.value;
|
|
645
|
+
try {
|
|
646
|
+
if (nextValue === "off") {
|
|
647
|
+
window.localStorage.removeItem(AUTO_RELOAD_STORAGE_KEY);
|
|
648
|
+
} else {
|
|
649
|
+
window.localStorage.setItem(AUTO_RELOAD_STORAGE_KEY, nextValue);
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
// Ignore localStorage write failures.
|
|
653
|
+
}
|
|
654
|
+
applyAutoReloadValue(nextValue);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
window.addEventListener("beforeunload", clearAutoReloadTimer);
|
|
658
|
+
})();
|
|
659
|
+
</script>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function deriveInboxSignalFromSnapshot(snapshot) {
|
|
663
|
+
if (!snapshot) {
|
|
664
|
+
return "unknown";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const outerState = formatStateToken(snapshot.outerState, "unknown");
|
|
668
|
+
const outerAction = formatStateToken(snapshot.outerAction, "unknown");
|
|
669
|
+
const statusClass = formatStateToken(snapshot.statusClass, "unknown");
|
|
670
|
+
const copilotState = formatStateToken(snapshot.layers?.copilot?.currentState);
|
|
671
|
+
const sameHeadCleanConverged = snapshot.layers?.copilot?.sameHeadCleanConverged === true;
|
|
672
|
+
const copilotLoopDisposition = formatStateToken(snapshot.layers?.copilot?.loopDisposition);
|
|
673
|
+
const reviewerState = formatStateToken(snapshot.layers?.reviewer?.currentState);
|
|
674
|
+
|
|
675
|
+
if (snapshot.needsAttention === true
|
|
676
|
+
|| outerState === OUTER_STATE.NEEDS_RECONCILE
|
|
677
|
+
|| outerState === OUTER_STATE.STOP_NEEDS_HUMAN
|
|
678
|
+
|| outerAction === "stop"
|
|
679
|
+
|| statusClass === "blocked"
|
|
680
|
+
|| copilotState === "unresolved_feedback_present"
|
|
681
|
+
|| copilotState === "already_fixed_needs_reply_resolve") {
|
|
682
|
+
return "attention";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (sameHeadCleanConverged || copilotLoopDisposition === "clean_converged") {
|
|
686
|
+
return "gate";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (copilotState === "waiting_for_copilot_review"
|
|
690
|
+
|| reviewerState === "waiting_for_author_followup"
|
|
691
|
+
|| reviewerState === "waiting_for_re_request") {
|
|
692
|
+
return "waiting";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (outerState === OUTER_STATE.HANDOFF_TO_COPILOT_LOOP
|
|
696
|
+
|| outerState === OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP
|
|
697
|
+
|| outerAction === "reenter_copilot_loop"
|
|
698
|
+
|| outerAction === "reenter_reviewer_loop") {
|
|
699
|
+
return "attention";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (copilotState === "waiting_for_ci") {
|
|
703
|
+
return "pending";
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (outerState === OUTER_STATE.DONE_TERMINAL || statusClass === "done") {
|
|
707
|
+
return "ready";
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (snapshot.sourceMode === "unavailable") {
|
|
711
|
+
return "unknown";
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return "waiting";
|
|
715
|
+
}
|