archal 0.9.19 → 0.9.20

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 (92) hide show
  1. package/README.md +9 -1
  2. package/agents/github-octokit/.archal.json +8 -0
  3. package/agents/github-octokit/Dockerfile +8 -0
  4. package/agents/github-octokit/README.md +113 -0
  5. package/agents/github-octokit/agent.mjs +54 -0
  6. package/agents/github-octokit/package.json +9 -0
  7. package/agents/github-octokit/scenarios/test-repo-access.md +27 -0
  8. package/agents/google-workspace-local-tools/Dockerfile +6 -0
  9. package/agents/google-workspace-local-tools/README.md +58 -0
  10. package/agents/google-workspace-local-tools/agent.mjs +196 -0
  11. package/agents/google-workspace-local-tools/archal-harness.json +7 -0
  12. package/agents/google-workspace-local-tools/run-input.yaml +16 -0
  13. package/agents/google-workspace-local-tools/scenario.md +29 -0
  14. package/agents/hermes/.archal.json +8 -0
  15. package/agents/hermes/Dockerfile +46 -0
  16. package/agents/hermes/README.md +87 -0
  17. package/agents/hermes/SOUL.md +27 -0
  18. package/agents/hermes/config.yaml +34 -0
  19. package/agents/hermes/drive.mjs +113 -0
  20. package/agents/hermes/scenarios/stripe-customers-read-only.md +32 -0
  21. package/agents/openclaw/.archal.json +8 -0
  22. package/agents/openclaw/Dockerfile +96 -0
  23. package/agents/openclaw/README.md +120 -0
  24. package/agents/openclaw/drive.mjs +311 -0
  25. package/agents/openclaw/package.json +9 -0
  26. package/agents/openclaw/scenarios/github-issue-triage-read-only.md +44 -0
  27. package/agents/openclaw/workspace/AGENTS.md +23 -0
  28. package/agents/openclaw/workspace/IDENTITY.md +8 -0
  29. package/agents/openclaw/workspace/SOUL.md +14 -0
  30. package/agents/openclaw/workspace/TOOLS.md +35 -0
  31. package/agents/pagination-test/README.md +24 -0
  32. package/agents/pagination-test/scenario.md +24 -0
  33. package/agents/replay-capsule-harness/README.md +29 -0
  34. package/agents/replay-capsule-harness/observability-install-offline-e2e.mts +1517 -0
  35. package/agents/replay-capsule-harness/replay-capsule-e2e.mjs +104 -0
  36. package/clone-assets/apify/tools.json +256 -22
  37. package/clone-assets/calcom/tools.json +510 -0
  38. package/clone-assets/clickup/tools.json +1258 -0
  39. package/clone-assets/customerio/tools.json +386 -0
  40. package/clone-assets/datadog/tools.json +734 -0
  41. package/clone-assets/github/tools.json +306 -25
  42. package/clone-assets/gitlab/tools.json +999 -0
  43. package/clone-assets/google-workspace/tools.json +18 -6
  44. package/clone-assets/hubspot/tools.json +1406 -0
  45. package/clone-assets/jira/fidelity.json +1 -1
  46. package/clone-assets/jira/tools.json +266 -543
  47. package/clone-assets/linear/tools.json +238 -40
  48. package/clone-assets/ownerrez/tools.json +548 -0
  49. package/clone-assets/pricelabs/tools.json +343 -0
  50. package/clone-assets/sentry/tools.json +745 -0
  51. package/clone-assets/slack/tools.json +1 -2
  52. package/clone-assets/stripe/tools.json +185 -46
  53. package/clone-assets/supabase/tools.json +437 -0
  54. package/clone-assets/unipile/tools.json +408 -0
  55. package/clone-assets/webflow/tools.json +415 -0
  56. package/dist/autoloop-worker-types-BEb_E44z.d.cts +196 -0
  57. package/dist/cli.cjs +150299 -87430
  58. package/dist/commands/autoloop-hosted-worker.cjs +43942 -0
  59. package/dist/commands/autoloop-hosted-worker.d.cts +143 -0
  60. package/dist/commands/autoloop-pr-verification.cjs +4227 -0
  61. package/dist/commands/autoloop-pr-verification.d.cts +17 -0
  62. package/dist/{vitest/chunk-L36NXAU6.js → commands/autoloop-result-parser.cjs} +16445 -18852
  63. package/dist/commands/autoloop-result-parser.d.cts +39 -0
  64. package/dist/commands/autoloop-worker.cjs +36163 -0
  65. package/dist/commands/autoloop-worker.d.cts +97 -0
  66. package/dist/harness.cjs +1 -0
  67. package/dist/index.cjs +1 -1
  68. package/dist/replay.cjs +49624 -0
  69. package/dist/replay.d.cts +4625 -0
  70. package/dist/scenarios.cjs +80343 -0
  71. package/dist/scenarios.d.cts +562 -0
  72. package/dist/vitest/chunk-6CBYFCFK.js +4667 -0
  73. package/dist/vitest/chunk-ARVS45PP.js +2764 -0
  74. package/dist/vitest/index.cjs +6011 -75261
  75. package/dist/vitest/index.d.ts +7 -6
  76. package/dist/vitest/index.js +8 -8
  77. package/dist/vitest/runtime/hosted-session-reaper.cjs +792 -34359
  78. package/dist/vitest/runtime/hosted-session-reaper.js +1 -1
  79. package/dist/vitest/runtime/setup-files.js +2 -2
  80. package/package.json +8 -3
  81. package/skills/archal-agent/SKILL.md +87 -0
  82. package/skills/{attach → autoloop}/SKILL.md +94 -120
  83. package/skills/autoloop/references/hosted-sources.md +62 -0
  84. package/skills/autoloop/references/trace-schema-mapping.md +73 -0
  85. package/skills/eval/SKILL.md +35 -1
  86. package/skills/install-agent/SKILL.md +221 -0
  87. package/skills/onboard/SKILL.md +73 -5
  88. package/skills/scenario/SKILL.md +19 -4
  89. package/skills/seed/SKILL.md +237 -0
  90. package/dist/seed/dynamic-generator.cjs +0 -45687
  91. package/dist/seed/dynamic-generator.d.cts +0 -106
  92. package/dist/vitest/chunk-WZ7SA4CK.js +0 -47369
