@akagilnc/pi-workflow-roles 0.1.1751

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 (192) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +104 -0
  3. package/README.zh-CN.md +133 -0
  4. package/THIRD_PARTY_NOTICES.md +60 -0
  5. package/dist/activation-ledger-git.js +68 -0
  6. package/dist/activation-ledger-session.js +120 -0
  7. package/dist/activation-ledger-topology.js +239 -0
  8. package/dist/activation-reconciliation.js +61 -0
  9. package/dist/audit-escalation.js +108 -0
  10. package/dist/auditor-dossier-tool.js +35 -0
  11. package/dist/canonical-json.js +78 -0
  12. package/dist/compliance-transport.js +77 -0
  13. package/dist/doctor-contracts.js +172 -0
  14. package/dist/dossier-resolution.js +103 -0
  15. package/dist/evidence-child-executor.js +661 -0
  16. package/dist/exact-utf8.js +12 -0
  17. package/dist/git-object-id.js +7 -0
  18. package/dist/in-process-session.js +50 -0
  19. package/dist/merger-contracts.js +76 -0
  20. package/dist/navigator-attendance.js +995 -0
  21. package/dist/navigator-invocation-identity.js +220 -0
  22. package/dist/open-tool-schema.js +39 -0
  23. package/dist/package-contracts/collector-output.js +50 -0
  24. package/dist/package-contracts/fixer-output.js +72 -0
  25. package/dist/package-contracts/fixer-packet.js +77 -0
  26. package/dist/package-contracts/judge-output.js +17 -0
  27. package/dist/package-contracts/reviewer-output.js +82 -0
  28. package/dist/package-contracts/terminating-tools.js +173 -0
  29. package/dist/package-contracts/worker-output.js +13 -0
  30. package/dist/package-owned-tool-idle.js +104 -0
  31. package/dist/packaged-role-registry.js +34 -0
  32. package/dist/public-cli/main.js +23867 -0
  33. package/dist/public-command-renderer.js +20 -0
  34. package/dist/reviewer-agent.js +93 -0
  35. package/dist/reviewer-child-executor.js +23 -0
  36. package/dist/reviewer-construction.js +95 -0
  37. package/dist/reviewer-dispatch.js +77 -0
  38. package/dist/reviewer-execution-ledger.js +160 -0
  39. package/dist/reviewer-failure-diagnostic.js +17 -0
  40. package/dist/reviewer-git-snapshot.js +38 -0
  41. package/dist/reviewer-pinned-git.js +146 -0
  42. package/dist/reviewer-preflight-error.js +15 -0
  43. package/dist/reviewer-prompt-identity.js +10 -0
  44. package/dist/reviewer-scope-prompt.js +21 -0
  45. package/dist/reviewer-workspace.js +151 -0
  46. package/dist/sha256.js +5 -0
  47. package/dist/sitian-record-entry.js +33 -0
  48. package/dist/stderr-jsonl.js +26 -0
  49. package/dist/stream-idle-guard.js +75 -0
  50. package/dist/tool-execution-observation.js +141 -0
  51. package/dist/uuidv7.js +21 -0
  52. package/dist/work-subject-identity.js +53 -0
  53. package/extensions/role-runtime.ts +303 -0
  54. package/package.json +69 -0
  55. package/packets/fixer-prerequisites.json +6 -0
  56. package/packets/fixer-repair.md +5 -0
  57. package/packets/judge-apply.md +77 -0
  58. package/packets/judge-authority.md +64 -0
  59. package/packets/judge-plan.md +55 -0
  60. package/packets/judge-review.md +49 -0
  61. package/packets/judge-submission.md +34 -0
  62. package/resources/methods/code-review/SKILL.md +92 -0
  63. package/resources/methods/code-review/agents/openai.yaml +3 -0
  64. package/resources/methods/code-review/provenance.json +26 -0
  65. package/resources/methods/diagnosing-bugs/SKILL.md +134 -0
  66. package/resources/methods/diagnosing-bugs/agents/openai.yaml +3 -0
  67. package/resources/methods/diagnosing-bugs/provenance.json +31 -0
  68. package/resources/methods/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  69. package/resources/methods/resolving-merge-conflicts/SKILL.md +14 -0
  70. package/resources/methods/resolving-merge-conflicts/agents/openai.yaml +3 -0
  71. package/resources/methods/resolving-merge-conflicts/provenance.json +26 -0
  72. package/resources/methods/tdd/SKILL.md +38 -0
  73. package/resources/methods/tdd/agents/openai.yaml +3 -0
  74. package/resources/methods/tdd/mocking.md +59 -0
  75. package/resources/methods/tdd/provenance.json +36 -0
  76. package/resources/methods/tdd/tests.md +77 -0
  77. package/resources/navigator-route-playbook.md +32 -0
  78. package/schemas/tool-execution-observation.schema.json +107 -0
  79. package/scripts/build-package.mjs +65 -0
  80. package/scripts/generate-tool-execution-observation-schema.ts +7 -0
  81. package/souls/coder.md +10 -0
  82. package/souls/collector.md +11 -0
  83. package/souls/doctor-auditor.md +23 -0
  84. package/souls/doctor.md +8 -0
  85. package/souls/fixer-auditor.md +33 -0
  86. package/souls/fixer.md +13 -0
  87. package/souls/judge-auditor.md +33 -0
  88. package/souls/judge.md +74 -0
  89. package/souls/merger.md +5 -0
  90. package/souls/navigator.md +5 -0
  91. package/souls/reviewer-auditor.md +25 -0
  92. package/souls/reviewer.md +11 -0
  93. package/src/activation-ledger-git.ts +96 -0
  94. package/src/activation-ledger-session.ts +188 -0
  95. package/src/activation-ledger-topology.ts +301 -0
  96. package/src/activation-ledger.ts +240 -0
  97. package/src/activation-reconciliation.ts +163 -0
  98. package/src/activation-trace.ts +38 -0
  99. package/src/audit-escalation.ts +177 -0
  100. package/src/auditor-dossier-tool.ts +48 -0
  101. package/src/auditor-soul.ts +28 -0
  102. package/src/canonical-json.ts +74 -0
  103. package/src/canonical-skill-binding.ts +107 -0
  104. package/src/collector-config.ts +89 -0
  105. package/src/collector-evidence.ts +461 -0
  106. package/src/collector-github.ts +656 -0
  107. package/src/collector-identity.ts +161 -0
  108. package/src/collector-ledger.ts +827 -0
  109. package/src/collector-receipt.ts +87 -0
  110. package/src/collector-role.ts +592 -0
  111. package/src/collector-tool-schemas.ts +19 -0
  112. package/src/compliance-transport.ts +130 -0
  113. package/src/doctor-auditor.ts +53 -0
  114. package/src/doctor-contracts.ts +166 -0
  115. package/src/doctor-evidence.ts +47 -0
  116. package/src/doctor-role.ts +18 -0
  117. package/src/dossier-resolution.ts +137 -0
  118. package/src/evidence-child-executor.ts +775 -0
  119. package/src/exact-utf8.ts +9 -0
  120. package/src/factory-board.ts +1822 -0
  121. package/src/git-object-id.ts +11 -0
  122. package/src/human-format.ts +65 -0
  123. package/src/in-process-session.ts +78 -0
  124. package/src/judge-auditor.ts +55 -0
  125. package/src/judge-recording-anti-forge.ts +53 -0
  126. package/src/judge-role.ts +160 -0
  127. package/src/merger-contracts.ts +71 -0
  128. package/src/merger-git-state.ts +76 -0
  129. package/src/merger-role.ts +60 -0
  130. package/src/navigator-attendance.ts +1254 -0
  131. package/src/navigator-invocation-identity.ts +446 -0
  132. package/src/open-tool-schema.ts +46 -0
  133. package/src/package-contracts/collector-output.ts +109 -0
  134. package/src/package-contracts/fixer-output.ts +81 -0
  135. package/src/package-contracts/fixer-packet.ts +93 -0
  136. package/src/package-contracts/judge-output.ts +39 -0
  137. package/src/package-contracts/reviewer-output.ts +115 -0
  138. package/src/package-contracts/terminating-tools.ts +259 -0
  139. package/src/package-contracts/worker-output.ts +36 -0
  140. package/src/package-owned-tool-idle.ts +134 -0
  141. package/src/package-resources/method-skill-binding.ts +87 -0
  142. package/src/package-resources/method-skill.ts +358 -0
  143. package/src/packaged-role-registry.ts +36 -0
  144. package/src/public-cli/cli-errors.ts +11 -0
  145. package/src/public-cli/cli-io.ts +4 -0
  146. package/src/public-cli/cli.ts +912 -0
  147. package/src/public-cli/coder-run.ts +575 -0
  148. package/src/public-cli/collector-run.ts +375 -0
  149. package/src/public-cli/command-renderer.ts +8 -0
  150. package/src/public-cli/config.ts +346 -0
  151. package/src/public-cli/doctor-run.ts +355 -0
  152. package/src/public-cli/explicit-internal.ts +274 -0
  153. package/src/public-cli/fixer-run.ts +587 -0
  154. package/src/public-cli/host-pi-runtime.ts +112 -0
  155. package/src/public-cli/invocation.ts +1958 -0
  156. package/src/public-cli/judge-run.ts +507 -0
  157. package/src/public-cli/main.ts +15 -0
  158. package/src/public-cli/merger-run.ts +681 -0
  159. package/src/public-cli/public-run-credentials.ts +71 -0
  160. package/src/public-cli/registry.ts +153 -0
  161. package/src/public-cli/reviewer-run.ts +561 -0
  162. package/src/public-cli/run-lifecycle.ts +884 -0
  163. package/src/public-cli/settlement.ts +3765 -0
  164. package/src/public-cli/terminal.ts +325 -0
  165. package/src/public-command-renderer.ts +43 -0
  166. package/src/reviewer-agent.ts +94 -0
  167. package/src/reviewer-auditor.ts +53 -0
  168. package/src/reviewer-child-executor.ts +31 -0
  169. package/src/reviewer-construction.ts +137 -0
  170. package/src/reviewer-dispatch.ts +94 -0
  171. package/src/reviewer-execution-ledger.ts +206 -0
  172. package/src/reviewer-failure-diagnostic.ts +18 -0
  173. package/src/reviewer-git-snapshot.ts +53 -0
  174. package/src/reviewer-pinned-git.ts +144 -0
  175. package/src/reviewer-preflight-error.ts +14 -0
  176. package/src/reviewer-prompt-identity.ts +17 -0
  177. package/src/reviewer-role.ts +193 -0
  178. package/src/reviewer-scope-prompt.ts +24 -0
  179. package/src/reviewer-settlement.ts +63 -0
  180. package/src/reviewer-workspace.ts +111 -0
  181. package/src/role-runtime.ts +884 -0
  182. package/src/sha256.ts +6 -0
  183. package/src/sitian-record-entry.ts +57 -0
  184. package/src/stderr-jsonl.ts +28 -0
  185. package/src/stream-idle-guard.ts +98 -0
  186. package/src/ticket-snapshot.ts +662 -0
  187. package/src/ticket-trajectory.ts +1000 -0
  188. package/src/tool-execution-observation.ts +168 -0
  189. package/src/uuidv7.ts +1 -0
  190. package/src/work-subject-identity.ts +94 -0
  191. package/src/worker-role.ts +434 -0
  192. package/src/worker-submission-gates.ts +225 -0
