@mmerterden/multi-agent-pipeline 20.29.0 → 20.33.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 (227) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +50 -5
  3. package/README.tr.md +64 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +9 -9
  7. package/install/_unattended-profile.mjs +61 -0
  8. package/install/catalog-history.json +1 -1
  9. package/install/claude.mjs +19 -3
  10. package/install/codex.mjs +2 -0
  11. package/install/copilot.mjs +13 -0
  12. package/install/index.mjs +10 -1
  13. package/install/templates/claude-hooks.json +12 -1
  14. package/manifest.json +230 -88
  15. package/package.json +3 -1
  16. package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
  17. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
  18. package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
  19. package/pipeline/commands/multi-agent/office-on/SKILL.md +51 -0
  20. package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
  21. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
  22. package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
  23. package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
  24. package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
  25. package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
  26. package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
  27. package/pipeline/commands/multi-agent/sync/SKILL.md +15 -3
  28. package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
  29. package/pipeline/contract/CHANGELOG.md +108 -0
  30. package/pipeline/contract/README.md +72 -12
  31. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  32. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  33. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  34. package/pipeline/contract/fixtures/launch-plan.json +1 -1
  35. package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
  36. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  37. package/pipeline/contract/fixtures/runs-failed.json +6 -1
  38. package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
  39. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
  40. package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
  41. package/pipeline/contract/fixtures/runs-running.json +6 -1
  42. package/pipeline/contract/manifest.json +506 -27
  43. package/pipeline/contract/types/index.d.ts +829 -4
  44. package/pipeline/lib/bg-launch.mjs +106 -0
  45. package/pipeline/lib/code-digest.mjs +49 -0
  46. package/pipeline/lib/credential-resolve.mjs +42 -2
  47. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
  48. package/pipeline/multi-agent-refs/features/office-threat-model.md +109 -0
  49. package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
  50. package/pipeline/multi-agent-refs/help/en.md +3 -0
  51. package/pipeline/multi-agent-refs/help/tr.md +3 -0
  52. package/pipeline/multi-agent-refs/phases/modes.md +7 -7
  53. package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
  54. package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
  55. package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
  56. package/pipeline/office/NOTICE.md +45 -0
  57. package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
  58. package/pipeline/office/assets/BIN.png +0 -0
  59. package/pipeline/office/assets/BOOKSHELF.png +0 -0
  60. package/pipeline/office/assets/CACTUS.png +0 -0
  61. package/pipeline/office/assets/CLOCK.png +0 -0
  62. package/pipeline/office/assets/COFFEE.png +0 -0
  63. package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
  64. package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
  65. package/pipeline/office/assets/COUNTER_SINK.png +0 -0
  66. package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
  67. package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
  68. package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
  69. package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
  70. package/pipeline/office/assets/DESK_FRONT.png +0 -0
  71. package/pipeline/office/assets/DESK_SIDE.png +0 -0
  72. package/pipeline/office/assets/DEVICE_STAND.png +0 -0
  73. package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
  74. package/pipeline/office/assets/FILING_CABINET.png +0 -0
  75. package/pipeline/office/assets/FRIDGE.png +0 -0
  76. package/pipeline/office/assets/HANGING_PLANT.png +0 -0
  77. package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
  78. package/pipeline/office/assets/LARGE_PLANT.png +0 -0
  79. package/pipeline/office/assets/MAILBOX.png +0 -0
  80. package/pipeline/office/assets/MOP_BUCKET.png +0 -0
  81. package/pipeline/office/assets/PC_BACK.png +0 -0
  82. package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
  83. package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
  84. package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
  85. package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
  86. package/pipeline/office/assets/PC_SIDE.png +0 -0
  87. package/pipeline/office/assets/PLANT.png +0 -0
  88. package/pipeline/office/assets/PLANT_2.png +0 -0
  89. package/pipeline/office/assets/POT.png +0 -0
  90. package/pipeline/office/assets/PRINTER.png +0 -0
  91. package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
  92. package/pipeline/office/assets/SERVER_RACK.png +0 -0
  93. package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
  94. package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
  95. package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
  96. package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
  97. package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
  98. package/pipeline/office/assets/SOFA_BACK.png +0 -0
  99. package/pipeline/office/assets/SOFA_FRONT.png +0 -0
  100. package/pipeline/office/assets/SOFA_SIDE.png +0 -0
  101. package/pipeline/office/assets/TABLE_FRONT.png +0 -0
  102. package/pipeline/office/assets/TOILET_STALL.png +0 -0
  103. package/pipeline/office/assets/VENDING.png +0 -0
  104. package/pipeline/office/assets/WASHBASIN.png +0 -0
  105. package/pipeline/office/assets/WATER_COOLER.png +0 -0
  106. package/pipeline/office/assets/WHITEBOARD.png +0 -0
  107. package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
  108. package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
  109. package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
  110. package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
  111. package/pipeline/office/assets/char_0.png +0 -0
  112. package/pipeline/office/assets/char_1.png +0 -0
  113. package/pipeline/office/assets/char_2.png +0 -0
  114. package/pipeline/office/assets/char_3.png +0 -0
  115. package/pipeline/office/assets/char_4.png +0 -0
  116. package/pipeline/office/assets/char_5.png +0 -0
  117. package/pipeline/office/assets/floor_0.png +0 -0
  118. package/pipeline/office/assets/floor_1.png +0 -0
  119. package/pipeline/office/assets/floor_2.png +0 -0
  120. package/pipeline/office/assets/floor_3.png +0 -0
  121. package/pipeline/office/assets/floor_4.png +0 -0
  122. package/pipeline/office/assets/floor_5.png +0 -0
  123. package/pipeline/office/assets/floor_6.png +0 -0
  124. package/pipeline/office/assets/floor_7.png +0 -0
  125. package/pipeline/office/assets/floor_8.png +0 -0
  126. package/pipeline/office/assets/wall_0.png +0 -0
  127. package/pipeline/office/index.html +8431 -0
  128. package/pipeline/office/toolkit.json +3 -0
  129. package/pipeline/schemas/activity-chat.schema.json +78 -0
  130. package/pipeline/schemas/activity-roles.json +48 -0
  131. package/pipeline/schemas/activity-roles.schema.json +49 -0
  132. package/pipeline/schemas/activity.schema.json +613 -0
  133. package/pipeline/schemas/agent-state.schema.json +493 -115
  134. package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
  135. package/pipeline/schemas/autopilot-on.schema.json +111 -0
  136. package/pipeline/schemas/contract-error.schema.json +45 -4
  137. package/pipeline/schemas/issue-maturity.schema.json +68 -0
  138. package/pipeline/schemas/job-change.schema.json +72 -0
  139. package/pipeline/schemas/job-request.schema.json +38 -0
  140. package/pipeline/schemas/launch.json +4 -4
  141. package/pipeline/schemas/ledger.schema.json +202 -0
  142. package/pipeline/schemas/office-credentials.schema.json +71 -0
  143. package/pipeline/schemas/office-jobs.json +253 -0
  144. package/pipeline/schemas/office-jobs.schema.json +62 -0
  145. package/pipeline/schemas/office-memory.schema.json +105 -0
  146. package/pipeline/schemas/office-settings-request.schema.json +30 -0
  147. package/pipeline/schemas/office-settings.schema.json +67 -0
  148. package/pipeline/schemas/permissions.schema.json +47 -0
  149. package/pipeline/schemas/phases.json +10 -2
  150. package/pipeline/schemas/phases.schema.json +86 -19
  151. package/pipeline/schemas/prefs.schema.json +19 -0
  152. package/pipeline/schemas/prs.schema.json +78 -0
  153. package/pipeline/schemas/prune-logs-request.schema.json +26 -0
  154. package/pipeline/schemas/prune-logs.schema.json +52 -0
  155. package/pipeline/schemas/repo-change.schema.json +54 -0
  156. package/pipeline/schemas/repo-request.schema.json +29 -0
  157. package/pipeline/schemas/repos.schema.json +49 -1
  158. package/pipeline/schemas/review-verdict-request.schema.json +20 -0
  159. package/pipeline/schemas/review-verdict.schema.json +62 -0
  160. package/pipeline/schemas/routine-delete.schema.json +59 -0
  161. package/pipeline/schemas/routines.schema.json +38 -0
  162. package/pipeline/schemas/run-evidence.schema.json +29 -0
  163. package/pipeline/schemas/run-results.schema.json +218 -0
  164. package/pipeline/schemas/runs-index.schema.json +257 -46
  165. package/pipeline/schemas/service-read.schema.json +58 -0
  166. package/pipeline/schemas/stack-change.schema.json +82 -0
  167. package/pipeline/schemas/stack-request.schema.json +33 -0
  168. package/pipeline/schemas/stack.schema.json +59 -0
  169. package/pipeline/schemas/steer-request.schema.json +25 -0
  170. package/pipeline/schemas/steer.schema.json +59 -0
  171. package/pipeline/schemas/system-status.schema.json +111 -0
  172. package/pipeline/schemas/tool-call-request.schema.json +19 -0
  173. package/pipeline/schemas/tool-call.schema.json +47 -0
  174. package/pipeline/schemas/tools.schema.json +65 -0
  175. package/pipeline/schemas/unattended-profile.json +7 -2
  176. package/pipeline/scripts/_active-run.mjs +2 -1
  177. package/pipeline/scripts/_cost.mjs +66 -1
  178. package/pipeline/scripts/_office-smoke-lib.sh +115 -0
  179. package/pipeline/scripts/_spawn-async.mjs +77 -0
  180. package/pipeline/scripts/_transcript-tail.mjs +274 -0
  181. package/pipeline/scripts/activity-snapshot.mjs +1223 -0
  182. package/pipeline/scripts/autopilot-control.mjs +214 -9
  183. package/pipeline/scripts/autopilot-runner.mjs +7 -5
  184. package/pipeline/scripts/contract-server.mjs +1853 -197
  185. package/pipeline/scripts/cost-analyze.mjs +2 -53
  186. package/pipeline/scripts/cost-table.json +1 -0
  187. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
  188. package/pipeline/scripts/job-launch.mjs +477 -0
  189. package/pipeline/scripts/launch-request.mjs +94 -9
  190. package/pipeline/scripts/office-build.mjs +226 -0
  191. package/pipeline/scripts/office-control.mjs +410 -0
  192. package/pipeline/scripts/office-credentials.mjs +106 -0
  193. package/pipeline/scripts/office-ledger.mjs +409 -0
  194. package/pipeline/scripts/office-memory.mjs +200 -0
  195. package/pipeline/scripts/office-prune.mjs +125 -0
  196. package/pipeline/scripts/office-settings.mjs +145 -0
  197. package/pipeline/scripts/phase-tracker.sh +12 -5
  198. package/pipeline/scripts/prune-logs.sh +53 -2
  199. package/pipeline/scripts/routine-registry.mjs +42 -6
  200. package/pipeline/scripts/run-results.mjs +373 -0
  201. package/pipeline/scripts/runs-index.mjs +62 -2
  202. package/pipeline/scripts/service-read.mjs +244 -0
  203. package/pipeline/scripts/stack-toggle.mjs +285 -0
  204. package/pipeline/scripts/steer-run.mjs +97 -0
  205. package/pipeline/scripts/system-status.mjs +196 -0
  206. package/pipeline/scripts/task-mirror.mjs +150 -0
  207. package/pipeline/scripts/task-mirror.sh +20 -0
  208. package/pipeline/scripts/toolkit-tools.mjs +625 -0
  209. package/pipeline/scripts/transcript-index.mjs +4 -1
  210. package/pipeline/scripts/uninstall.mjs +3 -0
  211. package/pipeline/scripts/work-items.mjs +473 -0
  212. package/pipeline/skills/.skill-manifest.json +23 -11
  213. package/pipeline/skills/.skills-index.json +34 -1
  214. package/pipeline/skills/shared/README.md +6 -3
  215. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
  216. package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
  217. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +21 -0
  218. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
  219. package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
  220. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
  221. package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
  222. package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
  223. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
  224. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
  225. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  226. package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
  227. package/pipeline/skills/skills-index.md +4 -1
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file run-results.mjs - what a run produced, read from its own files: the
4
+ * diff summary, the review findings, the test outcome, the screenshots and,
5
+ * for an analysis run, the documents it wrote.
6
+ *
7
+ * Every source is a file or a state field the phases already write; nothing is
8
+ * computed here that a phase did not record:
9
+ *
10
+ * diff agent-state `diffRisk` (diff-risk-score.mjs, Phase 3), else the
11
+ * file and line counts of `.review-diff.txt`
12
+ * findings `triage-output.json` (Phase 3 triage), else agent-state
13
+ * `review.findings` (a standalone /multi-agent:review)
14
+ * tests agent-state `uiTest` and the `test-summary` gate entries
15
+ * (test-summary.mjs)
16
+ * evidence `.pipeline/evidence/*.png|jpg` and agent-state `visualEvidence`
17
+ * analysis agent-state `analysisSpec.outputs` (the documents' text) and
18
+ * `analysis.docPath[]`
19
+ *
20
+ * Each file is looked for in the run's log directory, its `artifacts/` copy
21
+ * (worktree-finalize.sh salvages there) and, while the run is live, its
22
+ * worktree. The state's worktreePath is used only when git lists it as a
23
+ * worktree of the state's projectRoot. A part with no source is null, never
24
+ * an empty success.
25
+ *
26
+ * An analysis document is read only when its real path sits inside one of
27
+ * those roots and it is Markdown or HTML; any other path in the state keeps
28
+ * its name with `text: null`, so a state file cannot point the reader at an
29
+ * arbitrary file.
30
+ *
31
+ * Text a person or tool wrote (finding text, test names, document bodies) is
32
+ * scrubbed and run through the credential redactor; paths are reduced to file
33
+ * names.
34
+ *
35
+ * Usage:
36
+ * run-results.mjs <task-id> [--json]
37
+ * run-results.mjs <task-id> --evidence <name> one image, base64, as JSON
38
+ *
39
+ * Exit: 0 printed, 2 usage, 3 no such run or image.
40
+ *
41
+ * @module pipeline/scripts/run-results
42
+ */
43
+
44
+ import { execFileSync } from "node:child_process";
45
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
46
+ import { basename, join, sep } from "node:path";
47
+ import { selectRuns } from "./runs-index.mjs";
48
+ import { createRedactor } from "./transcript-index.mjs";
49
+ import { readRegularJson } from "../lib/regular-file.mjs";
50
+ import { runMain } from "../lib/fatal.mjs";
51
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
52
+
53
+ export const CONTRACT_VERSION = "1.0.0";
54
+ export const EVIDENCE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.(?:png|jpe?g)$/i;
55
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
56
+ const MAX_DOC_CHARS = 200_000;
57
+ const MAX_FINDINGS = 200;
58
+ const DOC_EXT = /\.(?:md|markdown|html?)$/i;
59
+
60
+ export class ResultsError extends Error {}
61
+
62
+ function readState(dir) {
63
+ for (const p of [join(dir, "agent-state.json"), join(dir, "artifacts", "agent-state.json")]) {
64
+ if (existsSync(p)) return readRegularJson(p, null) ?? {};
65
+ }
66
+ return {};
67
+ }
68
+
69
+ const realOrNull = (p) => {
70
+ try {
71
+ return realpathSync(p);
72
+ } catch {
73
+ return null;
74
+ }
75
+ };
76
+
77
+ /**
78
+ * The state's worktreePath when git lists it as a worktree of the state's
79
+ * projectRoot, else null.
80
+ */
81
+ export function verifiedWorktree(state) {
82
+ const wt = typeof state.worktreePath === "string" ? realOrNull(state.worktreePath) : null;
83
+ const root = typeof state.projectRoot === "string" ? realOrNull(state.projectRoot) : null;
84
+ if (!wt || !root) return null;
85
+ let listing;
86
+ try {
87
+ listing = execFileSync("git", ["-C", root, "worktree", "list", "--porcelain"], {
88
+ encoding: "utf8",
89
+ timeout: 5000,
90
+ stdio: ["ignore", "pipe", "ignore"],
91
+ });
92
+ } catch {
93
+ return null;
94
+ }
95
+ for (const line of listing.split("\n")) {
96
+ if (line.startsWith("worktree ") && realOrNull(line.slice(9)) === wt) return wt;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /** The directories a run's files may be in, most durable first. */
102
+ function roots(run, state) {
103
+ const out = [run.dir, join(run.dir, "artifacts")];
104
+ const wt = verifiedWorktree(state);
105
+ if (wt) out.push(wt);
106
+ return out;
107
+ }
108
+
109
+ /** The real path of `p` when it is a Markdown or HTML file inside one of `dirs`. */
110
+ function containedDoc(p, dirs) {
111
+ const real = realOrNull(p);
112
+ if (!real || !DOC_EXT.test(real)) return null;
113
+ for (const d of dirs) {
114
+ const rd = realOrNull(d);
115
+ if (rd && real.startsWith(rd + sep)) return real;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function firstExisting(dirs, rel) {
121
+ for (const d of dirs) {
122
+ const p = join(d, rel);
123
+ try {
124
+ if (statSync(p).isFile()) return p;
125
+ } catch {
126
+ // Not in this root.
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function diffPart(state, dirs) {
133
+ const r = state.diffRisk;
134
+ if (r && typeof r === "object" && Number.isInteger(r.files)) {
135
+ return {
136
+ files: r.files,
137
+ added: Number.isInteger(r.loc_added) ? r.loc_added : null,
138
+ removed: Number.isInteger(r.loc_removed) ? r.loc_removed : null,
139
+ maxScore: typeof r.max_score === "number" ? r.max_score : null,
140
+ signals: Array.isArray(r.signals) ? r.signals.map(String) : [],
141
+ changed: [],
142
+ source: "diffRisk",
143
+ };
144
+ }
145
+ const p = firstExisting(dirs, ".review-diff.txt");
146
+ if (!p) return null;
147
+ const files = new Set();
148
+ let added = 0;
149
+ let removed = 0;
150
+ for (const line of readFileSync(p, "utf8").split("\n")) {
151
+ const m = /^\+\+\+ b\/(.+)$/.exec(line);
152
+ if (m) files.add(m[1]);
153
+ else if (line.startsWith("+") && !line.startsWith("+++")) added++;
154
+ else if (line.startsWith("-") && !line.startsWith("---")) removed++;
155
+ }
156
+ return {
157
+ files: files.size,
158
+ added,
159
+ removed,
160
+ maxScore: null,
161
+ signals: [],
162
+ changed: [...files].slice(0, 200).map((f) => basename(f)),
163
+ source: ".review-diff.txt",
164
+ };
165
+ }
166
+
167
+ function findingsPart(state, dirs, R) {
168
+ const p = firstExisting(dirs, "triage-output.json");
169
+ const triage = p ? readRegularJson(p, null) : null;
170
+ const shape = (f, verdict) => ({
171
+ severity: String(f?.severity ?? "unknown"),
172
+ file: f?.file ? basename(String(f.file)) : null,
173
+ line: Number.isInteger(f?.line) ? f.line : null,
174
+ issue: R.excerpt(f?.issue ?? f?.title ?? f?.message ?? "", 400),
175
+ fix: f?.fix ? R.excerpt(f.fix, 400) : null,
176
+ reviewer: f?.reviewer ? String(f.reviewer) : null,
177
+ verdict,
178
+ });
179
+ if (triage && typeof triage === "object") {
180
+ const items = [];
181
+ for (const v of ["accepted", "deferred", "rejected"]) {
182
+ for (const f of Array.isArray(triage[v]) ? triage[v] : []) items.push(shape(f, v));
183
+ }
184
+ return {
185
+ approved: typeof triage.approved === "boolean" ? triage.approved : null,
186
+ items: items.slice(0, MAX_FINDINGS),
187
+ total: items.length,
188
+ source: "triage-output.json",
189
+ };
190
+ }
191
+ const review = state.review;
192
+ if (review && Array.isArray(review.findings)) {
193
+ const items = review.findings.map((f) => shape(f, f?.status ?? f?.verdict ?? "accepted"));
194
+ return {
195
+ approved: null,
196
+ items: items.slice(0, MAX_FINDINGS),
197
+ total: items.length,
198
+ source: "agent-state review",
199
+ };
200
+ }
201
+ return null;
202
+ }
203
+
204
+ function testsPart(state, R) {
205
+ const gates = Array.isArray(state.gates) ? state.gates : [];
206
+ const summaries = gates.filter(
207
+ (g) => g && (g.gate === "test-summary" || g.name === "test-summary"),
208
+ );
209
+ const last = summaries.at(-1) ?? null;
210
+ const ui = state.uiTest && typeof state.uiTest === "object" ? state.uiTest : null;
211
+ if (!last && !ui) return null;
212
+ const count = (k) => {
213
+ const v = last?.counts?.[k] ?? last?.[k];
214
+ return Number.isInteger(v) ? v : null;
215
+ };
216
+ return {
217
+ status: last?.status ?? last?.verdict ?? ui?.status ?? null,
218
+ passed: count("passed"),
219
+ failed: count("failed"),
220
+ skipped: count("skipped"),
221
+ uiTest: ui
222
+ ? {
223
+ ran: ui.ran === true,
224
+ status: ui.status ?? null,
225
+ notRunReason: ui.notRunReason ? R.excerpt(ui.notRunReason, 200) : null,
226
+ }
227
+ : null,
228
+ source: last ? "test-summary gate" : "agent-state uiTest",
229
+ };
230
+ }
231
+
232
+ function evidenceDirs(dirs) {
233
+ return dirs.map((d) => join(d, ".pipeline", "evidence")).filter((d) => existsSync(d));
234
+ }
235
+
236
+ function evidencePart(state, dirs) {
237
+ const names = new Map();
238
+ for (const d of evidenceDirs(dirs)) {
239
+ let list;
240
+ try {
241
+ list = readdirSync(d);
242
+ } catch {
243
+ continue;
244
+ }
245
+ for (const f of list) if (EVIDENCE_NAME.test(f) && !names.has(f)) names.set(f, null);
246
+ }
247
+ const ve = state.visualEvidence;
248
+ for (const side of ["before", "after"]) {
249
+ for (const e of Array.isArray(ve?.[side]) ? ve[side] : []) {
250
+ const f = typeof e?.file === "string" ? basename(e.file) : null;
251
+ if (f && names.has(f)) names.set(f, side);
252
+ }
253
+ }
254
+ return [...names.entries()].sort().map(([name, phase]) => ({ name, phase }));
255
+ }
256
+
257
+ function analysisPart(state, dirs, R) {
258
+ const outputs = state.analysisSpec?.outputs;
259
+ const paths = [
260
+ ...(Array.isArray(outputs?.localPaths) ? outputs.localPaths : []),
261
+ ...(Array.isArray(state.analysis?.docPath) ? state.analysis.docPath : []),
262
+ ].filter((p) => typeof p === "string");
263
+ if (!paths.length && !outputs) return null;
264
+ const documents = [];
265
+ for (const p of [...new Set(paths)]) {
266
+ const real = containedDoc(p, dirs);
267
+ try {
268
+ if (!real || !statSync(real).isFile()) throw new Error("not readable here");
269
+ const text = readFileSync(real, "utf8").slice(0, MAX_DOC_CHARS);
270
+ documents.push({ name: basename(p), text: R.clean(text) });
271
+ } catch {
272
+ documents.push({ name: basename(p), text: null });
273
+ }
274
+ }
275
+ return {
276
+ documents,
277
+ confluencePages: Array.isArray(outputs?.confluencePageUrls)
278
+ ? outputs.confluencePageUrls.length
279
+ : 0,
280
+ jiraIssueKey:
281
+ typeof outputs?.jiraIssueKey === "string" &&
282
+ /^[A-Z][A-Z0-9_]*-[0-9]+$/.test(outputs.jiraIssueKey)
283
+ ? outputs.jiraIssueKey
284
+ : null,
285
+ };
286
+ }
287
+
288
+ function runFor(taskId) {
289
+ const { found, records } = selectRuns({ taskId });
290
+ if (!found || !records.length) throw new ResultsError("no such run");
291
+ return records[0];
292
+ }
293
+
294
+ /** Everything a run produced, as one document. */
295
+ export function runResults(taskId) {
296
+ const run = runFor(taskId);
297
+ const state = readState(run.dir);
298
+ const dirs = roots(run, state);
299
+ const R = createRedactor();
300
+ return {
301
+ contractVersion: CONTRACT_VERSION,
302
+ taskId: run.taskId,
303
+ diff: diffPart(state, dirs),
304
+ findings: findingsPart(state, dirs, R),
305
+ tests: testsPart(state, R),
306
+ evidence: evidencePart(state, dirs),
307
+ analysis: analysisPart(state, dirs, R),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * One screenshot as base64. The name must be a bare image file name found in
313
+ * one of the run's evidence directories; its real path must stay inside it.
314
+ */
315
+ export function runEvidence(taskId, name) {
316
+ if (typeof name !== "string" || !EVIDENCE_NAME.test(name))
317
+ throw new ResultsError("no such image");
318
+ const run = runFor(taskId);
319
+ const state = readState(run.dir);
320
+ for (const d of evidenceDirs(roots(run, state))) {
321
+ const p = join(d, name);
322
+ let real;
323
+ let realDir;
324
+ try {
325
+ real = realpathSync(p);
326
+ realDir = realpathSync(d);
327
+ } catch {
328
+ continue;
329
+ }
330
+ if (!real.startsWith(realDir + sep)) continue;
331
+ const st = statSync(real);
332
+ if (!st.isFile() || st.size > MAX_IMAGE_BYTES) continue;
333
+ const mime = /\.png$/i.test(name) ? "image/png" : "image/jpeg";
334
+ return {
335
+ contractVersion: CONTRACT_VERSION,
336
+ taskId: run.taskId,
337
+ name,
338
+ mime,
339
+ data: readFileSync(real).toString("base64"),
340
+ };
341
+ }
342
+ throw new ResultsError("no such image");
343
+ }
344
+
345
+ function main(argv) {
346
+ const [taskId, ...rest] = argv;
347
+ if (!taskId) {
348
+ process.stderr.write("usage: run-results.mjs <task-id> [--json] | --evidence <name>\n");
349
+ return 2;
350
+ }
351
+ try {
352
+ if (rest[0] === "--evidence") {
353
+ process.stdout.write(JSON.stringify(runEvidence(taskId, rest[1])) + "\n");
354
+ return 0;
355
+ }
356
+ if (rest.some((a) => a !== "--json")) {
357
+ process.stderr.write("usage: run-results.mjs <task-id> [--json] | --evidence <name>\n");
358
+ return 2;
359
+ }
360
+ process.stdout.write(JSON.stringify(runResults(taskId), null, 2) + "\n");
361
+ return 0;
362
+ } catch (err) {
363
+ if (!(err instanceof ResultsError)) throw err;
364
+ process.stderr.write(`run-results: ${err.message}\n`);
365
+ return 3;
366
+ }
367
+ }
368
+
369
+ if (invokedDirectly(import.meta.url)) {
370
+ runMain("run-results", () => {
371
+ process.exitCode = main(process.argv.slice(2));
372
+ });
373
+ }
@@ -81,12 +81,12 @@ const PHASE_WAITING_FROM = PHASE_CONTRACT.thresholds.waitingFromPhase;
81
81
  */
82
82
  const LAST_PHASE = PHASE_CONTRACT.phases[PHASE_CONTRACT.phases.length - 1].id;
83
83
 
84
- export const CONTRACT_VERSION = "1.2.0";
84
+ export const CONTRACT_VERSION = "1.3.0";
85
85
 
86
86
  const STATE_MAX_BYTES = 16 * 1024 * 1024;
87
87
 
88
88
  /** The run fields runs-index.schema.json marks x-sensitivity local. */
89
- export const LOCAL_RUN_FIELDS = ["dir", "worktreePath", "prUrl", "duplicateOf"];
89
+ export const LOCAL_RUN_FIELDS = ["dir", "worktreePath", "prUrl", "duplicateOf", "channelUrls"];
90
90
 
91
91
  const GROUPS = {
92
92
  waiting: "Waiting on you",
@@ -226,6 +226,57 @@ function runTypeOf(state, kind) {
226
226
  return "development";
227
227
  }
228
228
 
229
+ const JIRA_KEY = /^[A-Z][A-Z0-9_]{0,30}-[0-9]{1,9}$/;
230
+ const GH_PART = /^[A-Za-z0-9_.-]{1,100}$/;
231
+
232
+ /**
233
+ * Where a run's work came from: its Jira key, its GitHub issue as
234
+ * owner/repo#N, or free text. A GitHub issue that got a Jira carries both.
235
+ */
236
+ export function sourceOf(state) {
237
+ const jira =
238
+ typeof state?.jiraId === "string" && JIRA_KEY.test(state.jiraId) ? state.jiraId : null;
239
+ const g = state?.githubIssue;
240
+ const issue =
241
+ g &&
242
+ GH_PART.test(String(g.owner)) &&
243
+ GH_PART.test(String(g.repo)) &&
244
+ Number.isInteger(g.number) &&
245
+ g.number > 0
246
+ ? `${g.owner}/${g.repo}#${g.number}`
247
+ : null;
248
+ if (issue) return { kind: "github", jira, issue };
249
+ if (jira) return { kind: "jira", jira, issue: null };
250
+ if (state?.inputType === "free-text") return { kind: "text", jira: null, issue: null };
251
+ return null;
252
+ }
253
+
254
+ const CHANNELS = ["pr", "jira", "confluence", "wiki", "issue"];
255
+ const CHANNEL_STATES = new Set(["posted", "skipped", "failed"]);
256
+
257
+ /** Where Phase 5 sent the report: each adapter's outcome, in a fixed order. */
258
+ export function channelsOf(state) {
259
+ const r = state?.channelResults;
260
+ if (!r || typeof r !== "object") return { channels: [], urls: {} };
261
+ const channels = [];
262
+ const urls = {};
263
+ for (const c of CHANNELS) {
264
+ const v = r[c];
265
+ if (!v || !CHANNEL_STATES.has(v.status)) continue;
266
+ channels.push({ channel: c, status: v.status });
267
+ if (typeof v.url === "string" && /^https:\/\/\S+$/.test(v.url)) urls[c] = v.url;
268
+ }
269
+ return { channels, urls };
270
+ }
271
+
272
+ /** The run's PR number, from the state or its PR address; null when neither names one. */
273
+ export function prNumberOf(state, prUrl) {
274
+ const n = state?.pr?.number;
275
+ if (Number.isInteger(n) && n > 0) return n;
276
+ const m = /\/(?:pull|pull-requests)\/(\d{1,9})(?:[/?#]|$)/.exec(String(prUrl ?? ""));
277
+ return m && Number(m[1]) > 0 ? Number(m[1]) : null;
278
+ }
279
+
229
280
  /** The issue summary Phase 0 recorded, else the input the run was launched with. */
230
281
  function titleOf(state) {
231
282
  return textOrNull(state?.title) ?? textOrNull(state?.launchRequest?.input);
@@ -388,6 +439,7 @@ export function buildIndex() {
388
439
  const tracker = readJson(trackerPath);
389
440
  const phases = summarisePhases(tracker);
390
441
  const prUrl = state?.pr?.url ?? state?.prUrl ?? null;
442
+ const prNumber = prNumberOf(state, prUrl);
391
443
  const kind = statePath ? "pipeline" : trackerPath ? "tracker-only" : "empty";
392
444
  const currentPhase = Number.isFinite(Number(state?.currentPhase))
393
445
  ? Number(state.currentPhase)
@@ -443,7 +495,15 @@ export function buildIndex() {
443
495
  // exactly what nobody measured.
444
496
  unlockedWrites: Number.isInteger(tracker?.unlockedWrites) ? tracker.unlockedWrites : 0,
445
497
  runType: runTypeOf(state, kind),
498
+ mode:
499
+ typeof state?.mode === "string" && /^[a-z][a-z-]{0,30}$/.test(state.mode)
500
+ ? state.mode
501
+ : null,
446
502
  title: titleOf(state),
503
+ source: sourceOf(state),
504
+ prNumber,
505
+ channels: channelsOf(state).channels,
506
+ channelUrls: channelsOf(state).urls,
447
507
  progress: runProgress(phases.phases),
448
508
  currentTask: currentTaskOf(phases.phases, currentPhase),
449
509
  // Whether the session the run was launched under is alive, from