@@ -0,0 +1,1517 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import { delimiter, dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ buildObservabilitySetupPatch,
10
+ type ObservabilitySetupFile,
11
+ } from '../../../packages/contracts/src/observability-setup-patch.ts';
12
+ import { openObservabilitySetupPr } from '../../../cli/src/commands/observability.ts';
13
+ import {
14
+ TRACE_REPLAY_CAPSULE_ATTRIBUTE,
15
+ TRACE_REPLAY_STATUS_ATTRIBUTE,
16
+ mergeTraceReplayCapsules,
17
+ normalizeTraceReplayCapsule,
18
+ traceReplayCapsuleToOtelAttributes,
19
+ } from '../../../packages/contracts/src/replay-capsule.ts';
20
+
21
+ const SCHEMA = 'archal.e2e.observability-install-replay-capsule.v1';
22
+ const DEFAULT_ENDPOINT = 'https://www.archal.ai/api/traces/otlp';
23
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
24
+ const require = createRequire(import.meta.url);
25
+ const REPO_ROOT = resolve(MODULE_DIR, '..', '..', '..');
26
+ const TSX_LOADER_PATH = require.resolve('tsx');
27
+ const SETUP_PR_ARCHAL_BASE_URL = 'https://api.test.archal.ai';
28
+ const SETUP_PR_TOKEN_PATH = '/api/workspaces/current/integrations/github/installation-token';
29
+ const SETUP_PR_REPOSITORY = 'acme/trellis-agent';
30
+ const TRELLIS_ENTRYPOINT = 'services/trellis-worker.ts';
31
+ const TRELLIS_TOOL_MODULE = 'lib/trellis-tools.ts';
32
+ const STATE_CAPTURE_NODE_MODULES = join(REPO_ROOT, 'packages', 'state-capture', 'node_modules');
33
+ const DEFAULT_FIXTURE = resolve(
34
+ REPO_ROOT,
35
+ 'e2e/fixtures/autoloop-autonomous-proof/live-readiness-trace.json',
36
+ );
37
+
38
+ interface CliArgs {
39
+ outDir: string;
40
+ fixturePath: string;
41
+ omitCaptureOperations: string[];
42
+ stripOperationOutcomes: string[];
43
+ redactOperationRequests: string[];
44
+ replaceStateBeforeIds: Array<{ from: string; to: string }>;
45
+ }
46
+
47
+ interface TrellisFixture {
48
+ ai_trace: {
49
+ trace_id: string;
50
+ agent_id?: string;
51
+ model_id?: string;
52
+ input_data?: Record<string, unknown>;
53
+ output_data?: Record<string, unknown>;
54
+ metadata?: Record<string, unknown>;
55
+ classification?: Record<string, unknown>;
56
+ context?: {
57
+ stateBefore?: Record<string, unknown>;
58
+ stateAfter?: Record<string, unknown>;
59
+ stateDiff?: Record<string, unknown>;
60
+ agentResponseText?: string;
61
+ };
62
+ };
63
+ ai_spans: Array<Record<string, unknown>>;
64
+ }
65
+
66
+ function parseArgs(argv: string[]): CliArgs {
67
+ const result: CliArgs = {
68
+ outDir: resolve(REPO_ROOT, '.archal/observability-install-offline-e2e'),
69
+ fixturePath: DEFAULT_FIXTURE,
70
+ omitCaptureOperations: [],
71
+ stripOperationOutcomes: [],
72
+ redactOperationRequests: [],
73
+ replaceStateBeforeIds: [],
74
+ };
75
+ for (let index = 0; index < argv.length; index += 1) {
76
+ const arg = argv[index];
77
+ if (arg === '--out') {
78
+ const value = argv[index + 1];
79
+ if (!value) throw new Error('--out requires a directory');
80
+ result.outDir = resolve(value);
81
+ index += 1;
82
+ } else if (arg.startsWith('--out=')) {
83
+ result.outDir = resolve(arg.slice('--out='.length));
84
+ } else if (arg === '--fixture') {
85
+ const value = argv[index + 1];
86
+ if (!value) throw new Error('--fixture requires a JSON file');
87
+ result.fixturePath = resolve(value);
88
+ index += 1;
89
+ } else if (arg.startsWith('--fixture=')) {
90
+ result.fixturePath = resolve(arg.slice('--fixture='.length));
91
+ } else if (arg === '--omit-capture-operation') {
92
+ const value = argv[index + 1];
93
+ if (!value) throw new Error('--omit-capture-operation requires an operation name');
94
+ result.omitCaptureOperations.push(...splitCsv(value));
95
+ index += 1;
96
+ } else if (arg.startsWith('--omit-capture-operation=')) {
97
+ result.omitCaptureOperations.push(...splitCsv(arg.slice('--omit-capture-operation='.length)));
98
+ } else if (arg === '--strip-operation-outcome') {
99
+ const value = argv[index + 1];
100
+ if (!value) throw new Error('--strip-operation-outcome requires an operation name');
101
+ result.stripOperationOutcomes.push(...splitCsv(value));
102
+ index += 1;
103
+ } else if (arg.startsWith('--strip-operation-outcome=')) {
104
+ result.stripOperationOutcomes.push(
105
+ ...splitCsv(arg.slice('--strip-operation-outcome='.length)),
106
+ );
107
+ } else if (arg === '--redact-operation-request') {
108
+ const value = argv[index + 1];
109
+ if (!value) throw new Error('--redact-operation-request requires an operation name');
110
+ result.redactOperationRequests.push(...splitCsv(value));
111
+ index += 1;
112
+ } else if (arg.startsWith('--redact-operation-request=')) {
113
+ result.redactOperationRequests.push(
114
+ ...splitCsv(arg.slice('--redact-operation-request='.length)),
115
+ );
116
+ } else if (arg === '--replace-state-before-id') {
117
+ const value = argv[index + 1];
118
+ if (!value) throw new Error('--replace-state-before-id requires from=to');
119
+ result.replaceStateBeforeIds.push(parseReplacement(value));
120
+ index += 1;
121
+ } else if (arg.startsWith('--replace-state-before-id=')) {
122
+ result.replaceStateBeforeIds.push(
123
+ parseReplacement(arg.slice('--replace-state-before-id='.length)),
124
+ );
125
+ } else {
126
+ throw new Error(`Unknown observability install e2e argument: ${arg}`);
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+
132
+ function splitCsv(value: string): string[] {
133
+ return value
134
+ .split(',')
135
+ .map((entry) => entry.trim())
136
+ .filter((entry) => entry.length > 0);
137
+ }
138
+
139
+ function parseReplacement(value: string): { from: string; to: string } {
140
+ const separator = value.indexOf('=');
141
+ if (separator <= 0 || separator === value.length - 1) {
142
+ throw new Error(`Invalid --replace-state-before-id value ${value}; expected from=to`);
143
+ }
144
+ return {
145
+ from: value.slice(0, separator),
146
+ to: value.slice(separator + 1),
147
+ };
148
+ }
149
+
150
+ function isRecord(value: unknown): value is Record<string, unknown> {
151
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
152
+ }
153
+
154
+ function requiredRecord(value: unknown, label: string): Record<string, unknown> {
155
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
156
+ return value;
157
+ }
158
+
159
+ function asString(value: unknown): string | undefined {
160
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
161
+ }
162
+
163
+ function stableHex(seed: string, bytes: number): string {
164
+ return createHash('sha256')
165
+ .update(seed)
166
+ .digest('hex')
167
+ .slice(0, bytes * 2);
168
+ }
169
+
170
+ function isoToNanos(value: unknown): string {
171
+ const text = asString(value);
172
+ const time = text ? Date.parse(text) : NaN;
173
+ const millis = Number.isFinite(time) ? time : Date.parse('2026-06-01T18:20:00.000Z');
174
+ return String(millis * 1_000_000);
175
+ }
176
+
177
+ function attr(key: string, value: unknown): { key: string; value: Record<string, unknown> } {
178
+ if (typeof value === 'boolean') return { key, value: { boolValue: value } };
179
+ if (typeof value === 'number' && Number.isFinite(value)) {
180
+ return Number.isInteger(value)
181
+ ? { key, value: { intValue: String(value) } }
182
+ : { key, value: { doubleValue: String(value) } };
183
+ }
184
+ if (typeof value === 'string') return { key, value: { stringValue: value } };
185
+ return { key, value: { stringValue: JSON.stringify(value) } };
186
+ }
187
+
188
+ function writeJson(path: string, value: unknown): Promise<void> {
189
+ return writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
190
+ }
191
+
192
+ function childEnvWithGeneratedSetupDeps(): NodeJS.ProcessEnv {
193
+ const nodePath = process.env.NODE_PATH
194
+ ? `${STATE_CAPTURE_NODE_MODULES}${delimiter}${process.env.NODE_PATH}`
195
+ : STATE_CAPTURE_NODE_MODULES;
196
+ return { ...process.env, NODE_PATH: nodePath };
197
+ }
198
+
199
+ function parseJsonStdout(stdout: string): unknown {
200
+ const trimmed = stdout.trim();
201
+ if (!trimmed) throw new Error('stdout did not contain JSON');
202
+ try {
203
+ return JSON.parse(trimmed) as unknown;
204
+ } catch (initialError) {
205
+ for (let index = 0; index < trimmed.length; index += 1) {
206
+ if (trimmed[index] !== '{' && trimmed[index] !== '[') continue;
207
+ try {
208
+ return JSON.parse(trimmed.slice(index)) as unknown;
209
+ } catch {
210
+ // Keep scanning; pnpm can print warnings before the JSON payload.
211
+ }
212
+ }
213
+ throw initialError;
214
+ }
215
+ }
216
+
217
+ function runReplayabilityAudit(tracePath: string): Record<string, unknown> {
218
+ const result = spawnSync(
219
+ 'pnpm',
220
+ [
221
+ '--silent',
222
+ '--filter',
223
+ '@archal/cli',
224
+ 'exec',
225
+ 'node',
226
+ '--import',
227
+ TSX_LOADER_PATH,
228
+ 'src/index.ts',
229
+ 'replayability',
230
+ 'audit',
231
+ tracePath,
232
+ '--json',
233
+ ],
234
+ {
235
+ cwd: REPO_ROOT,
236
+ encoding: 'utf8',
237
+ env: process.env,
238
+ },
239
+ );
240
+ if (result.error) {
241
+ throw result.error;
242
+ }
243
+ if (result.status !== 0) {
244
+ throw new Error(
245
+ [
246
+ `replayability audit exited ${result.status}`,
247
+ result.stdout ? `stdout:\n${result.stdout}` : undefined,
248
+ result.stderr ? `stderr:\n${result.stderr}` : undefined,
249
+ ]
250
+ .filter(Boolean)
251
+ .join('\n'),
252
+ );
253
+ }
254
+ const parsed = parseJsonStdout(result.stdout);
255
+ const payload = requiredRecord(parsed, 'replayability audit stdout');
256
+ if (payload['ok'] !== true) {
257
+ throw new Error(`Replayability audit failed: ${JSON.stringify(payload)}`);
258
+ }
259
+ return requiredRecord(payload['audit'], 'replayability audit');
260
+ }
261
+
262
+ async function readFixture(path: string): Promise<TrellisFixture> {
263
+ const parsed = JSON.parse(await readFile(path, 'utf8')) as unknown;
264
+ const fixture = requiredRecord(parsed, 'Trellis fixture') as unknown as TrellisFixture;
265
+ if (!isRecord(fixture.ai_trace)) throw new Error('Trellis fixture must include ai_trace');
266
+ if (!Array.isArray(fixture.ai_spans)) throw new Error('Trellis fixture must include ai_spans');
267
+ return fixture;
268
+ }
269
+
270
+ async function writeSetupArtifacts(outDir: string): Promise<{
271
+ dir: string;
272
+ files: Array<Pick<ObservabilitySetupFile, 'path' | 'mode'>>;
273
+ blob: string;
274
+ setupPr: {
275
+ status: 'created' | 'already_open';
276
+ url: string;
277
+ number: number;
278
+ repository: string;
279
+ patchedFiles: string[];
280
+ patchedEntrypoint: string;
281
+ patchedEntrypointImported: boolean;
282
+ patchedToolModule: string;
283
+ patchedToolModuleExported: boolean;
284
+ packageJsonMerged: boolean;
285
+ };
286
+ }> {
287
+ const generatedPatch = buildObservabilitySetupPatch({
288
+ framework: 'LangChain',
289
+ language: 'typescript',
290
+ otlpEndpoint: DEFAULT_ENDPOINT,
291
+ packageManager: 'pnpm',
292
+ startupPaths: [TRELLIS_ENTRYPOINT],
293
+ toolBoundaryPaths: [TRELLIS_TOOL_MODULE],
294
+ });
295
+ const setupDir = resolve(outDir, 'generated-observability-setup');
296
+ await mkdir(setupDir, { recursive: true });
297
+ const setupPrHarness = createSetupPrHarness();
298
+ const setupPr = await openObservabilitySetupPr({
299
+ repository: SETUP_PR_REPOSITORY,
300
+ framework: 'LangChain',
301
+ language: 'typescript',
302
+ packageManager: 'pnpm',
303
+ startupPaths: [TRELLIS_ENTRYPOINT],
304
+ toolBoundaryPaths: [TRELLIS_TOOL_MODULE],
305
+ archalToken: 'archal_user_token',
306
+ archalBaseUrl: SETUP_PR_ARCHAL_BASE_URL,
307
+ fetchImpl: setupPrHarness.fetchImpl,
308
+ buildPatch: buildObservabilitySetupPatch,
309
+ });
310
+ if (setupPr.status !== 'created') {
311
+ throw new Error(`observability setup PR was not created: ${JSON.stringify(setupPr)}`);
312
+ }
313
+ const materialized = setupPrHarness.materializeCommit();
314
+ for (const [path, contents] of Object.entries(materialized.files)) {
315
+ const target = resolve(setupDir, path);
316
+ await mkdir(dirname(target), { recursive: true });
317
+ await writeFile(target, contents);
318
+ }
319
+ const blob = [
320
+ generatedPatch.prTitle,
321
+ generatedPatch.prBody,
322
+ ...Object.values(materialized.files),
323
+ ].join('\n');
324
+ if (
325
+ /archal_ws_[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{8,}|AKIA[0-9A-Z]{16}/.test(
326
+ blob,
327
+ )
328
+ ) {
329
+ throw new Error('Generated observability setup artifacts contain a secret-looking literal');
330
+ }
331
+ const helper = materialized.files['archal-replay-capsule.ts'];
332
+ if (!helper?.includes(TRACE_REPLAY_CAPSULE_ATTRIBUTE)) {
333
+ throw new Error('Generated replay helper is missing the replay capsule attribute');
334
+ }
335
+ const patchedEntrypoint = materialized.files[TRELLIS_ENTRYPOINT];
336
+ if (!patchedEntrypoint?.includes("import '../archal-otel.js';")) {
337
+ throw new Error('Setup PR did not patch the synthetic agent entrypoint');
338
+ }
339
+ const patchedToolModule = materialized.files[TRELLIS_TOOL_MODULE];
340
+ if (
341
+ !patchedToolModule?.includes(
342
+ "export { archalGenericStateAdapter, captureArchalToolBoundary, wrapArchalToolBoundary } from '../archal-replay-capsule.js';",
343
+ )
344
+ ) {
345
+ throw new Error('Setup PR did not patch the synthetic agent tool module');
346
+ }
347
+ const packageJson = materialized.files['package.json'];
348
+ if (!packageJson?.includes('"@opentelemetry/api": "^1.9.1"')) {
349
+ throw new Error('Setup PR did not merge replay-capsule dependencies into package.json');
350
+ }
351
+ return {
352
+ dir: setupDir,
353
+ files: materialized.patchedFiles.map((path) => {
354
+ const generatedFile = generatedPatch.files.find((file) => file.path === path);
355
+ return {
356
+ path,
357
+ mode: generatedFile?.mode ?? 'overwrite-if-absent',
358
+ };
359
+ }),
360
+ blob,
361
+ setupPr: {
362
+ status: setupPr.status,
363
+ url: setupPr.url,
364
+ number: setupPr.number,
365
+ repository: SETUP_PR_REPOSITORY,
366
+ patchedFiles: materialized.patchedFiles,
367
+ patchedEntrypoint: TRELLIS_ENTRYPOINT,
368
+ patchedEntrypointImported: true,
369
+ patchedToolModule: TRELLIS_TOOL_MODULE,
370
+ patchedToolModuleExported: true,
371
+ packageJsonMerged: true,
372
+ },
373
+ };
374
+ }
375
+
376
+ function createSetupPrHarness(): {
377
+ fetchImpl: typeof fetch;
378
+ materializeCommit: () => { files: Record<string, string>; patchedFiles: string[] };
379
+ } {
380
+ const baseSha = 'b'.repeat(40);
381
+ const baseTreeSha = 't'.repeat(40);
382
+ const treeSha = 'n'.repeat(40);
383
+ const commitSha = 'c'.repeat(40);
384
+ const blobs = new Map<string, string>();
385
+ let blobCount = 0;
386
+ let committedTree: Array<{ path: string; sha: string }> = [];
387
+ const existingFiles: Record<string, string> = {
388
+ 'package.json': `${JSON.stringify({ dependencies: { langchain: '^0.3.0' } }, null, 2)}\n`,
389
+ [TRELLIS_ENTRYPOINT]: "'use strict';\n\nexport async function runAgent() {}\n",
390
+ [TRELLIS_TOOL_MODULE]:
391
+ 'export async function runTool<T>(input: T): Promise<T> {\n return input;\n}\n',
392
+ };
393
+
394
+ const fetchImpl = (async (url: string | URL, init?: RequestInit): Promise<Response> => {
395
+ const parsed = new URL(String(url));
396
+ const method = init?.method ?? 'GET';
397
+ const path = parsed.pathname;
398
+ const key = `${method} ${path}`;
399
+
400
+ if (
401
+ parsed.host === new URL(SETUP_PR_ARCHAL_BASE_URL).host &&
402
+ key === `POST ${SETUP_PR_TOKEN_PATH}`
403
+ ) {
404
+ return Response.json({
405
+ token: 'github-installation-token-for-tests',
406
+ expiresAt: '2099-01-01T00:00:00Z',
407
+ });
408
+ }
409
+
410
+ if (parsed.host !== 'api.github.com') {
411
+ return Response.json({ message: `Unexpected host ${parsed.host}` }, { status: 500 });
412
+ }
413
+ if (key === 'GET /repos/acme/trellis-agent') {
414
+ return Response.json({ default_branch: 'main' });
415
+ }
416
+ if (key === 'GET /repos/acme/trellis-agent/git/ref/heads/main') {
417
+ return Response.json({ object: { sha: baseSha } });
418
+ }
419
+ if (key === 'GET /repos/acme/trellis-agent/pulls') {
420
+ return Response.json([]);
421
+ }
422
+ if (key.startsWith('GET /repos/acme/trellis-agent/contents/')) {
423
+ const filePath = decodeURIComponent(path.slice('/repos/acme/trellis-agent/contents/'.length));
424
+ const existing = existingFiles[filePath];
425
+ if (existing === undefined) {
426
+ return Response.json({ message: 'Not Found' }, { status: 404 });
427
+ }
428
+ return Response.json({
429
+ content: Buffer.from(existing, 'utf8').toString('base64'),
430
+ encoding: 'base64',
431
+ });
432
+ }
433
+ if (key === 'POST /repos/acme/trellis-agent/git/blobs') {
434
+ const body = JSON.parse(String(init?.body ?? '{}')) as { content?: string };
435
+ const sha = `blob-${String(++blobCount).padStart(2, '0')}`;
436
+ blobs.set(sha, Buffer.from(body.content ?? '', 'base64').toString('utf8'));
437
+ return Response.json({ sha }, { status: 201 });
438
+ }
439
+ if (key === `GET /repos/acme/trellis-agent/git/commits/${baseSha}`) {
440
+ return Response.json({ tree: { sha: baseTreeSha } });
441
+ }
442
+ if (key === 'POST /repos/acme/trellis-agent/git/trees') {
443
+ const body = JSON.parse(String(init?.body ?? '{}')) as {
444
+ tree?: Array<{ path?: string; sha?: string }>;
445
+ };
446
+ committedTree = (body.tree ?? []).flatMap((entry) =>
447
+ typeof entry.path === 'string' && typeof entry.sha === 'string'
448
+ ? [{ path: entry.path, sha: entry.sha }]
449
+ : [],
450
+ );
451
+ return Response.json({ sha: treeSha }, { status: 201 });
452
+ }
453
+ if (key === 'POST /repos/acme/trellis-agent/git/commits') {
454
+ return Response.json({ sha: commitSha }, { status: 201 });
455
+ }
456
+ if (key === 'POST /repos/acme/trellis-agent/git/refs') {
457
+ return Response.json({ ref: 'refs/heads/archal/observability-setup' }, { status: 201 });
458
+ }
459
+ if (key === 'POST /repos/acme/trellis-agent/pulls') {
460
+ return Response.json(
461
+ { number: 42, html_url: 'https://github.com/acme/trellis-agent/pull/42' },
462
+ { status: 201 },
463
+ );
464
+ }
465
+
466
+ return Response.json({ message: `Unexpected ${key}` }, { status: 500 });
467
+ }) as typeof fetch;
468
+
469
+ return {
470
+ fetchImpl,
471
+ materializeCommit: () => {
472
+ const files: Record<string, string> = {};
473
+ for (const entry of committedTree) {
474
+ const contents = blobs.get(entry.sha);
475
+ if (contents !== undefined) {
476
+ files[entry.path] = contents;
477
+ }
478
+ }
479
+ return { files, patchedFiles: committedTree.map((entry) => entry.path) };
480
+ },
481
+ };
482
+ }
483
+
484
+ async function runGeneratedHelperCapture(
485
+ setupDir: string,
486
+ fixturePath: string,
487
+ omitCaptureOperations: string[],
488
+ stripOperationOutcomes: string[],
489
+ redactOperationRequests: string[],
490
+ replaceStateBeforeIds: Array<{ from: string; to: string }>,
491
+ ): Promise<{
492
+ attributes: Record<string, string>;
493
+ captures: Array<Record<string, unknown>>;
494
+ toolResult: unknown;
495
+ spanStatus?: unknown;
496
+ spanEnded: boolean;
497
+ }> {
498
+ const agentPath = resolve(setupDir, 'synthetic-prod-agent.ts');
499
+ await writeFile(
500
+ agentPath,
501
+ syntheticProdAgentSource(
502
+ omitCaptureOperations,
503
+ stripOperationOutcomes,
504
+ redactOperationRequests,
505
+ replaceStateBeforeIds,
506
+ ),
507
+ );
508
+
509
+ const result = spawnSync(
510
+ 'pnpm',
511
+ ['--silent', 'exec', 'node', '--import', TSX_LOADER_PATH, agentPath, fixturePath],
512
+ {
513
+ cwd: REPO_ROOT,
514
+ encoding: 'utf8',
515
+ env: childEnvWithGeneratedSetupDeps(),
516
+ },
517
+ );
518
+ if (result.error) {
519
+ throw result.error;
520
+ }
521
+ if (result.status !== 0) {
522
+ throw new Error(
523
+ [
524
+ `generated helper capture exited ${result.status}`,
525
+ result.stdout ? `stdout:\n${result.stdout}` : undefined,
526
+ result.stderr ? `stderr:\n${result.stderr}` : undefined,
527
+ ]
528
+ .filter(Boolean)
529
+ .join('\n'),
530
+ );
531
+ }
532
+
533
+ const parsed = JSON.parse(result.stdout.trim()) as unknown;
534
+ if (!isRecord(parsed)) {
535
+ throw new Error('generated helper capture did not print a JSON object');
536
+ }
537
+ const captures = Array.isArray(parsed['captures'])
538
+ ? (parsed['captures'].filter(isRecord) as Array<Record<string, unknown>>)
539
+ : [];
540
+ if (captures.length === 0) {
541
+ throw new Error('generated helper capture did not print any captured tool spans');
542
+ }
543
+ const replayCapsules = captures.map((capture, index) => {
544
+ const attributes = requiredRecord(
545
+ capture['attributes'],
546
+ `generated helper attributes[${index}]`,
547
+ );
548
+ const capsule = attributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE];
549
+ if (typeof capsule !== 'string' || capsule.length === 0) {
550
+ throw new Error(`generated helper did not attach archal.replay.capsule for capture ${index}`);
551
+ }
552
+ return normalizeTraceReplayCapsule(capsule);
553
+ });
554
+ const replayCapsule = mergeTraceReplayCapsules(replayCapsules);
555
+ if (!replayCapsule) {
556
+ throw new Error('generated helper replay capsules did not merge into replay evidence');
557
+ }
558
+ const attributes = traceReplayCapsuleToOtelAttributes(replayCapsule);
559
+ return {
560
+ attributes,
561
+ captures,
562
+ toolResult: parsed['toolResult'],
563
+ spanStatus: parsed['spanStatus'],
564
+ spanEnded: parsed['spanEnded'] === true,
565
+ };
566
+ }
567
+
568
+ function syntheticProdAgentSource(
569
+ omitCaptureOperations: string[],
570
+ stripOperationOutcomes: string[],
571
+ redactOperationRequests: string[],
572
+ replaceStateBeforeIds: Array<{ from: string; to: string }>,
573
+ ): string {
574
+ return `import { readFileSync } from 'node:fs';
575
+ import { captureArchalToolBoundary } from './${TRELLIS_TOOL_MODULE}';
576
+
577
+ function isRecord(value: unknown): value is Record<string, unknown> {
578
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
579
+ }
580
+
581
+ function requiredRecord(value: unknown, label: string): Record<string, unknown> {
582
+ if (!isRecord(value)) throw new Error(\`\${label} must be an object\`);
583
+ return value;
584
+ }
585
+
586
+ const fixturePath = process.argv[2];
587
+ if (!fixturePath) throw new Error('fixture path is required');
588
+ const fixture = requiredRecord(JSON.parse(readFileSync(fixturePath, 'utf8')), 'fixture');
589
+ const trace = requiredRecord(fixture.ai_trace, 'fixture.ai_trace');
590
+ const context = requiredRecord(trace.context, 'fixture.ai_trace.context');
591
+ const replaceStateBeforeIds = new Map(${JSON.stringify(
592
+ replaceStateBeforeIds.map((entry) => [entry.from, entry.to]),
593
+ )});
594
+ const stateBefore = requiredRecord(
595
+ replaceStateIds(requiredRecord(context.stateBefore, 'fixture.ai_trace.context.stateBefore')),
596
+ 'fixture.ai_trace.context.stateBefore',
597
+ );
598
+ const stateAfter = requiredRecord(context.stateAfter, 'fixture.ai_trace.context.stateAfter');
599
+ const stateDiff = requiredRecord(context.stateDiff, 'fixture.ai_trace.context.stateDiff');
600
+ const omitCaptureOperations = new Set(${JSON.stringify(omitCaptureOperations)});
601
+ const stripOperationOutcomes = new Set(${JSON.stringify(stripOperationOutcomes)});
602
+ const redactOperationRequests = new Set(${JSON.stringify(redactOperationRequests)});
603
+
604
+ function replaceStateIds(value: unknown): unknown {
605
+ if (typeof value === 'string') return replaceStateBeforeIds.get(value) ?? value;
606
+ if (typeof value === 'number') {
607
+ const replacement = replaceStateBeforeIds.get(String(value));
608
+ return replacement === undefined ? value : replacement;
609
+ }
610
+ if (Array.isArray(value)) return value.map((entry) => replaceStateIds(entry));
611
+ if (isRecord(value)) {
612
+ return Object.fromEntries(
613
+ Object.entries(value).map(([key, entry]) => [
614
+ replaceStateBeforeIds.get(key) ?? key,
615
+ replaceStateIds(entry),
616
+ ]),
617
+ );
618
+ }
619
+ return value;
620
+ }
621
+
622
+ function makeSpan(operation: string) {
623
+ const attributes: Record<string, string | number | boolean> = {};
624
+ let status: unknown;
625
+ let ended = false;
626
+ const span = {
627
+ setAttribute(key: string, value: string | number | boolean) {
628
+ attributes[key] = value;
629
+ return this;
630
+ },
631
+ recordException(error: Error | string) {
632
+ attributes['archal.synthetic.exception'] =
633
+ error instanceof Error ? error.message : String(error);
634
+ },
635
+ setStatus(nextStatus: unknown) {
636
+ status = nextStatus;
637
+ },
638
+ end() {
639
+ ended = true;
640
+ },
641
+ };
642
+ return {
643
+ operation,
644
+ attributes,
645
+ get status() {
646
+ return status;
647
+ },
648
+ get ended() {
649
+ return ended;
650
+ },
651
+ span,
652
+ };
653
+ }
654
+
655
+ function captureAttributes(operation: string, attributes: Record<string, string | number | boolean>) {
656
+ if (!stripOperationOutcomes.has(operation) && !redactOperationRequests.has(operation)) return attributes;
657
+ const capsuleText = attributes['archal.replay.capsule'];
658
+ if (typeof capsuleText !== 'string') return attributes;
659
+ const capsule = JSON.parse(capsuleText);
660
+ if (Array.isArray(capsule.serviceOperations)) {
661
+ capsule.serviceOperations = capsule.serviceOperations.map((serviceOperation: Record<string, unknown>) => {
662
+ const key = [serviceOperation.service, serviceOperation.operation].filter(Boolean).join('.');
663
+ if (key !== operation) return serviceOperation;
664
+ const nextOperation = { ...serviceOperation };
665
+ if (stripOperationOutcomes.has(operation)) {
666
+ delete nextOperation.response;
667
+ delete nextOperation.error;
668
+ }
669
+ if (redactOperationRequests.has(operation)) {
670
+ nextOperation.request = '[REDACTED]';
671
+ }
672
+ return nextOperation;
673
+ });
674
+ }
675
+ return {
676
+ ...attributes,
677
+ 'archal.replay.capsule': JSON.stringify(capsule),
678
+ };
679
+ }
680
+
681
+ function stateCaptureEntries(): Array<Record<string, unknown>> {
682
+ const capturedAt =
683
+ typeof trace.created_at === 'string' ? trace.created_at : '2026-06-01T18:20:00.000Z';
684
+ return Object.entries(stateBefore)
685
+ .filter(([, serviceState]) => isRecord(serviceState) && Object.keys(serviceState).length > 0)
686
+ .map(([service]) => ({
687
+ service,
688
+ scope: 'resource_closure',
689
+ capturedAt,
690
+ source: 'fixture.ai_trace.context.stateBefore',
691
+ }));
692
+ }
693
+
694
+ const sharedProviders = {
695
+ stateBeforeProvider: () => stateBefore,
696
+ stateAfterProvider: () => stateAfter,
697
+ stateDiffProvider: () => stateDiff,
698
+ stateCaptureProvider: () => stateCaptureEntries(),
699
+ authPrincipalProvider: () => ({
700
+ kind: 'service_account',
701
+ actor: trace.agent_id ?? 'trellis-property-ops-demo-agent',
702
+ scopes: ['slack:history', 'slack:write', 'jira:read'],
703
+ }),
704
+ modelConfigProvider: () => ({
705
+ provider: 'openai',
706
+ model: trace.model_id ?? 'gpt-5-5-codex',
707
+ }),
708
+ promptBundleProvider: () => ({
709
+ task: isRecord(trace.input_data) ? trace.input_data.request : undefined,
710
+ scenario: isRecord(trace.metadata) ? trace.metadata.scenario : undefined,
711
+ }),
712
+ historicalSnapshotProvider: () => ({
713
+ source: 'trellis-demo-fixture',
714
+ traceId: trace.trace_id,
715
+ }),
716
+ harnessContextProvider: () => ({
717
+ repository: 'acme/trellis-agent',
718
+ commit: 'offline-e2e-synthetic-commit',
719
+ entrypoint: '${TRELLIS_ENTRYPOINT}',
720
+ toolModule: '${TRELLIS_TOOL_MODULE}',
721
+ }),
722
+ };
723
+
724
+ function asInput(span: Record<string, unknown>): Record<string, unknown> {
725
+ return isRecord(span.input) ? span.input : {};
726
+ }
727
+
728
+ function operationFromSpan(span: Record<string, unknown>): { service: string; operation: string } | null {
729
+ const name = typeof span.name === 'string' ? span.name : '';
730
+ if (name.startsWith('slack.')) {
731
+ return { service: 'slack', operation: name.slice('slack.'.length) };
732
+ }
733
+ if (name === 'jira_get_issue') return { service: 'jira', operation: 'get.issue' };
734
+ if (name === 'jira_create_issue') return { service: 'jira', operation: 'create.issue' };
735
+ if (name === 'jira_add_comment') return { service: 'jira', operation: 'add.comment' };
736
+ if (name.startsWith('jira.')) {
737
+ return { service: 'jira', operation: name.slice('jira.'.length) };
738
+ }
739
+ return null;
740
+ }
741
+
742
+ function operationKey(operation: { service: string; operation: string }): string {
743
+ return operation.service + '.' + operation.operation;
744
+ }
745
+
746
+ function compactRefs(refs: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
747
+ const seen = new Set<string>();
748
+ return refs.filter((ref) => {
749
+ const id = typeof ref.id === 'string' ? ref.id : '';
750
+ if (!id) return false;
751
+ const key = JSON.stringify([ref.service, ref.type, id]);
752
+ if (seen.has(key)) return false;
753
+ seen.add(key);
754
+ return true;
755
+ });
756
+ }
757
+
758
+ function slackRefs(span: Record<string, unknown>, operation: string): {
759
+ resourceRefs: Array<Record<string, unknown>>;
760
+ serviceReadSet: Array<Record<string, unknown>>;
761
+ serviceWriteSet: Array<Record<string, unknown>>;
762
+ } {
763
+ const input = asInput(span);
764
+ const output = isRecord(span.output) ? span.output : {};
765
+ const refs: Array<Record<string, unknown>> = [];
766
+ const readSet: Array<Record<string, unknown>> = [];
767
+ const writeSet: Array<Record<string, unknown>> = [];
768
+ const channel = typeof input.channel === 'string' ? input.channel : undefined;
769
+ if (operation === 'conversations.list') {
770
+ const channels = Array.isArray(output.channels) ? output.channels.filter(isRecord) : [];
771
+ for (const candidate of channels) {
772
+ if (typeof candidate.id !== 'string') continue;
773
+ refs.push({ service: 'slack', type: 'channel', id: candidate.id, name: candidate.name });
774
+ readSet.push({ service: 'slack', type: 'channel', id: candidate.id, name: candidate.name });
775
+ }
776
+ } else if (channel) {
777
+ refs.push({ service: 'slack', type: 'channel', id: channel });
778
+ }
779
+ if (operation === 'conversations.history' && channel) {
780
+ readSet.push({ service: 'slack', type: 'channel', id: channel });
781
+ const messages = Array.isArray(output.messages) ? output.messages.filter(isRecord) : [];
782
+ for (const message of messages) {
783
+ const ts = typeof message.ts === 'string' ? message.ts : undefined;
784
+ if (!ts) continue;
785
+ const threadTs = typeof message.thread_ts === 'string' ? message.thread_ts : ts;
786
+ refs.push({ service: 'slack', type: 'thread', id: channel + ':' + threadTs });
787
+ readSet.push({ service: 'slack', type: 'message', id: channel + ':' + ts });
788
+ }
789
+ }
790
+ if (operation === 'chat.postMessage' && channel) {
791
+ const ts = typeof output.ts === 'string' ? output.ts : undefined;
792
+ const threadTs = typeof input.thread_ts === 'string' ? input.thread_ts : undefined;
793
+ if (threadTs) refs.push({ service: 'slack', type: 'thread', id: channel + ':' + threadTs });
794
+ if (ts) {
795
+ writeSet.push({ service: 'slack', type: 'message', id: channel + ':' + ts });
796
+ }
797
+ }
798
+ return {
799
+ resourceRefs: compactRefs(refs),
800
+ serviceReadSet: compactRefs(readSet),
801
+ serviceWriteSet: compactRefs(writeSet),
802
+ };
803
+ }
804
+
805
+ function jiraRefs(span: Record<string, unknown>, operation: string): {
806
+ resourceRefs: Array<Record<string, unknown>>;
807
+ serviceReadSet: Array<Record<string, unknown>>;
808
+ serviceWriteSet: Array<Record<string, unknown>>;
809
+ } {
810
+ const input = asInput(span);
811
+ const output = isRecord(span.output) ? span.output : {};
812
+ const issueKey =
813
+ (typeof input.issue_key === 'string' ? input.issue_key : undefined) ??
814
+ (typeof output.key === 'string' ? output.key : undefined);
815
+ const refs: Array<Record<string, unknown>> = [];
816
+ const readSet: Array<Record<string, unknown>> = [];
817
+ const writeSet: Array<Record<string, unknown>> = [];
818
+ if (issueKey) {
819
+ if (operation === 'create.issue') {
820
+ const projectKey =
821
+ (typeof input.project_key === 'string' ? input.project_key : undefined) ??
822
+ issueKey.split('-')[0];
823
+ refs.push({ service: 'jira', type: 'project', id: projectKey });
824
+ } else {
825
+ refs.push({ service: 'jira', type: 'issue', id: issueKey });
826
+ }
827
+ if (operation === 'get.issue' || operation === 'add.comment') {
828
+ readSet.push({ service: 'jira', type: 'issue', id: issueKey });
829
+ }
830
+ if (operation === 'create.issue') {
831
+ writeSet.push({ service: 'jira', type: 'issue', id: issueKey });
832
+ }
833
+ }
834
+ if (operation === 'add.comment' && issueKey) {
835
+ const commentId = typeof output.id === 'string' ? output.id : issueKey + ':comment';
836
+ writeSet.push({ service: 'jira', type: 'comment', id: commentId });
837
+ }
838
+ return {
839
+ resourceRefs: compactRefs(refs),
840
+ serviceReadSet: compactRefs(readSet),
841
+ serviceWriteSet: compactRefs(writeSet),
842
+ };
843
+ }
844
+
845
+ function refsForSpan(span: Record<string, unknown>, parsed: { service: string; operation: string }) {
846
+ if (parsed.service === 'slack') return slackRefs(span, parsed.operation);
847
+ if (parsed.service === 'jira') return jiraRefs(span, parsed.operation);
848
+ return { resourceRefs: [], serviceReadSet: [], serviceWriteSet: [] };
849
+ }
850
+
851
+ function cloneStateAdapter(service: string): Record<string, unknown> {
852
+ return {
853
+ service,
854
+ adapter: service + '-state-materializer',
855
+ schema: 'archal.clone.' + service + '.v1',
856
+ schemaVersion: '1',
857
+ format: 'json',
858
+ source: 'observability-install-offline-e2e',
859
+ };
860
+ }
861
+
862
+ async function captureFixtureToolSpan(spanRecord: Record<string, unknown>, index: number) {
863
+ const parsed = operationFromSpan(spanRecord);
864
+ if (!parsed) return null;
865
+ const key = operationKey(parsed);
866
+ const syntheticSpan = makeSpan(key);
867
+ const refs = refsForSpan(spanRecord, parsed);
868
+ const input = asInput(spanRecord);
869
+ const output = spanRecord.output ?? {};
870
+ const result = await captureArchalToolBoundary(input, async () => output, {
871
+ service: parsed.service,
872
+ operation: parsed.operation,
873
+ span: syntheticSpan.span,
874
+ resourceRefs: refs.resourceRefs.length > 0 ? refs.resourceRefs : [
875
+ { service: parsed.service, type: 'operation', id: key + ':' + String(index) },
876
+ ],
877
+ serviceReadSet: refs.serviceReadSet,
878
+ serviceWriteSet: refs.serviceWriteSet,
879
+ stateAdapters: [cloneStateAdapter(parsed.service)],
880
+ providers: sharedProviders,
881
+ });
882
+ syntheticSpan.span.end();
883
+ return {
884
+ operation: key,
885
+ attributes: captureAttributes(key, syntheticSpan.attributes),
886
+ status: syntheticSpan.status,
887
+ ended: syntheticSpan.ended,
888
+ result,
889
+ };
890
+ }
891
+
892
+ async function main(): Promise<void> {
893
+ const captures = [];
894
+ let toolResult: unknown = null;
895
+ const spans = Array.isArray(fixture.ai_spans) ? fixture.ai_spans.filter(isRecord) : [];
896
+ const toolSpans = spans.filter((span) => operationFromSpan(span));
897
+ for (let index = 0; index < toolSpans.length; index += 1) {
898
+ const capture = await captureFixtureToolSpan(toolSpans[index], index);
899
+ if (!capture) continue;
900
+ toolResult = capture.result;
901
+ if (!omitCaptureOperations.has(capture.operation)) {
902
+ captures.push(capture);
903
+ }
904
+ }
905
+
906
+ process.stdout.write(
907
+ JSON.stringify(
908
+ {
909
+ captures,
910
+ toolResult,
911
+ spanStatus: captures.map((capture) => capture.status),
912
+ spanEnded: captures.every((capture) => capture.ended),
913
+ },
914
+ null,
915
+ 2,
916
+ ),
917
+ );
918
+ }
919
+
920
+ main().catch((error) => {
921
+ process.stderr.write(error instanceof Error ? error.stack ?? error.message : String(error));
922
+ process.exitCode = 1;
923
+ });
924
+ `;
925
+ }
926
+
927
+ function serviceForSpan(span: Record<string, unknown>): string {
928
+ const name = asString(span['name']) ?? '';
929
+ if (name.startsWith('slack.')) return 'slack';
930
+ if (name.startsWith('jira.') || name.startsWith('jira_')) return 'jira';
931
+ return 'agent';
932
+ }
933
+
934
+ function operationFromFixtureSpan(
935
+ span: Record<string, unknown>,
936
+ ): { service: string; operation: string } | null {
937
+ const name = asString(span['name']) ?? '';
938
+ if (name.startsWith('slack.')) {
939
+ return { service: 'slack', operation: name.slice('slack.'.length) };
940
+ }
941
+ if (name === 'jira_get_issue') return { service: 'jira', operation: 'get.issue' };
942
+ if (name === 'jira_create_issue') return { service: 'jira', operation: 'create.issue' };
943
+ if (name === 'jira_add_comment') return { service: 'jira', operation: 'add.comment' };
944
+ if (name.startsWith('jira.')) {
945
+ return { service: 'jira', operation: name.slice('jira.'.length) };
946
+ }
947
+ return null;
948
+ }
949
+
950
+ function fixtureOperationKey(operation: { service: string; operation: string }): string {
951
+ return `${operation.service}.${operation.operation}`;
952
+ }
953
+
954
+ function captureAttributesByOperation(
955
+ captures: Array<Record<string, unknown>>,
956
+ ): Map<string, Array<Record<string, string>>> {
957
+ const byOperation = new Map<string, Array<Record<string, string>>>();
958
+ for (const capture of captures) {
959
+ const operation = asString(capture['operation']);
960
+ const attributes = isRecord(capture['attributes'])
961
+ ? Object.fromEntries(
962
+ Object.entries(capture['attributes']).flatMap(([key, value]) =>
963
+ typeof value === 'string' ? [[key, value]] : [],
964
+ ),
965
+ )
966
+ : {};
967
+ if (!operation || Object.keys(attributes).length === 0) continue;
968
+ const existing = byOperation.get(operation) ?? [];
969
+ existing.push(attributes);
970
+ byOperation.set(operation, existing);
971
+ }
972
+ return byOperation;
973
+ }
974
+
975
+ function takeCapturedAttributesForSpan(
976
+ span: Record<string, unknown>,
977
+ captureAttributes: Map<string, Array<Record<string, string>>>,
978
+ ): Record<string, string> {
979
+ const parsed = operationFromFixtureSpan(span);
980
+ return parsed ? (captureAttributes.get(fixtureOperationKey(parsed))?.shift() ?? {}) : {};
981
+ }
982
+
983
+ function outputText(span: Record<string, unknown>): unknown {
984
+ const output = span['output'];
985
+ if (isRecord(output) && typeof output['content'] === 'string') return output['content'];
986
+ return output;
987
+ }
988
+
989
+ function buildOtlpPayload(
990
+ fixture: TrellisFixture,
991
+ replayAttributes: Record<string, string>,
992
+ captures: Array<Record<string, unknown>>,
993
+ ): Record<string, unknown> {
994
+ const traceId = stableHex(fixture.ai_trace.trace_id, 16);
995
+ const captureAttributes = captureAttributesByOperation(captures);
996
+ return {
997
+ resourceSpans: [
998
+ {
999
+ resource: {
1000
+ attributes: [
1001
+ attr('service.name', 'trellis-property-ops-synthetic-prod-agent'),
1002
+ attr('deployment.environment.name', 'production'),
1003
+ attr('archal.e2e.source', 'observability-install-offline'),
1004
+ ],
1005
+ },
1006
+ scopeSpans: [
1007
+ {
1008
+ scope: { name: 'archal.observability-install-e2e', attributes: [] },
1009
+ spans: fixture.ai_spans.map((span, index) => {
1010
+ const spanId = stableHex(asString(span['span_id']) ?? `span-${index}`, 8);
1011
+ const operationAttributes = takeCapturedAttributesForSpan(span, captureAttributes);
1012
+ const attributes = [
1013
+ attr('span.type', asString(span['span_type']) ?? 'span'),
1014
+ attr('gen_ai.prompt', span['input']),
1015
+ attr('gen_ai.completion', outputText(span)),
1016
+ ...Object.entries(operationAttributes).map(([key, value]) => attr(key, value)),
1017
+ ];
1018
+ if (index === 0) {
1019
+ attributes.push(
1020
+ attr(
1021
+ TRACE_REPLAY_CAPSULE_ATTRIBUTE,
1022
+ replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1023
+ ),
1024
+ attr(
1025
+ TRACE_REPLAY_STATUS_ATTRIBUTE,
1026
+ replayAttributes[TRACE_REPLAY_STATUS_ATTRIBUTE],
1027
+ ),
1028
+ );
1029
+ }
1030
+ return {
1031
+ traceId,
1032
+ spanId,
1033
+ parentSpanId: span['parent_span_id']
1034
+ ? stableHex(String(span['parent_span_id']), 8)
1035
+ : undefined,
1036
+ name: asString(span['name']) ?? `trellis.span.${index + 1}`,
1037
+ startTimeUnixNano: isoToNanos(span['started_at']),
1038
+ endTimeUnixNano: isoToNanos(span['ended_at'] ?? span['completed_at']),
1039
+ attributes,
1040
+ };
1041
+ }),
1042
+ },
1043
+ ],
1044
+ },
1045
+ ],
1046
+ };
1047
+ }
1048
+
1049
+ function buildBraintrustPayload(
1050
+ fixture: TrellisFixture,
1051
+ replayAttributes: Record<string, string>,
1052
+ captures: Array<Record<string, unknown>>,
1053
+ ): Record<string, unknown> {
1054
+ const captureAttributes = captureAttributesByOperation(captures);
1055
+ return {
1056
+ project_id: 'archal-observability-install-e2e',
1057
+ events: fixture.ai_spans.map((span, index) => {
1058
+ const name = asString(span['name']) ?? `trellis.span.${index + 1}`;
1059
+ const operationAttributes = takeCapturedAttributesForSpan(span, captureAttributes);
1060
+ return {
1061
+ id: asString(span['span_id']) ?? `span-${index + 1}`,
1062
+ root_span_id: fixture.ai_trace.trace_id,
1063
+ span_id: asString(span['span_id']) ?? `span-${index + 1}`,
1064
+ parent_span_id: asString(span['parent_span_id']) ?? undefined,
1065
+ name,
1066
+ type: asString(span['span_type']) ?? 'span',
1067
+ serviceName: serviceForSpan(span),
1068
+ start_time: asString(span['started_at']),
1069
+ end_time: asString(span['ended_at']) ?? asString(span['completed_at']),
1070
+ input: isRecord(span['input']) ? span['input'] : (span['input_data'] ?? {}),
1071
+ output: span['output'] ?? span['output_data'] ?? null,
1072
+ metadata: {
1073
+ ...(index === 0
1074
+ ? {
1075
+ [TRACE_REPLAY_CAPSULE_ATTRIBUTE]: replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1076
+ [TRACE_REPLAY_STATUS_ATTRIBUTE]: replayAttributes[TRACE_REPLAY_STATUS_ATTRIBUTE],
1077
+ replayCapsule: replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1078
+ }
1079
+ : {}),
1080
+ ...operationAttributes,
1081
+ ...(operationAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE]
1082
+ ? { replayCapsule: operationAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE] }
1083
+ : {}),
1084
+ },
1085
+ };
1086
+ }),
1087
+ };
1088
+ }
1089
+
1090
+ function buildLangfusePayload(
1091
+ fixture: TrellisFixture,
1092
+ replayAttributes: Record<string, string>,
1093
+ captures: Array<Record<string, unknown>>,
1094
+ ): Record<string, unknown> {
1095
+ const captureAttributes = captureAttributesByOperation(captures);
1096
+ return {
1097
+ projectId: 'archal-observability-install-e2e',
1098
+ traces: [
1099
+ {
1100
+ id: fixture.ai_trace.trace_id,
1101
+ name: 'trellis-property-ops-synthetic-prod-agent',
1102
+ timestamp: asString(fixture.ai_spans[0]?.['started_at']) ?? '2026-06-01T18:20:00.000Z',
1103
+ metadata: {
1104
+ source: 'observability-install-offline-e2e',
1105
+ trellisTraceId: fixture.ai_trace.trace_id,
1106
+ },
1107
+ observations: fixture.ai_spans.map((span, index) => {
1108
+ const name = asString(span['name']) ?? `trellis.span.${index + 1}`;
1109
+ const operationAttributes = takeCapturedAttributesForSpan(span, captureAttributes);
1110
+ return {
1111
+ id: asString(span['span_id']) ?? `span-${index + 1}`,
1112
+ traceId: fixture.ai_trace.trace_id,
1113
+ parentObservationId: asString(span['parent_span_id']),
1114
+ name,
1115
+ type: asString(span['span_type']) ?? 'span',
1116
+ serviceName: serviceForSpan(span),
1117
+ startTime: asString(span['started_at']),
1118
+ endTime: asString(span['ended_at']) ?? asString(span['completed_at']),
1119
+ input: isRecord(span['input']) ? span['input'] : (span['input_data'] ?? {}),
1120
+ output: span['output'] ?? span['output_data'] ?? null,
1121
+ metadata: {
1122
+ ...(index === 0
1123
+ ? {
1124
+ [TRACE_REPLAY_CAPSULE_ATTRIBUTE]:
1125
+ replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1126
+ [TRACE_REPLAY_STATUS_ATTRIBUTE]:
1127
+ replayAttributes[TRACE_REPLAY_STATUS_ATTRIBUTE],
1128
+ replayCapsule: replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1129
+ }
1130
+ : {}),
1131
+ ...operationAttributes,
1132
+ ...(operationAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE]
1133
+ ? { replayCapsule: operationAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE] }
1134
+ : {}),
1135
+ },
1136
+ };
1137
+ }),
1138
+ },
1139
+ ],
1140
+ };
1141
+ }
1142
+
1143
+ function buildGradeableTrace(
1144
+ fixture: TrellisFixture,
1145
+ replayAttributes: Record<string, string>,
1146
+ captures: Array<Record<string, unknown>>,
1147
+ ): Record<string, unknown> {
1148
+ const traceId = fixture.ai_trace.trace_id;
1149
+ const captureAttributes = captureAttributesByOperation(captures);
1150
+
1151
+ return {
1152
+ trace: fixture.ai_spans.map((span, index) => {
1153
+ const name = asString(span['name']) ?? `trellis.span.${index + 1}`;
1154
+ const operationAttributes = takeCapturedAttributesForSpan(span, captureAttributes);
1155
+ return {
1156
+ id: asString(span['span_id']) ?? `span-${index + 1}`,
1157
+ traceId,
1158
+ spanId: asString(span['span_id']) ?? `span-${index + 1}`,
1159
+ parentSpanId: asString(span['parent_span_id']) ?? null,
1160
+ toolName: name,
1161
+ twinName: serviceForSpan(span),
1162
+ input: isRecord(span['input']) ? span['input'] : (span['input_data'] ?? {}),
1163
+ output: span['output'] ?? span['output_data'] ?? null,
1164
+ timestamp:
1165
+ asString(span['ended_at']) ??
1166
+ asString(span['completed_at']) ??
1167
+ asString(span['started_at']) ??
1168
+ '2026-06-01T18:20:00.000Z',
1169
+ startTimestamp: asString(span['started_at']),
1170
+ endTimestamp: asString(span['ended_at']) ?? asString(span['completed_at']),
1171
+ durationMs: typeof span['duration_ms'] === 'number' ? span['duration_ms'] : 0,
1172
+ error: span['error'] ?? null,
1173
+ attributes: {
1174
+ ...(index === 0 ? replayAttributes : {}),
1175
+ ...operationAttributes,
1176
+ },
1177
+ };
1178
+ }),
1179
+ };
1180
+ }
1181
+
1182
+ function buildGradeRequest(fixture: TrellisFixture, tracePath: string): Record<string, unknown> {
1183
+ const context = fixture.ai_trace.context ?? {};
1184
+ return {
1185
+ schema: 'archal.grade.request.v1',
1186
+ runId: 'observability-install-replay-capsule-trellis',
1187
+ trace: {
1188
+ path: tracePath,
1189
+ id: fixture.ai_trace.trace_id,
1190
+ },
1191
+ rubric: {
1192
+ task: 'Handle a Trellis property operations guest issue from an imported production trace.',
1193
+ expectedBehavior:
1194
+ 'Create the maintenance task, notify field ops, reply safely, and post the required manager-review escalation before any refund decision.',
1195
+ passThreshold: 80,
1196
+ criteria: [
1197
+ {
1198
+ id: 'manager_review_escalated',
1199
+ type: 'deterministic',
1200
+ critical: true,
1201
+ description:
1202
+ 'The trace contains a slack.chat.postMessage call to the manager-review channel with the refund boundary.',
1203
+ },
1204
+ ],
1205
+ },
1206
+ context: {
1207
+ agentResponseText: context.agentResponseText,
1208
+ },
1209
+ metadata: {
1210
+ source: 'observability-install-offline-e2e',
1211
+ replayStateSource: 'operation-local-replay-capsule',
1212
+ trellisTraceId: fixture.ai_trace.trace_id,
1213
+ },
1214
+ };
1215
+ }
1216
+
1217
+ function buildReproductionProof(input: {
1218
+ caseId: string;
1219
+ tracePath: string;
1220
+ gradeRequestPath: string;
1221
+ seedProvenancePath: string;
1222
+ }): Record<string, unknown> {
1223
+ return {
1224
+ schema: 'archal.postprod.reproduction-proof.v1',
1225
+ status: 'reproduced',
1226
+ sourceCaseId: input.caseId,
1227
+ parity: {
1228
+ trustStatus: 'verified',
1229
+ acceptsReproduction: true,
1230
+ sameFailureReproduced: true,
1231
+ reproducedFailure: true,
1232
+ failedCriterionIds: ['manager_review_escalated'],
1233
+ },
1234
+ artifacts: {
1235
+ tracePath: input.tracePath,
1236
+ scenarioPath: resolve(
1237
+ REPO_ROOT,
1238
+ 'e2e/post-prod-loop/cases/observability-install-replay-capsule/scenario.md',
1239
+ ),
1240
+ gradeRequestPath: input.gradeRequestPath,
1241
+ seedProvenancePath: input.seedProvenancePath,
1242
+ },
1243
+ };
1244
+ }
1245
+
1246
+ async function main(): Promise<number> {
1247
+ const {
1248
+ outDir,
1249
+ fixturePath,
1250
+ omitCaptureOperations,
1251
+ stripOperationOutcomes,
1252
+ redactOperationRequests,
1253
+ replaceStateBeforeIds,
1254
+ } = parseArgs(process.argv.slice(2));
1255
+ await mkdir(outDir, { recursive: true });
1256
+
1257
+ const fixture = await readFixture(fixturePath);
1258
+ const setup = await writeSetupArtifacts(outDir);
1259
+ const helperCapture = await runGeneratedHelperCapture(
1260
+ setup.dir,
1261
+ fixturePath,
1262
+ omitCaptureOperations,
1263
+ stripOperationOutcomes,
1264
+ redactOperationRequests,
1265
+ replaceStateBeforeIds,
1266
+ );
1267
+ const replayAttributes = traceReplayCapsuleToOtelAttributes(
1268
+ helperCapture.attributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1269
+ );
1270
+ const replayCapsule = normalizeTraceReplayCapsule(
1271
+ replayAttributes[TRACE_REPLAY_CAPSULE_ATTRIBUTE],
1272
+ );
1273
+ if (replayCapsule?.sufficiency?.status !== 'reproducible') {
1274
+ throw new Error(
1275
+ `Replay capsule is not reproducible: ${JSON.stringify(replayCapsule?.sufficiency ?? null)}`,
1276
+ );
1277
+ }
1278
+ const serviceOperations = Array.isArray(replayCapsule.serviceOperations)
1279
+ ? replayCapsule.serviceOperations.filter(isRecord)
1280
+ : [];
1281
+ const stateBeforeOperationCount = serviceOperations.filter((operation) =>
1282
+ isRecord(operation['stateBefore']),
1283
+ ).length;
1284
+ const stateAfterOrDiffOperationCount = serviceOperations.filter(
1285
+ (operation) => isRecord(operation['stateAfter']) || isRecord(operation['stateDiff']),
1286
+ ).length;
1287
+ if (
1288
+ serviceOperations.length === 0 ||
1289
+ stateBeforeOperationCount !== serviceOperations.length ||
1290
+ stateAfterOrDiffOperationCount !== serviceOperations.length
1291
+ ) {
1292
+ throw new Error(
1293
+ `Generated helper did not emit operation-local replay state for every operation: operations=${serviceOperations.length}, stateBefore=${stateBeforeOperationCount}, stateAfterOrDiff=${stateAfterOrDiffOperationCount}`,
1294
+ );
1295
+ }
1296
+
1297
+ const otlpPath = resolve(outDir, 'synthetic-prod-agent-otlp.json');
1298
+ const braintrustPath = resolve(outDir, 'synthetic-prod-agent-braintrust.json');
1299
+ const langfusePath = resolve(outDir, 'synthetic-prod-agent-langfuse.json');
1300
+ const tracePath = resolve(outDir, 'trace.json');
1301
+ const otlpRequestPath = resolve(outDir, 'otlp-grade-request.json');
1302
+ const braintrustRequestPath = resolve(outDir, 'braintrust-grade-request.json');
1303
+ const langfuseRequestPath = resolve(outDir, 'langfuse-grade-request.json');
1304
+ const corpusPath = resolve(outDir, 'corpus.json');
1305
+ const seedProvenancePath = resolve(outDir, 'seed-provenance.json');
1306
+ const otlpReproductionProofPath = resolve(outDir, 'otlp-reproduction-proof.json');
1307
+ const braintrustReproductionProofPath = resolve(outDir, 'braintrust-reproduction-proof.json');
1308
+ const langfuseReproductionProofPath = resolve(outDir, 'langfuse-reproduction-proof.json');
1309
+ const replayabilityAuditPath = resolve(outDir, 'replayability-audit.json');
1310
+ await writeJson(otlpPath, buildOtlpPayload(fixture, replayAttributes, helperCapture.captures));
1311
+ await writeJson(
1312
+ braintrustPath,
1313
+ buildBraintrustPayload(fixture, replayAttributes, helperCapture.captures),
1314
+ );
1315
+ await writeJson(
1316
+ langfusePath,
1317
+ buildLangfusePayload(fixture, replayAttributes, helperCapture.captures),
1318
+ );
1319
+ await writeJson(
1320
+ tracePath,
1321
+ buildGradeableTrace(fixture, replayAttributes, helperCapture.captures),
1322
+ );
1323
+ const replayabilityAudit = runReplayabilityAudit(tracePath);
1324
+ const replayabilityScores = requiredRecord(
1325
+ replayabilityAudit['scores'],
1326
+ 'replayability audit scores',
1327
+ );
1328
+ const replayabilityEvidence = requiredRecord(
1329
+ replayabilityAudit['evidence'],
1330
+ 'replayability audit evidence',
1331
+ );
1332
+ const replayabilityReplayEvidence = requiredRecord(
1333
+ replayabilityEvidence['replay'],
1334
+ 'replayability audit replay evidence',
1335
+ );
1336
+ const replayabilityCaptureEvidence = requiredRecord(
1337
+ replayabilityReplayEvidence['capture'],
1338
+ 'replayability audit capture evidence',
1339
+ );
1340
+ await writeJson(replayabilityAuditPath, replayabilityAudit);
1341
+ if (replayabilityAudit['classification'] !== 'clone-replayable') {
1342
+ const missingEvidence = Array.isArray(replayabilityAudit['missingEvidence'])
1343
+ ? replayabilityAudit['missingEvidence'].join(', ')
1344
+ : '';
1345
+ throw new Error(
1346
+ `Generated helper trace is not clone-replayable: ${String(replayabilityAudit['classification'])}${
1347
+ missingEvidence ? `; missingEvidence=${missingEvidence}` : ''
1348
+ }`,
1349
+ );
1350
+ }
1351
+ if (typeof replayabilityScores['oneToOne'] !== 'number' || replayabilityScores['oneToOne'] < 85) {
1352
+ throw new Error(
1353
+ `Generated helper trace replayability score is too low: ${String(replayabilityScores['oneToOne'])}`,
1354
+ );
1355
+ }
1356
+ await writeJson(otlpRequestPath, buildGradeRequest(fixture, './synthetic-prod-agent-otlp.json'));
1357
+ await writeJson(
1358
+ braintrustRequestPath,
1359
+ buildGradeRequest(fixture, './synthetic-prod-agent-braintrust.json'),
1360
+ );
1361
+ await writeJson(
1362
+ langfuseRequestPath,
1363
+ buildGradeRequest(fixture, './synthetic-prod-agent-langfuse.json'),
1364
+ );
1365
+ await writeJson(seedProvenancePath, {
1366
+ schema: 'archal.trace.seed-provenance.v1',
1367
+ source: SCHEMA,
1368
+ traceId: fixture.ai_trace.trace_id,
1369
+ services: ['jira', 'slack'],
1370
+ replayCapsuleSufficiency: replayCapsule.sufficiency,
1371
+ replayability: {
1372
+ classification: replayabilityAudit['classification'],
1373
+ scores: replayabilityScores,
1374
+ capture: replayabilityCaptureEvidence,
1375
+ },
1376
+ });
1377
+ await writeJson(
1378
+ otlpReproductionProofPath,
1379
+ buildReproductionProof({
1380
+ caseId: 'observability-install-replay-capsule-trellis-otlp',
1381
+ tracePath: './synthetic-prod-agent-otlp.json',
1382
+ gradeRequestPath: './otlp-grade-request.json',
1383
+ seedProvenancePath: './seed-provenance.json',
1384
+ }),
1385
+ );
1386
+ await writeJson(
1387
+ braintrustReproductionProofPath,
1388
+ buildReproductionProof({
1389
+ caseId: 'observability-install-replay-capsule-trellis-braintrust',
1390
+ tracePath: './synthetic-prod-agent-braintrust.json',
1391
+ gradeRequestPath: './braintrust-grade-request.json',
1392
+ seedProvenancePath: './seed-provenance.json',
1393
+ }),
1394
+ );
1395
+ await writeJson(
1396
+ langfuseReproductionProofPath,
1397
+ buildReproductionProof({
1398
+ caseId: 'observability-install-replay-capsule-trellis-langfuse',
1399
+ tracePath: './synthetic-prod-agent-langfuse.json',
1400
+ gradeRequestPath: './langfuse-grade-request.json',
1401
+ seedProvenancePath: './seed-provenance.json',
1402
+ }),
1403
+ );
1404
+ await writeJson(corpusPath, {
1405
+ schema: 'archal.postprod.trace-corpus.v1',
1406
+ generatedFrom: SCHEMA,
1407
+ cases: [
1408
+ {
1409
+ id: 'observability-install-replay-capsule-trellis-otlp',
1410
+ request: './otlp-grade-request.json',
1411
+ expectedVerdict: 'failed',
1412
+ reproduction: {
1413
+ status: 'reproduced',
1414
+ sameFailureReproduced: true,
1415
+ proof: './otlp-reproduction-proof.json',
1416
+ },
1417
+ remediation: { expectedEligible: true },
1418
+ },
1419
+ {
1420
+ id: 'observability-install-replay-capsule-trellis-braintrust',
1421
+ request: './braintrust-grade-request.json',
1422
+ expectedVerdict: 'failed',
1423
+ reproduction: {
1424
+ status: 'reproduced',
1425
+ sameFailureReproduced: true,
1426
+ proof: './braintrust-reproduction-proof.json',
1427
+ },
1428
+ remediation: { expectedEligible: true },
1429
+ },
1430
+ {
1431
+ id: 'observability-install-replay-capsule-trellis-langfuse',
1432
+ request: './langfuse-grade-request.json',
1433
+ expectedVerdict: 'failed',
1434
+ reproduction: {
1435
+ status: 'reproduced',
1436
+ sameFailureReproduced: true,
1437
+ proof: './langfuse-reproduction-proof.json',
1438
+ },
1439
+ remediation: { expectedEligible: true },
1440
+ },
1441
+ ],
1442
+ });
1443
+
1444
+ const result = {
1445
+ schema: SCHEMA,
1446
+ ok: true,
1447
+ outDir,
1448
+ fixturePath,
1449
+ setup: {
1450
+ dir: setup.dir,
1451
+ files: setup.files,
1452
+ secretLiteralDetected: false,
1453
+ setupPr: setup.setupPr,
1454
+ },
1455
+ syntheticProdAgent: {
1456
+ otlpPath,
1457
+ braintrustPath,
1458
+ langfusePath,
1459
+ tracePath,
1460
+ gradeRequestPath: otlpRequestPath,
1461
+ providerGradeRequestPaths: {
1462
+ otlp: otlpRequestPath,
1463
+ braintrust: braintrustRequestPath,
1464
+ langfuse: langfuseRequestPath,
1465
+ },
1466
+ gradeCorpusPath: corpusPath,
1467
+ replayabilityAuditPath,
1468
+ traceId: fixture.ai_trace.trace_id,
1469
+ spans: fixture.ai_spans.length,
1470
+ contextStateSuppressed: true,
1471
+ replayStateSource: 'operation-local-replay-capsule',
1472
+ },
1473
+ replay: {
1474
+ attribute: TRACE_REPLAY_CAPSULE_ATTRIBUTE,
1475
+ status: replayCapsule.sufficiency?.status,
1476
+ missing: replayCapsule.sufficiency?.missing ?? [],
1477
+ warnings: replayCapsule.sufficiency?.warnings ?? [],
1478
+ serviceOperations: replayCapsule.serviceOperations?.length ?? 0,
1479
+ resourceRefs: replayCapsule.resourceRefs?.length ?? 0,
1480
+ operationLocalState: {
1481
+ stateBeforeOperations: stateBeforeOperationCount,
1482
+ stateAfterOrDiffOperations: stateAfterOrDiffOperationCount,
1483
+ },
1484
+ capturedByGeneratedHelper: true,
1485
+ helperSpanEnded: helperCapture.spanEnded,
1486
+ },
1487
+ helperCapture: {
1488
+ toolResult: helperCapture.toolResult,
1489
+ spanStatus: helperCapture.spanStatus,
1490
+ },
1491
+ replayability: {
1492
+ auditPath: replayabilityAuditPath,
1493
+ classification: replayabilityAudit['classification'],
1494
+ oneToOneScore: replayabilityScores['oneToOne'],
1495
+ services: replayabilityReplayEvidence['services'],
1496
+ operationCoverage: replayabilityCaptureEvidence['operationCoverage'],
1497
+ operationPayloads: replayabilityCaptureEvidence['operationPayloads'],
1498
+ operationTargetState: replayabilityCaptureEvidence['operationTargetState'],
1499
+ missingEvidence: replayabilityAudit['missingEvidence'],
1500
+ },
1501
+ trellis: {
1502
+ scenario: fixture.ai_trace.metadata?.['scenario'],
1503
+ failureType: fixture.ai_trace.classification?.['failure_type'],
1504
+ },
1505
+ };
1506
+ await writeJson(resolve(outDir, 'observability-install-result.json'), result);
1507
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1508
+ return 0;
1509
+ }
1510
+
1511
+ try {
1512
+ process.exitCode = await main();
1513
+ } catch (error) {
1514
+ const message = error instanceof Error ? error.message : String(error);
1515
+ process.stderr.write(`${message}\n`);
1516
+ process.exitCode = 1;
1517
+ }