@@ -0,0 +1,827 @@
1
+ import Value from "typebox/value";
2
+
3
+ import type { CollectorManifest, CollectorRepository } from "./collector-config.ts";
4
+ import {
5
+ applyEvidenceVersionHistory,
6
+ assignWindowRelations,
7
+ COLLECTOR_ELIGIBILITY_MS,
8
+ measureNormalizedBytes,
9
+ normalizeAuthenticatedUserEvidence,
10
+ normalizeIssueCommentEvidence,
11
+ normalizePullRequestEvidence,
12
+ normalizePullRequestReactionEvidence,
13
+ normalizeReviewCommentEvidence,
14
+ normalizeReviewEvidence,
15
+ sha256Text,
16
+ type CollectorClock,
17
+ type CollectorEvidenceRecord,
18
+ type CollectorSnapshot,
19
+ } from "./collector-evidence.ts";
20
+ import {
21
+ buildCollectorRequestBody,
22
+ type CollectorGitHubTransport,
23
+ type GitHubPageDiagnostics,
24
+ type GitHubPullRequest,
25
+ } from "./collector-github.ts";
26
+ import {
27
+ collectorObserveArgsSchema,
28
+ collectorOutputArgsSchema,
29
+ collectorRequestArgsSchema,
30
+ collectorWaitArgsSchema,
31
+ } from "./collector-tool-schemas.ts";
32
+ import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
33
+
34
+ export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
35
+ export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
36
+ export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
37
+ export { COLLECTOR_OUTPUT_TOOL };
38
+
39
+ export const COLLECTOR_OPERATIONAL_TOOLS = [
40
+ COLLECTOR_OBSERVE_TOOL,
41
+ COLLECTOR_REQUEST_TOOL,
42
+ COLLECTOR_WAIT_TOOL,
43
+ ] as const;
44
+
45
+ export type CollectorOperationalTool = (typeof COLLECTOR_OPERATIONAL_TOOLS)[number];
46
+
47
+ export type CollectorRequestAttempt = {
48
+ attemptId: string;
49
+ requestId: string;
50
+ observedHead: string;
51
+ snapshotId: string;
52
+ marker: string;
53
+ body: string;
54
+ startedAt: string;
55
+ status: "started" | "succeeded" | "rejected" | "ambiguous_loss" | "recovered";
56
+ responseDiagnostics?: string;
57
+ commentEvidenceId?: string;
58
+ /** Snapshot whose observation first established the authenticated marker. */
59
+ recoverySnapshotId?: string;
60
+ };
61
+
62
+ export type CollectorWaitRecord = {
63
+ waitId: string;
64
+ requestedMs: number;
65
+ effectiveMs: number;
66
+ startedAt: string;
67
+ endedAt: string;
68
+ cutoffReached: boolean;
69
+ };
70
+
71
+ export type CollectorTransportFailure = {
72
+ failureId: string;
73
+ kind: "ambiguous_request_loss" | "api" | "pagination" | "size";
74
+ message: string;
75
+ requestId?: string;
76
+ observedHead?: string;
77
+ marker?: string;
78
+ recovered: boolean;
79
+ };
80
+
81
+ export type CollectorConfigState = {
82
+ repository: CollectorRepository;
83
+ prNumber: number;
84
+ manifest: CollectorManifest;
85
+ };
86
+
87
+ export type CollectorLedger = {
88
+ readonly config: CollectorConfigState;
89
+ readonly fatal: boolean;
90
+ readonly fatalReason: string | undefined;
91
+ readonly outputAccepted: boolean;
92
+ readonly activationRecorded: boolean;
93
+ readonly activationTime: Date | undefined;
94
+ readonly deadlineTime: Date | undefined;
95
+ readonly activationMono: number | undefined;
96
+ readonly deadlineMono: number | undefined;
97
+ readonly requesterLogin: string | undefined;
98
+ readonly latestCompleteSnapshotId: string | undefined;
99
+ readonly finalObservationRequired: boolean;
100
+ readonly finalObservationCompleted: boolean;
101
+ readonly unresolvedTransportFailure: boolean;
102
+ readonly mutationGeneration: number;
103
+ readonly observedGeneration: number;
104
+
105
+ latchFatal(reason: string, cause?: unknown): Error;
106
+ assertNotFatal(): void;
107
+ recordActivation(clock: CollectorClock): void;
108
+ beginOperational(toolName: string, toolCallId: string): void;
109
+ completeOperational(toolCallId: string): void;
110
+ markOutputAccepted(): void;
111
+ noteCutoffObserved(): void;
112
+ assertOutputObservationLaw(clock: CollectorClock): void;
113
+
114
+ observe(
115
+ transport: CollectorGitHubTransport,
116
+ clock: CollectorClock,
117
+ signal?: AbortSignal,
118
+ ): Promise<{
119
+ snapshot: CollectorSnapshot;
120
+ modelView: unknown;
121
+ }>;
122
+
123
+ request(
124
+ input: { requestId: string; snapshotId: string },
125
+ transport: CollectorGitHubTransport,
126
+ clock: CollectorClock,
127
+ signal?: AbortSignal,
128
+ ): Promise<unknown>;
129
+
130
+ wait(
131
+ input: { durationMs: number },
132
+ clock: CollectorClock,
133
+ signal?: AbortSignal,
134
+ ): Promise<unknown>;
135
+
136
+ getSnapshot(snapshotId: string): CollectorSnapshot | undefined;
137
+ getEvidence(evidenceId: string): CollectorEvidenceRecord | undefined;
138
+ allEvidence(): readonly CollectorEvidenceRecord[];
139
+ allSnapshots(): readonly CollectorSnapshot[];
140
+ requestAttempts(): readonly CollectorRequestAttempt[];
141
+ waits(): readonly CollectorWaitRecord[];
142
+ transportFailures(): readonly CollectorTransportFailure[];
143
+ requestById(requestId: string): CollectorConfigState["manifest"]["requests"][number] | undefined;
144
+ };
145
+
146
+ function isOperationalTool(name: string): name is CollectorOperationalTool {
147
+ return (COLLECTOR_OPERATIONAL_TOOLS as readonly string[]).includes(name);
148
+ }
149
+
150
+ /** Shared Check owner for Collector tool argument shapes (schema seam, not batch law). */
151
+ export function collectorToolArgumentsValid(
152
+ name: string,
153
+ args: unknown,
154
+ ): boolean {
155
+ // Residual envelope: reject missing/null args before schema check.
156
+ if (args === undefined || args === null) return false;
157
+ switch (name) {
158
+ case COLLECTOR_OBSERVE_TOOL:
159
+ return Value.Check(collectorObserveArgsSchema, args);
160
+ case COLLECTOR_REQUEST_TOOL:
161
+ return Value.Check(collectorRequestArgsSchema, args);
162
+ case COLLECTOR_WAIT_TOOL:
163
+ return Value.Check(collectorWaitArgsSchema, args);
164
+ case COLLECTOR_OUTPUT_TOOL:
165
+ return Value.Check(collectorOutputArgsSchema, args);
166
+ default:
167
+ return false;
168
+ }
169
+ }
170
+
171
+ export function createCollectorLedger(config: CollectorConfigState): CollectorLedger {
172
+ let fatal = false;
173
+ let fatalReason: string | undefined;
174
+ let outputAccepted = false;
175
+ let activationTime: Date | undefined;
176
+ let deadlineTime: Date | undefined;
177
+ let activationMono: number | undefined;
178
+ let deadlineMono: number | undefined;
179
+ let requesterLogin: string | undefined;
180
+ let latestCompleteSnapshotId: string | undefined;
181
+ let finalObservationRequired = false;
182
+ let finalObservationCompleted = false;
183
+ let activeOperationalCallId: string | undefined;
184
+ let mutationGeneration = 0;
185
+ let observedGeneration = 0;
186
+
187
+ const evidenceById = new Map<string, CollectorEvidenceRecord>();
188
+ const evidenceByVersion = new Map<string, string>();
189
+ const snapshots: CollectorSnapshot[] = [];
190
+ const attempts: CollectorRequestAttempt[] = [];
191
+ const attemptKeys = new Set<string>();
192
+ const waits: CollectorWaitRecord[] = [];
193
+ const transportFailures: CollectorTransportFailure[] = [];
194
+
195
+ const latchFatal = (reason: string, cause?: unknown): Error => {
196
+ fatal = true;
197
+ fatalReason = reason;
198
+ const error = new Error(reason, cause === undefined ? undefined : { cause });
199
+ Object.assign(error, { collectorFatal: true });
200
+ return error;
201
+ };
202
+
203
+ const assertNotFatal = (): void => {
204
+ if (fatal) throw new Error(fatalReason ?? "Collector is in fatal state");
205
+ };
206
+
207
+ const storeEvidence = (record: CollectorEvidenceRecord): CollectorEvidenceRecord => {
208
+ const existingId = evidenceByVersion.get(record.versionId);
209
+ if (existingId !== undefined) {
210
+ return evidenceById.get(existingId) ?? record;
211
+ }
212
+ evidenceByVersion.set(record.versionId, record.evidenceId);
213
+ evidenceById.set(record.evidenceId, record);
214
+ return record;
215
+ };
216
+
217
+ const monoNowOrThrow = (clock: CollectorClock): number => clock.monoNow();
218
+
219
+ const pastCutoff = (clock: CollectorClock): boolean => {
220
+ if (deadlineMono === undefined) return false;
221
+ return monoNowOrThrow(clock) >= deadlineMono;
222
+ };
223
+
224
+ const remainingMs = (clock: CollectorClock): number => {
225
+ if (deadlineMono === undefined) return COLLECTOR_ELIGIBILITY_MS;
226
+ return Math.max(0, deadlineMono - monoNowOrThrow(clock));
227
+ };
228
+
229
+ const prIdentity = (pr: GitHubPullRequest): string =>
230
+ `${pr.state}|${pr.headOid}|${pr.updatedAt ?? ""}`;
231
+
232
+ const fetchObserveSurfaces = async (
233
+ transport: CollectorGitHubTransport,
234
+ observedAt: string,
235
+ signal?: AbortSignal,
236
+ ) => {
237
+ const owner = config.repository.owner;
238
+ const repo = config.repository.repo;
239
+ const prNumber = config.prNumber;
240
+ const signalOpt = signal === undefined ? {} : { signal };
241
+ const user = await transport.getAuthenticatedUser(signalOpt);
242
+ const prInitial = await transport.getPullRequest({
243
+ owner,
244
+ repo,
245
+ prNumber,
246
+ ...signalOpt,
247
+ });
248
+ const reviews = await transport.listPullRequestReviews({
249
+ owner,
250
+ repo,
251
+ prNumber,
252
+ ...signalOpt,
253
+ });
254
+ const reactions = transport.listPullRequestReactions === undefined
255
+ ? { items: [], pages: [] }
256
+ : await transport.listPullRequestReactions({ owner, repo, prNumber, ...signalOpt });
257
+ const issueComments = await transport.listIssueComments({
258
+ owner,
259
+ repo,
260
+ prNumber,
261
+ ...signalOpt,
262
+ });
263
+ const reviewComments = await transport.listReviewComments({
264
+ owner,
265
+ repo,
266
+ prNumber,
267
+ ...signalOpt,
268
+ });
269
+ const prTerminal = await transport.getPullRequest({
270
+ owner,
271
+ repo,
272
+ prNumber,
273
+ ...signalOpt,
274
+ });
275
+ return { user, prInitial, reviews, reactions, issueComments, reviewComments, prTerminal };
276
+ };
277
+
278
+ const ledger: CollectorLedger = {
279
+ get config() {
280
+ return config;
281
+ },
282
+ get fatal() {
283
+ return fatal;
284
+ },
285
+ get fatalReason() {
286
+ return fatalReason;
287
+ },
288
+ get outputAccepted() {
289
+ return outputAccepted;
290
+ },
291
+ get activationRecorded() {
292
+ return activationTime !== undefined;
293
+ },
294
+ get activationTime() {
295
+ return activationTime;
296
+ },
297
+ get deadlineTime() {
298
+ return deadlineTime;
299
+ },
300
+ get activationMono() {
301
+ return activationMono;
302
+ },
303
+ get deadlineMono() {
304
+ return deadlineMono;
305
+ },
306
+ get requesterLogin() {
307
+ return requesterLogin;
308
+ },
309
+ get latestCompleteSnapshotId() {
310
+ return latestCompleteSnapshotId;
311
+ },
312
+ get finalObservationRequired() {
313
+ return finalObservationRequired;
314
+ },
315
+ get finalObservationCompleted() {
316
+ return finalObservationCompleted;
317
+ },
318
+ get unresolvedTransportFailure() {
319
+ return transportFailures.some((failure) => !failure.recovered);
320
+ },
321
+ get mutationGeneration() {
322
+ return mutationGeneration;
323
+ },
324
+ get observedGeneration() {
325
+ return observedGeneration;
326
+ },
327
+
328
+ latchFatal,
329
+ assertNotFatal,
330
+
331
+ recordActivation(clock) {
332
+ assertNotFatal();
333
+ if (activationTime !== undefined) return;
334
+ activationTime = clock.wallNow();
335
+ activationMono = clock.monoNow();
336
+ deadlineTime = new Date(activationTime.getTime() + COLLECTOR_ELIGIBILITY_MS);
337
+ deadlineMono = activationMono + COLLECTOR_ELIGIBILITY_MS;
338
+ },
339
+
340
+ beginOperational(toolName, toolCallId) {
341
+ assertNotFatal();
342
+ if (outputAccepted && toolName !== COLLECTOR_OUTPUT_TOOL) {
343
+ throw latchFatal("Collector output already accepted; no further operations");
344
+ }
345
+ // Idempotent for the same call across tool_call preflight and execute.
346
+ if (activeOperationalCallId === toolCallId) {
347
+ return;
348
+ }
349
+ if (activeOperationalCallId !== undefined) {
350
+ throw latchFatal("Collector operational call already active");
351
+ }
352
+
353
+ if (toolName === COLLECTOR_OUTPUT_TOOL) {
354
+ return;
355
+ }
356
+
357
+ if (!isOperationalTool(toolName)) {
358
+ throw latchFatal(`Unknown Collector tool ${toolName}`);
359
+ }
360
+ activeOperationalCallId = toolCallId;
361
+ },
362
+
363
+ completeOperational(toolCallId) {
364
+ if (activeOperationalCallId === toolCallId) {
365
+ activeOperationalCallId = undefined;
366
+ }
367
+ },
368
+
369
+ markOutputAccepted() {
370
+ assertNotFatal();
371
+ if (outputAccepted) throw latchFatal("Collector output is singleton");
372
+ outputAccepted = true;
373
+ },
374
+
375
+ noteCutoffObserved() {
376
+ finalObservationRequired = true;
377
+ },
378
+
379
+ assertOutputObservationLaw(clock) {
380
+ assertNotFatal();
381
+ if (activationTime === undefined || deadlineTime === undefined || deadlineMono === undefined) {
382
+ throw new Error("Collector output requires activation timeline");
383
+ }
384
+ if (latestCompleteSnapshotId === undefined) {
385
+ throw new Error("Collector output requires a complete final snapshot");
386
+ }
387
+ if (observedGeneration !== mutationGeneration) {
388
+ throw new Error(
389
+ "Collector output requires a complete observe after the latest request/wait mutation",
390
+ );
391
+ }
392
+ const snapshot = snapshots.find((item) => item.snapshotId === latestCompleteSnapshotId);
393
+ if (snapshot === undefined || !snapshot.complete) {
394
+ throw new Error("Collector final snapshot is missing or incomplete");
395
+ }
396
+
397
+ const mono = monoNowOrThrow(clock);
398
+ const atOrAfterCutoff = mono >= deadlineMono;
399
+ if (atOrAfterCutoff) {
400
+ finalObservationRequired = true;
401
+ if (
402
+ snapshot.completedMono === undefined ||
403
+ snapshot.completedMono < deadlineMono
404
+ ) {
405
+ throw new Error(
406
+ "Collector output at/after cutoff requires a complete observation finished at or after the cutoff",
407
+ );
408
+ }
409
+ finalObservationCompleted = true;
410
+ }
411
+ },
412
+
413
+ async observe(transport, clock, signal) {
414
+ assertNotFatal();
415
+ if (activationTime === undefined) {
416
+ throw latchFatal("Collector observe requires activation");
417
+ }
418
+ if (signal?.aborted) {
419
+ const abortMessage = signal.reason instanceof Error
420
+ ? signal.reason.message
421
+ : String(signal.reason ?? "aborted");
422
+ throw latchFatal(
423
+ `Collector observe failed: ${abortMessage}`,
424
+ signal.reason,
425
+ );
426
+ }
427
+ const observedAt = clock.wallNow().toISOString();
428
+ const cutoff = pastCutoff(clock);
429
+ if (cutoff) {
430
+ finalObservationRequired = true;
431
+ }
432
+
433
+ let surfaces;
434
+ try {
435
+ surfaces = await fetchObserveSurfaces(transport, observedAt, signal);
436
+ if (prIdentity(surfaces.prInitial) !== prIdentity(surfaces.prTerminal)) {
437
+ // Retry full surfaces once; bind only a consistent terminal read.
438
+ surfaces = await fetchObserveSurfaces(transport, observedAt, signal);
439
+ if (prIdentity(surfaces.prInitial) !== prIdentity(surfaces.prTerminal)) {
440
+ throw new Error(
441
+ `PR identity drifted across observe bracket after retry (${prIdentity(surfaces.prInitial)} → ${prIdentity(surfaces.prTerminal)})`,
442
+ );
443
+ }
444
+ }
445
+ } catch (error) {
446
+ const message = error instanceof Error ? error.message : "unknown failure";
447
+ throw latchFatal(`Collector observe failed: ${message}`, error);
448
+ }
449
+
450
+ // First-sighting trust must not predate actual surface observation.
451
+ // Observe-start may be before cutoff while surfaces arrive after; stamp
452
+ // evidence wall metadata and mono trust only once surfaces are in hand
453
+ // (budget retain during fetch may keep the temporary start stamp).
454
+ const firstObservedAt = clock.wallNow().toISOString(); // metadata
455
+ const firstObservedMono = monoNowOrThrow(clock); // trust
456
+
457
+ // Always bind terminal PR identity fields.
458
+ const pr = surfaces.prTerminal;
459
+ const { user, reviews, reactions, issueComments, reviewComments } = surfaces;
460
+
461
+ requesterLogin = user.login.toLowerCase();
462
+
463
+ const pageDiagnostics: GitHubPageDiagnostics[] = [
464
+ ...reviews.pages,
465
+ ...reactions.pages,
466
+ ...issueComments.pages,
467
+ ...reviewComments.pages,
468
+ ];
469
+
470
+ const pendingRecords: CollectorEvidenceRecord[] = [];
471
+ pendingRecords.push(normalizeAuthenticatedUserEvidence(user, firstObservedAt));
472
+ pendingRecords.push(normalizePullRequestEvidence(pr, firstObservedAt));
473
+ for (const review of reviews.items) {
474
+ pendingRecords.push(normalizeReviewEvidence(review, firstObservedAt));
475
+ }
476
+ for (const reaction of reactions.items) {
477
+ pendingRecords.push(normalizePullRequestReactionEvidence(reaction, firstObservedAt));
478
+ }
479
+ for (const comment of issueComments.items) {
480
+ pendingRecords.push(normalizeIssueCommentEvidence(comment, firstObservedAt));
481
+ }
482
+ for (const comment of reviewComments.items) {
483
+ pendingRecords.push(normalizeReviewCommentEvidence(comment, firstObservedAt));
484
+ }
485
+
486
+ applyEvidenceVersionHistory(
487
+ pendingRecords,
488
+ [...evidenceById.values()],
489
+ { deadlineMono: deadlineMono!, firstObservedMono },
490
+ );
491
+ assignWindowRelations(pendingRecords, activationTime, deadlineTime);
492
+
493
+ const normalizedByteLength = measureNormalizedBytes(pendingRecords);
494
+
495
+ // Commit only after complete surfaces validated.
496
+ const storedIds: string[] = [];
497
+ for (const record of pendingRecords) {
498
+ const stored = storeEvidence(record);
499
+ storedIds.push(stored.evidenceId);
500
+ }
501
+ // Project modelView from canonical stored snapshot records (not mutable pending).
502
+ const storedRecords = storedIds.map((id) => {
503
+ const stored = evidenceById.get(id);
504
+ if (stored === undefined) {
505
+ throw latchFatal(`Collector observe lost stored evidence ${id}`);
506
+ }
507
+ return stored;
508
+ });
509
+
510
+ const completedAt = clock.wallNow().toISOString();
511
+ const completedMono = clock.monoNow();
512
+ const snapshotId = sha256Text(
513
+ `${completedAt}:${pr.headOid}:${storedIds.join(",")}`,
514
+ ).slice(0, 16);
515
+
516
+ // Recover ambiguous request markers if present.
517
+ for (const failure of transportFailures) {
518
+ if (failure.recovered || failure.kind !== "ambiguous_request_loss") continue;
519
+ if (failure.marker === undefined || failure.requestId === undefined) continue;
520
+ const found = storedRecords.find((record) =>
521
+ record.kind === "issue_comment" &&
522
+ record.authorLogin === requesterLogin &&
523
+ typeof record.body === "string" &&
524
+ record.body.includes(failure.marker!)
525
+ );
526
+ if (found) {
527
+ failure.recovered = true;
528
+ const attempt = attempts.find((item) =>
529
+ item.status === "ambiguous_loss" &&
530
+ item.requestId === failure.requestId &&
531
+ item.marker === failure.marker
532
+ );
533
+ if (attempt) {
534
+ attempt.status = "recovered";
535
+ attempt.commentEvidenceId = found.evidenceId;
536
+ attempt.recoverySnapshotId = snapshotId;
537
+ }
538
+ }
539
+ }
540
+
541
+ const snapshot: CollectorSnapshot = {
542
+ snapshotId,
543
+ observedAt,
544
+ completedAt,
545
+ completedMono,
546
+ host: "github.com",
547
+ repository: config.repository.canonical,
548
+ prNumber: config.prNumber,
549
+ prState: pr.state,
550
+ headOid: pr.headOid,
551
+ complete: true,
552
+ evidenceIds: storedIds,
553
+ pageDiagnostics,
554
+ normalizedByteLength,
555
+ };
556
+ snapshots.push(snapshot);
557
+ latestCompleteSnapshotId = snapshotId;
558
+ observedGeneration = mutationGeneration;
559
+ if (finalObservationRequired && completedMono >= (deadlineMono ?? 0)) {
560
+ finalObservationCompleted = true;
561
+ } else if (cutoff) {
562
+ finalObservationCompleted = true;
563
+ }
564
+
565
+ const modelView = buildObserveModelView({
566
+ snapshot,
567
+ records: storedRecords,
568
+ requesterLogin,
569
+ attempts,
570
+ });
571
+
572
+ return { snapshot, modelView };
573
+ },
574
+
575
+ async request(input, transport, clock, signal) {
576
+ assertNotFatal();
577
+ if (activationTime === undefined || deadlineTime === undefined) {
578
+ throw latchFatal("Collector request requires activation");
579
+ }
580
+ if (pastCutoff(clock)) {
581
+ finalObservationRequired = true;
582
+ throw latchFatal("Collector request is not permitted at or after the eligibility cutoff");
583
+ }
584
+ if (ledger.unresolvedTransportFailure) {
585
+ throw latchFatal("Collector cannot request while a transport failure is unrecovered");
586
+ }
587
+
588
+ const request = config.manifest.requests.find((item) => item.id === input.requestId);
589
+ if (request === undefined) {
590
+ throw new Error(`Unknown Collector requestId "${input.requestId}"`);
591
+ }
592
+
593
+ const snapshot = snapshots.find((item) => item.snapshotId === input.snapshotId);
594
+ if (snapshot === undefined) {
595
+ throw new Error(`Unknown Collector snapshotId \"${input.snapshotId}\"`);
596
+ }
597
+ if (snapshot.snapshotId !== latestCompleteSnapshotId) {
598
+ throw new Error("Collector request requires the latest complete snapshot");
599
+ }
600
+ if (snapshot.prState !== "OPEN") {
601
+ throw latchFatal("Collector cannot request on a non-OPEN pull request snapshot");
602
+ }
603
+
604
+ const { body, marker } = buildCollectorRequestBody({
605
+ configuredBody: request.requestBody,
606
+ manifestDigest: config.manifest.digest,
607
+ requestId: request.id,
608
+ headOid: snapshot.headOid,
609
+ });
610
+ const existingMarker = snapshot.evidenceIds.some((id) => {
611
+ const record = evidenceById.get(id);
612
+ return record?.kind === "issue_comment" &&
613
+ record.authorLogin === requesterLogin &&
614
+ typeof record.body === "string" &&
615
+ record.body.includes(marker);
616
+ });
617
+ if (existingMarker) {
618
+ throw new Error(
619
+ `Collector already has an authenticated same-marker request for request \"${input.requestId}\" at this HEAD`,
620
+ );
621
+ }
622
+
623
+ const attemptKey = [
624
+ config.repository.canonical,
625
+ String(config.prNumber),
626
+ snapshot.headOid,
627
+ request.id,
628
+ ].join("|");
629
+ if (attemptKeys.has(attemptKey)) {
630
+ throw new Error(
631
+ `Collector process-local request attempt already used for request \"${request.id}\" at HEAD ${snapshot.headOid}`,
632
+ );
633
+ }
634
+
635
+ const startedAt = clock.wallNow().toISOString();
636
+ const attemptId = sha256Text(`${attemptKey}:${startedAt}`).slice(0, 16);
637
+ const attempt: CollectorRequestAttempt = {
638
+ attemptId,
639
+ requestId: request.id,
640
+ observedHead: snapshot.headOid,
641
+ snapshotId: snapshot.snapshotId,
642
+ marker,
643
+ body,
644
+ startedAt,
645
+ status: "started",
646
+ };
647
+ attempts.push(attempt);
648
+ attemptKeys.add(attemptKey);
649
+
650
+ const result = await transport.createIssueComment({
651
+ owner: config.repository.owner,
652
+ repo: config.repository.repo,
653
+ prNumber: config.prNumber,
654
+ body,
655
+ ...(signal === undefined ? {} : { signal }),
656
+ });
657
+
658
+ // Successful or ambiguous request completion dirties observation generation.
659
+ mutationGeneration += 1;
660
+ finalObservationCompleted = false;
661
+
662
+ if (result.kind === "success") {
663
+ const record = storeEvidence(
664
+ normalizeIssueCommentEvidence(result.comment, startedAt),
665
+ );
666
+ attempt.status = "succeeded";
667
+ attempt.commentEvidenceId = record.evidenceId;
668
+ return {
669
+ status: "succeeded",
670
+ attemptId,
671
+ requestId: request.id,
672
+ observedHead: snapshot.headOid,
673
+ marker,
674
+ commentEvidenceId: record.evidenceId,
675
+ };
676
+ }
677
+
678
+ if (result.kind === "ambiguous_loss") {
679
+ attempt.status = "ambiguous_loss";
680
+ attempt.responseDiagnostics = result.diagnostics;
681
+ transportFailures.push({
682
+ failureId: sha256Text(`loss:${attemptId}`).slice(0, 16),
683
+ kind: "ambiguous_request_loss",
684
+ message: result.diagnostics,
685
+ requestId: request.id,
686
+ observedHead: snapshot.headOid,
687
+ marker,
688
+ recovered: false,
689
+ });
690
+ return {
691
+ status: "ambiguous_loss",
692
+ attemptId,
693
+ requestId: request.id,
694
+ observedHead: snapshot.headOid,
695
+ marker,
696
+ diagnostics: result.diagnostics,
697
+ };
698
+ }
699
+
700
+ attempt.status = "rejected";
701
+ attempt.responseDiagnostics = result.diagnostics;
702
+ throw latchFatal(
703
+ `Collector request rejected: ${result.diagnostics}`,
704
+ );
705
+ },
706
+
707
+ async wait(input, clock, signal) {
708
+ assertNotFatal();
709
+ if (activationTime === undefined) {
710
+ throw latchFatal("Collector wait requires activation");
711
+ }
712
+ if (!Number.isSafeInteger(input.durationMs) || input.durationMs < 1) {
713
+ throw new Error("Collector wait durationMs must be a positive safe integer");
714
+ }
715
+ if (input.durationMs > COLLECTOR_ELIGIBILITY_MS) {
716
+ throw new Error(
717
+ `Collector wait durationMs must be at most ${COLLECTOR_ELIGIBILITY_MS}`,
718
+ );
719
+ }
720
+ if (pastCutoff(clock)) {
721
+ finalObservationRequired = true;
722
+ throw latchFatal("Collector wait is not permitted at or after the eligibility cutoff");
723
+ }
724
+
725
+ const remaining = remainingMs(clock);
726
+ // Single-wait runtime cadence cap (v2 §6 / §9); schema max stays 15m.
727
+ const COLLECTOR_SINGLE_WAIT_MAX_MS = 300_000;
728
+ const effectiveMs = Math.min(
729
+ input.durationMs,
730
+ remaining,
731
+ COLLECTOR_SINGLE_WAIT_MAX_MS,
732
+ );
733
+ const startedAt = clock.wallNow().toISOString();
734
+ const waitId = sha256Text(`wait:${startedAt}:${effectiveMs}`).slice(0, 16);
735
+ await clock.sleep(effectiveMs, signal);
736
+ const endedAt = clock.wallNow().toISOString();
737
+ const cutoffReached = pastCutoff(clock);
738
+ if (cutoffReached) finalObservationRequired = true;
739
+ mutationGeneration += 1;
740
+ finalObservationCompleted = false;
741
+ const record: CollectorWaitRecord = {
742
+ waitId,
743
+ requestedMs: input.durationMs,
744
+ effectiveMs,
745
+ startedAt,
746
+ endedAt,
747
+ cutoffReached,
748
+ };
749
+ waits.push(record);
750
+ return {
751
+ waitId,
752
+ requestedMs: input.durationMs,
753
+ effectiveMs,
754
+ cutoffReached,
755
+ remainingMsAfter: remainingMs(clock),
756
+ };
757
+ },
758
+
759
+ getSnapshot(snapshotId) {
760
+ return snapshots.find((item) => item.snapshotId === snapshotId);
761
+ },
762
+ getEvidence(evidenceId) {
763
+ return evidenceById.get(evidenceId);
764
+ },
765
+ allEvidence() {
766
+ return [...evidenceById.values()];
767
+ },
768
+ allSnapshots() {
769
+ return [...snapshots];
770
+ },
771
+ requestAttempts() {
772
+ return [...attempts];
773
+ },
774
+ waits() {
775
+ return [...waits];
776
+ },
777
+ transportFailures() {
778
+ return [...transportFailures];
779
+ },
780
+ requestById(requestId) {
781
+ return config.manifest.requests.find((request) => request.id === requestId);
782
+ },
783
+ };
784
+
785
+ return ledger;
786
+ }
787
+
788
+ function buildObserveModelView(input: {
789
+ snapshot: CollectorSnapshot;
790
+ records: readonly CollectorEvidenceRecord[];
791
+ requesterLogin: string | undefined;
792
+ attempts: readonly CollectorRequestAttempt[];
793
+ }): unknown {
794
+ const relevant = input.records;
795
+ return {
796
+ snapshotId: input.snapshot.snapshotId,
797
+ observedAt: input.snapshot.observedAt,
798
+ completedAt: input.snapshot.completedAt,
799
+ prState: input.snapshot.prState,
800
+ headOid: input.snapshot.headOid,
801
+ complete: input.snapshot.complete,
802
+ evidence: relevant.map((record) => ({
803
+ evidenceId: record.evidenceId,
804
+ kind: record.kind,
805
+ authorLogin: record.authorLogin,
806
+ state: record.state,
807
+ body: record.body,
808
+ commitOid: record.commitOid,
809
+ htmlUrl: record.htmlUrl,
810
+ path: record.path,
811
+ // Single display fallback: current line, else originalLine.
812
+ line: record.line ?? record.originalLine,
813
+ side: record.side,
814
+ authoritativeTime: record.authoritativeTime,
815
+ windowRelation: record.windowRelation,
816
+ pullRequestReviewId: record.pullRequestReviewId,
817
+ })),
818
+ requestAttempts: input.attempts.map((attempt) => ({
819
+ attemptId: attempt.attemptId,
820
+ requestId: attempt.requestId,
821
+ observedHead: attempt.observedHead,
822
+ status: attempt.status,
823
+ marker: attempt.marker,
824
+ recoverySnapshotId: attempt.recoverySnapshotId,
825
+ })),
826
+ };
827
+ }