dev-loops 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Render the handoff envelope as a structured HTML section for the inspect-run viewer.
3
+ *
4
+ * Depends on the handoff envelope module from @dev-loops/core.
5
+ */
6
+ import { escapeHtml } from "./shared.mjs";
7
+
8
+ const LABELS = {
9
+ kind: "Kind",
10
+ repo: "Repository",
11
+ pr: "PR",
12
+ issue: "Issue",
13
+ branch: "Branch",
14
+ phase: "Phase",
15
+ currentGate: "Current gate",
16
+ currentHeadSha: "Head SHA",
17
+ ciStatus: "CI status",
18
+ unresolvedThreadCount: "Unresolved threads",
19
+ copilotRoundCount: "Completed rounds",
20
+ maxCopilotRounds: "Round limit",
21
+ executionMode: "Execution mode",
22
+ nextAction: "Next action",
23
+ asyncStartMode: "Async start mode",
24
+ requireDraftFirst: "Require draft first",
25
+ cwd: "Working directory",
26
+ worktreeRequired: "Worktree required",
27
+ angles: "Review angles",
28
+ excludeAngles: "Excluded angles",
29
+ blockCleanOnFindingSeverities: "Block on severities",
30
+ requireCi: "Require CI",
31
+ evidence: "Evidence",
32
+ maxFinalizationTurns: "Max finalization turns",
33
+ needsAttentionAfterMs: "Needs attention after",
34
+ activeNoticeAfterMs: "Active notice after",
35
+ };
36
+
37
+ function humanizeKey(key) {
38
+ if (LABELS[key]) {
39
+ return LABELS[key];
40
+ }
41
+ return String(key)
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
43
+ .replaceAll("_", " ")
44
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
45
+ }
46
+
47
+ function normalizeToken(value) {
48
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
49
+ }
50
+
51
+ function formatDurationMs(value) {
52
+ if (typeof value !== "number" || !Number.isFinite(value)) {
53
+ return renderPlainValue(value);
54
+ }
55
+ const seconds = value / 1000;
56
+ if (seconds < 60) {
57
+ return `${escapeHtml(String(seconds))} sec · ${escapeHtml(String(value))} ms`;
58
+ }
59
+ const minutes = seconds / 60;
60
+ if (minutes < 60) {
61
+ return `${escapeHtml(String(minutes))} min · ${escapeHtml(String(value))} ms`;
62
+ }
63
+ const hours = minutes / 60;
64
+ return `${escapeHtml(String(hours))} hr · ${escapeHtml(String(value))} ms`;
65
+ }
66
+
67
+ function renderBadge(label, tone = "neutral", extraClass = "") {
68
+ return `<span class="handoff-badge handoff-badge-${escapeHtml(tone)}${extraClass ? ` ${escapeHtml(extraClass)}` : ""}">${escapeHtml(String(label))}</span>`;
69
+ }
70
+
71
+ function renderBooleanBadge(value, { trueLabel = "Yes", falseLabel = "No" } = {}) {
72
+ if (value === null || value === undefined) {
73
+ return `<span class="handoff-empty-value">not set</span>`;
74
+ }
75
+ return value ? renderBadge(trueLabel, "success") : renderBadge(falseLabel, "muted");
76
+ }
77
+
78
+ function toneForGate(value) {
79
+ const token = normalizeToken(value);
80
+ if (["clean", "ready", "pass", "passed", "approved"].includes(token)) return "success";
81
+ if (["draft", "pending", "queued", "review"].includes(token)) return "warning";
82
+ if (["fail", "failed", "blocked", "rejected"].includes(token)) return "danger";
83
+ return "info";
84
+ }
85
+
86
+ function toneForCi(value) {
87
+ const token = normalizeToken(value);
88
+ if (["success", "passed", "green"].includes(token)) return "success";
89
+ if (["failure", "failed", "error", "red"].includes(token)) return "danger";
90
+ if (["pending", "running", "queued", "in_progress"].includes(token)) return "warning";
91
+ if (["skipped", "cancelled", "canceled"].includes(token)) return "muted";
92
+ return "neutral";
93
+ }
94
+
95
+ function toneForSeverity(value) {
96
+ const token = normalizeToken(value);
97
+ if (["required", "must-fix", "blocker", "high"].includes(token)) return "danger";
98
+ if (["worth-fixing-now", "warning", "medium"].includes(token)) return "warning";
99
+ return "info";
100
+ }
101
+
102
+ function renderPlainValue(value) {
103
+ if (value === null || value === undefined) {
104
+ return `<span class="handoff-empty-value">not set</span>`;
105
+ }
106
+ return escapeHtml(String(value));
107
+ }
108
+
109
+ function renderInlineValue(value, key = "") {
110
+ if (value === null || value === undefined) {
111
+ return `<span class="handoff-empty-value">not set</span>`;
112
+ }
113
+ if (key === "currentGate") {
114
+ return renderBadge(value, toneForGate(value));
115
+ }
116
+ if (key === "ciStatus") {
117
+ return renderBadge(value, toneForCi(value));
118
+ }
119
+ if (["executionMode", "asyncStartMode", "kind"].includes(key)) {
120
+ return renderBadge(value, "info");
121
+ }
122
+ if (["requireDraftFirst", "worktreeRequired", "requireCi"].includes(key)) {
123
+ return renderBooleanBadge(value);
124
+ }
125
+ if (["needsAttentionAfterMs", "activeNoticeAfterMs"].includes(key)) {
126
+ return formatDurationMs(value);
127
+ }
128
+ if (key === "currentHeadSha" && typeof value === "string") {
129
+ const shortSha = value.length > 12 ? value.slice(0, 12) : value;
130
+ return `<code title="${escapeHtml(value)}">${escapeHtml(shortSha)}</code>`;
131
+ }
132
+ return renderPlainValue(value);
133
+ }
134
+
135
+ function renderChipList(items, tone = "neutral", { code = false } = {}) {
136
+ if (!Array.isArray(items) || items.length === 0) {
137
+ return `<ul class="handoff-chip-list"><li>${renderBadge("none", "muted")}</li></ul>`;
138
+ }
139
+ return `<ul class="handoff-chip-list">${items.map((item) => `<li>${code ? `<code>${escapeHtml(String(item))}</code>` : renderBadge(item, tone)}</li>`).join("")}</ul>`;
140
+ }
141
+
142
+ function renderReadList(items) {
143
+ if (!Array.isArray(items) || items.length === 0) {
144
+ return `<p class="handoff-empty-copy">none</p>`;
145
+ }
146
+ return `<ol class="handoff-read-list">${items.map((item) => `<li><code>${escapeHtml(String(item))}</code></li>`).join("")}</ol>`;
147
+ }
148
+
149
+ function renderCriteria(criteria) {
150
+ if (!Array.isArray(criteria) || criteria.length === 0) {
151
+ return `<p class="handoff-empty-copy">none</p>`;
152
+ }
153
+ return `<ol class="handoff-criteria-list">${criteria.map((criterion) => `
154
+ <li class="handoff-criteria-item">
155
+ <div class="handoff-criteria-header">
156
+ ${renderBadge(criterion.severity ?? "not set", toneForSeverity(criterion.severity), "handoff-criteria-severity")}
157
+ <code>${escapeHtml(String(criterion.id ?? "unknown"))}</code>
158
+ </div>
159
+ <p>${escapeHtml(String(criterion.must ?? "not set"))}</p>
160
+ </li>
161
+ `).join("")}</ol>`;
162
+ }
163
+
164
+ function renderKeyValueList(entries, { compact = false } = {}) {
165
+ if (!Array.isArray(entries) || entries.length === 0) {
166
+ return `<p class="handoff-empty-copy">none</p>`;
167
+ }
168
+ return `<dl class="handoff-kv${compact ? " handoff-kv-compact" : ""}">${entries.map(({ key, label, valueHtml }) => `
169
+ <div class="handoff-kv-row"${key ? ` data-field="${escapeHtml(String(key))}"` : ""}>
170
+ <dt>${escapeHtml(label)}</dt>
171
+ <dd>${valueHtml}</dd>
172
+ </div>
173
+ `).join("")}</dl>`;
174
+ }
175
+
176
+ function renderObjectAsKeyValue(value) {
177
+ if (value === null || value === undefined) {
178
+ return `<span class="handoff-empty-value">not set</span>`;
179
+ }
180
+ const entries = Object.entries(value)
181
+ .filter(([, entryValue]) => entryValue !== undefined)
182
+ .map(([key, entryValue]) => ({
183
+ key,
184
+ label: humanizeKey(key),
185
+ valueHtml: renderEnvelopeValue(entryValue, key),
186
+ }));
187
+ return renderKeyValueList(entries, { compact: true });
188
+ }
189
+
190
+ function renderEnvelopeValue(value, key = "") {
191
+ if (value === null || value === undefined) {
192
+ return `<span class="handoff-empty-value">not set</span>`;
193
+ }
194
+ if (Array.isArray(value)) {
195
+ return renderReadList(value.map((item) => (typeof item === "object" ? JSON.stringify(item) : String(item))));
196
+ }
197
+ if (typeof value === "object") {
198
+ return renderObjectAsKeyValue(value);
199
+ }
200
+ return renderInlineValue(value, key);
201
+ }
202
+
203
+ function renderCard({ title, kicker = null, content, className = "" }) {
204
+ return `<article class="handoff-card${className ? ` ${escapeHtml(className)}` : ""}">
205
+ ${kicker ? `<p class="handoff-card-kicker">${escapeHtml(kicker)}</p>` : ""}
206
+ <h3>${escapeHtml(title)}</h3>
207
+ <div class="handoff-card-body">${content}</div>
208
+ </article>`;
209
+ }
210
+
211
+ function renderStatGrid(items) {
212
+ const normalizedItems = Array.isArray(items) ? items.filter(Boolean) : [];
213
+ if (normalizedItems.length === 0) {
214
+ return `<p class="handoff-empty-copy">none</p>`;
215
+ }
216
+ return `<div class="handoff-stat-grid">${normalizedItems.map(({ label, valueHtml }) => `
217
+ <div class="handoff-stat">
218
+ <span class="handoff-stat-label">${escapeHtml(String(label))}</span>
219
+ <span class="handoff-stat-value">${valueHtml}</span>
220
+ </div>
221
+ `).join("")}</div>`;
222
+ }
223
+
224
+ function buildIdentity(envelope) {
225
+ if (!envelope?.target) {
226
+ return "unknown";
227
+ }
228
+ const t = envelope.target;
229
+ if (t.kind === "pr" || t.kind === "issue") {
230
+ return `${t.repo || "?"}#${t.pr ?? t.issue ?? "?"}`;
231
+ }
232
+ if (t.kind === "local_branch") {
233
+ return t.branch ? `branch:${t.branch}` : "branch:?";
234
+ }
235
+ if (t.kind === "local_phase") {
236
+ return t.phase ? `phase:${t.phase}` : "phase:?";
237
+ }
238
+ return `${t.repo || "?"}#${t.pr ?? t.issue ?? "?"}`;
239
+ }
240
+
241
+ export function renderHandoffEnvelopeSection(envelope) {
242
+ if (!envelope) {
243
+ return `<section id="handoff-envelope-section" class="handoff-envelope-section">
244
+ <div class="handoff-empty-state">
245
+ <p class="handoff-card-kicker">Agent handoff</p>
246
+ <h2>Envelope unavailable</h2>
247
+ <p><em>Ensure buildDevLoopHandoffEnvelope() is callable and all inputs (resolver output, settings, gate state) are resolvable for the current PR.</em></p>
248
+ </div>
249
+ </section>`;
250
+ }
251
+
252
+ const identity = buildIdentity(envelope);
253
+ const targetEntries = envelope.target
254
+ ? Object.entries(envelope.target)
255
+ .filter(([, value]) => value !== undefined && value !== null)
256
+ .map(([key, value]) => ({ key, label: humanizeKey(key), valueHtml: renderInlineValue(value, key) }))
257
+ : [{ key: "target", label: "Target", valueHtml: `<span class="handoff-empty-value">missing</span>` }];
258
+ const currentStateEntries = [
259
+ { key: "currentGate", label: humanizeKey("currentGate"), valueHtml: renderInlineValue(envelope.currentGate, "currentGate") },
260
+ { key: "currentHeadSha", label: humanizeKey("currentHeadSha"), valueHtml: renderInlineValue(envelope.currentHeadSha, "currentHeadSha") },
261
+ { key: "ciStatus", label: humanizeKey("ciStatus"), valueHtml: renderInlineValue(envelope.ciStatus, "ciStatus") },
262
+ { key: "unresolvedThreadCount", label: humanizeKey("unresolvedThreadCount"), valueHtml: renderInlineValue(envelope.unresolvedThreadCount, "unresolvedThreadCount") },
263
+ { key: "copilotRoundCount", label: humanizeKey("copilotRoundCount"), valueHtml: renderInlineValue(envelope.copilotRoundCount, "copilotRoundCount") },
264
+ { key: "maxCopilotRounds", label: humanizeKey("maxCopilotRounds"), valueHtml: renderInlineValue(envelope.maxCopilotRounds, "maxCopilotRounds") },
265
+ { key: "executionMode", label: humanizeKey("executionMode"), valueHtml: renderInlineValue(envelope.executionMode, "executionMode") },
266
+ ];
267
+ const policyEntries = [
268
+ { key: "asyncStartMode", label: humanizeKey("asyncStartMode"), valueHtml: renderInlineValue(envelope.asyncStartMode, "asyncStartMode") },
269
+ { key: "requireDraftFirst", label: humanizeKey("requireDraftFirst"), valueHtml: renderInlineValue(envelope.requireDraftFirst, "requireDraftFirst") },
270
+ ];
271
+ const isolationEntries = [
272
+ { key: "cwd", label: humanizeKey("cwd"), valueHtml: renderInlineValue(envelope.cwd, "cwd") },
273
+ { key: "worktreeRequired", label: humanizeKey("worktreeRequired"), valueHtml: renderInlineValue(envelope.worktreeRequired, "worktreeRequired") },
274
+ ];
275
+ const controlEntries = envelope.control ? [
276
+ { key: "needsAttentionAfterMs", label: humanizeKey("needsAttentionAfterMs"), valueHtml: renderInlineValue(envelope.control.needsAttentionAfterMs, "needsAttentionAfterMs") },
277
+ { key: "activeNoticeAfterMs", label: humanizeKey("activeNoticeAfterMs"), valueHtml: renderInlineValue(envelope.control.activeNoticeAfterMs, "activeNoticeAfterMs") },
278
+ ] : [];
279
+
280
+ return `<section id="handoff-envelope-section" class="handoff-envelope-section">
281
+ <div class="handoff-hero">
282
+ <div class="handoff-hero-copy">
283
+ <p class="handoff-card-kicker">Agent handoff</p>
284
+ <h2>${escapeHtml(identity)}</h2>
285
+ <p class="handoff-hero-meta">Derived${envelope.derivedAt ? ` at ${escapeHtml(envelope.derivedAt)}` : ""} · Version ${escapeHtml(String(envelope.handoffVersion ?? "unknown"))}</p>
286
+ <div class="handoff-hero-badges">
287
+ ${renderBadge(`gate: ${envelope.currentGate ?? "not set"}`, toneForGate(envelope.currentGate))}
288
+ ${renderBadge(`ci: ${envelope.ciStatus ?? "not set"}`, toneForCi(envelope.ciStatus))}
289
+ ${renderBadge(`mode: ${envelope.executionMode ?? "not set"}`, "info")}
290
+ </div>
291
+ </div>
292
+ <div class="handoff-hero-side">
293
+ ${renderCard({
294
+ title: "At a glance",
295
+ kicker: "Summary",
296
+ className: "handoff-card-tight",
297
+ content: renderStatGrid([
298
+ { label: "Artifact", valueHtml: renderInlineValue(envelope.target?.kind, "kind") },
299
+ { label: "Target", valueHtml: renderInlineValue(envelope.target?.pr ?? envelope.target?.issue ?? envelope.target?.branch ?? envelope.target?.phase, "target") },
300
+ { label: "Rounds", valueHtml: `<strong>${renderPlainValue(envelope.copilotRoundCount)}</strong> / ${renderPlainValue(envelope.maxCopilotRounds)}` },
301
+ { label: "Isolation", valueHtml: renderInlineValue(envelope.worktreeRequired, "worktreeRequired") },
302
+ { label: "Threads", valueHtml: renderPlainValue(envelope.unresolvedThreadCount) },
303
+ { label: "Stop rules", valueHtml: renderPlainValue(Array.isArray(envelope.stopRules) ? envelope.stopRules.length : 0) },
304
+ ]),
305
+ })}
306
+ </div>
307
+ </div>
308
+
309
+ <div class="handoff-layout">
310
+ <div class="handoff-column handoff-column-side">
311
+ ${renderCard({ title: "Target", kicker: "Identity", content: renderKeyValueList(targetEntries) })}
312
+ ${renderCard({ title: "Current state", kicker: "Live status", content: renderKeyValueList(currentStateEntries) })}
313
+ ${renderCard({ title: "Worktree / isolation", kicker: "Execution boundary", content: renderKeyValueList(isolationEntries) })}
314
+ ${controlEntries.length > 0 ? renderCard({ title: "Runtime control", kicker: "Watch timers", content: renderKeyValueList(controlEntries) }) : ""}
315
+ </div>
316
+
317
+ <div class="handoff-column">
318
+ ${renderCard({
319
+ title: "Work directive",
320
+ kicker: "Next step",
321
+ className: "handoff-card-emphasis",
322
+ content: `
323
+ <div class="handoff-next-action"><p>${renderInlineValue(envelope.nextAction, "nextAction")}</p></div>
324
+ <div class="handoff-subsection">
325
+ <h4>Required reads</h4>
326
+ ${renderReadList(envelope.requiredReads)}
327
+ </div>
328
+ `,
329
+ })}
330
+
331
+ ${envelope.gateConfig ? renderCard({
332
+ title: "Gate configuration",
333
+ kicker: "Review policy",
334
+ content: `
335
+ ${renderStatGrid([
336
+ { label: "Angles", valueHtml: renderPlainValue(Array.isArray(envelope.gateConfig.angles) ? envelope.gateConfig.angles.length : 0) },
337
+ { label: "Excluded", valueHtml: renderPlainValue(Array.isArray(envelope.gateConfig.excludeAngles) ? envelope.gateConfig.excludeAngles.length : 0) },
338
+ { label: "Blockers", valueHtml: renderPlainValue(Array.isArray(envelope.gateConfig.blockCleanOnFindingSeverities) ? envelope.gateConfig.blockCleanOnFindingSeverities.length : 0) },
339
+ { label: "Require CI", valueHtml: renderInlineValue(envelope.gateConfig.requireCi, "requireCi") },
340
+ ])}
341
+ <div class="handoff-subgrid">
342
+ <div class="handoff-subsection">
343
+ <h4>Review angles</h4>
344
+ ${renderChipList(envelope.gateConfig.angles, "info")}
345
+ </div>
346
+ <div class="handoff-subsection">
347
+ <h4>Excluded angles</h4>
348
+ ${renderChipList(envelope.gateConfig.excludeAngles, "muted")}
349
+ <h4>Block on severities</h4>
350
+ ${Array.isArray(envelope.gateConfig.blockCleanOnFindingSeverities) && envelope.gateConfig.blockCleanOnFindingSeverities.length > 0
351
+ ? `<ul class="handoff-chip-list">${envelope.gateConfig.blockCleanOnFindingSeverities.map((severity) => `<li>${renderBadge(severity, toneForSeverity(severity))}</li>`).join("")}</ul>`
352
+ : renderChipList([], "muted")}
353
+ </div>
354
+ </div>
355
+ `,
356
+ }) : ""}
357
+
358
+ ${renderCard({
359
+ title: "Policy",
360
+ kicker: "Operating rules",
361
+ content: `
362
+ ${renderKeyValueList(policyEntries)}
363
+ <div class="handoff-subsection">
364
+ <h4>Stop rules</h4>
365
+ ${renderChipList(envelope.stopRules, "warning")}
366
+ </div>
367
+ `,
368
+ })}
369
+
370
+ ${envelope.acceptance ? renderCard({
371
+ title: "Acceptance contract",
372
+ kicker: "Done means",
373
+ content: `
374
+ ${renderStatGrid([
375
+ { label: "Criteria", valueHtml: renderPlainValue(Array.isArray(envelope.acceptance.criteria) ? envelope.acceptance.criteria.length : 0) },
376
+ { label: "Evidence", valueHtml: renderPlainValue(Array.isArray(envelope.acceptance.evidence) ? envelope.acceptance.evidence.length : 0) },
377
+ { label: "Max finalization", valueHtml: renderInlineValue(envelope.acceptance.maxFinalizationTurns, "maxFinalizationTurns") },
378
+ ])}
379
+ <div class="handoff-subsection">
380
+ <h4>Criteria</h4>
381
+ ${renderCriteria(envelope.acceptance.criteria)}
382
+ </div>
383
+ <div class="handoff-subsection">
384
+ <h4>Evidence</h4>
385
+ ${renderChipList(envelope.acceptance.evidence, "neutral")}
386
+ </div>
387
+ `,
388
+ }) : ""}
389
+
390
+ ${envelope.overrides ? renderCard({
391
+ title: "Explicit overrides",
392
+ kicker: "Manual exceptions",
393
+ content: renderObjectAsKeyValue(envelope.overrides),
394
+ }) : ""}
395
+ </div>
396
+ </div>
397
+ </section>`;
398
+ }