@tangle-network/traces 0.12.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_ANALYST_MODEL,
4
+ DEFAULT_QUESTION_MAX_COST_USD,
3
5
  DEFAULT_SANDBOX_BASE_URL,
6
+ MAX_TRACE_QUESTION_CHARS,
7
+ analysisEngineFromEnv,
4
8
  appendAll,
5
9
  assembleSessionBundle,
10
+ assertOutsideSourceBundle,
6
11
  buildPolicyEvidenceRecord,
12
+ buildSessionFactsReport,
7
13
  buildSessionIndexFromRows,
8
14
  cmdReplayVerify,
9
15
  cmdReplayVerifyBatch,
@@ -13,17 +19,21 @@ import {
13
19
  commandRedactor,
14
20
  executeUpload,
15
21
  exportTraceEvidenceFile,
22
+ externalFailureMessage,
16
23
  fileRunContextSupervisorRunReader,
24
+ findingRejectionDetail,
17
25
  haloAnalyzer,
18
26
  hodoscopeAnalyzer,
19
27
  importCodeTraceBench,
20
28
  indexSessionIdsByTrace,
21
29
  inspectSessionIndex,
22
- isFileRunContextDir,
30
+ isBridgeMismatchError,
23
31
  knownHarnesses,
32
+ loadTraceQuestionsFile,
24
33
  loadTracesConfig,
25
34
  locateSessions,
26
35
  mergeTracesConfig,
36
+ normalizeTraceQuestions,
27
37
  parseCorpusFlag,
28
38
  parseSession,
29
39
  parseSince,
@@ -33,12 +43,14 @@ import {
33
43
  readOtlpInput,
34
44
  readSessionIndexFile,
35
45
  renderInspectionReport,
46
+ renderSessionFacts,
36
47
  renderValidation,
37
48
  renderVerifiedFindingsSection,
38
49
  resolveAdapter,
39
50
  resolveRunWatchTarget,
40
51
  runTraceImprovement,
41
52
  runTraceInvestigation,
53
+ runTraceQuestions,
42
54
  saveReport,
43
55
  selectAdapters,
44
56
  serializePolicyEvidence,
@@ -57,89 +69,23 @@ import {
57
69
  writeOtlpFile,
58
70
  writePolicyEvidenceFile,
59
71
  writeSessionIndexFile,
60
- writeTraceEvidenceExportFile
61
- } from "./chunk-CV22VGCK.js";
72
+ writeTraceEvidenceExportFile,
73
+ writeTraceQuestionsArtifacts
74
+ } from "./chunk-7DOBU7BJ.js";
62
75
 
63
76
  // src/cli.ts
64
77
  import { readFileSync } from "fs";
65
- import { readdir, readFile, stat } from "fs/promises";
78
+ import { mkdir, mkdtemp, readFile, stat, writeFile } from "fs/promises";
79
+ import { tmpdir } from "os";
66
80
  import { basename, join, resolve } from "path";
67
81
  import {
68
82
  analyzeSupervisorRun,
69
83
  findSupervisorRunDirs,
70
- isUnavailable,
84
+ isRuntimeSupervisorRunDir,
71
85
  renderSupervisorRollupMarkdown,
72
86
  renderSupervisorRunMarkdown,
73
87
  rollupSupervisorRuns
74
88
  } from "@tangle-network/agent-eval/supervisor-run";
75
- import { createDspyRlmTraceEngine } from "@tangle-network/agent-eval/analyst";
76
-
77
- // src/analyst-model-call.ts
78
- import {
79
- callLlm,
80
- costReceiptFromLlm,
81
- costReceiptFromLlmError,
82
- LlmCallError
83
- } from "@tangle-network/agent-eval";
84
- var PROVIDER_ERROR_BODY_LIMIT = 2e3;
85
- function createAnalystModelCall(opts) {
86
- const { apiKey, baseUrl } = opts;
87
- return async ({ request, callId, signal }) => {
88
- try {
89
- const req = structuredClone(request);
90
- const response = await callLlm(req, {
91
- apiKey,
92
- baseUrl,
93
- signal,
94
- idempotencyKey: callId
95
- });
96
- return {
97
- succeeded: true,
98
- response,
99
- receipt: costReceiptFromLlm(response),
100
- execution: {
101
- callId,
102
- baseUrl,
103
- requestedModel: req.model,
104
- servedModel: response.servedModel ?? null,
105
- finishReason: response.finishReason ?? null,
106
- durationMs: response.durationMs
107
- }
108
- };
109
- } catch (error) {
110
- const err = error instanceof Error ? error : new Error(String(error));
111
- const aborted = signal.aborted;
112
- const message = aborted ? `AbortError: ${err.message}` : err.message;
113
- const detail = err instanceof LlmCallError ? { status: err.status, body: err.body.slice(0, PROVIDER_ERROR_BODY_LIMIT) } : {};
114
- return {
115
- succeeded: false,
116
- error: message,
117
- // costReceiptFromLlmError recovers the provider receipt when the
118
- // response completed but violated the contract; otherwise usage and
119
- // cost stay explicitly unknown.
120
- receipt: costReceiptFromLlmError(err) ?? {
121
- model: request.model,
122
- inputTokens: 0,
123
- outputTokens: 0,
124
- costUnknown: true,
125
- usageUnknown: true
126
- },
127
- execution: {
128
- callId,
129
- baseUrl,
130
- requestedModel: request.model,
131
- aborted,
132
- error: message,
133
- ...detail
134
- }
135
- };
136
- }
137
- };
138
- }
139
-
140
- // src/cli.ts
141
- var DEFAULT_ANALYST_MODEL = "gpt-5-mini";
142
- var TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
143
89
  function packageVersion() {
144
90
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
145
91
  if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
@@ -173,7 +119,8 @@ function parseArgs(argv) {
173
119
  attrs: [],
174
120
  concurrency: 4,
175
121
  verifyFindings: false,
176
- replayCorpora: []
122
+ replayCorpora: [],
123
+ questions: []
177
124
  };
178
125
  for (let i = 1; i < argv.length; i++) {
179
126
  const arg = argv[i];
@@ -231,6 +178,9 @@ function parseArgs(argv) {
231
178
  case "--otlp":
232
179
  a.otlp = next();
233
180
  break;
181
+ case "--source-bundle":
182
+ a.sourceBundle = next();
183
+ break;
234
184
  case "--otlp-out":
235
185
  a.otlpOut = next();
236
186
  break;
@@ -310,6 +260,17 @@ function parseArgs(argv) {
310
260
  case "--analyzer-prompt":
311
261
  a.analyzerPrompt = next();
312
262
  break;
263
+ case "--question": {
264
+ const v = next();
265
+ if (v !== void 0) a.questions.push(v);
266
+ break;
267
+ }
268
+ case "--questions":
269
+ a.questionsFile = next();
270
+ break;
271
+ case "--question-budget":
272
+ a.questionBudget = Number(next());
273
+ break;
313
274
  case "--redactor":
314
275
  a.redactorCmd = next();
315
276
  break;
@@ -332,7 +293,7 @@ function parseArgs(argv) {
332
293
  }
333
294
  return a;
334
295
  }
335
- var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set(["analyze", "investigate", "improve", "stream", "validate"]);
296
+ var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set(["analyze", "facts", "investigate", "improve", "ask", "stream", "validate"]);
336
297
  function migrateWritingOtlpFlag(args) {
337
298
  if (args.otlp === void 0 || OTLP_INPUT_COMMANDS.has(args.command)) return args;
338
299
  if (args.otlpOut !== void 0) {
@@ -358,8 +319,10 @@ function validateOtlpSelection(raw) {
358
319
  }
359
320
  var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
360
321
  "analyze",
322
+ "facts",
361
323
  "investigate",
362
324
  "improve",
325
+ "ask",
363
326
  "convert",
364
327
  "index",
365
328
  "evidence",
@@ -368,8 +331,10 @@ var CURRENT_SESSION_COMMANDS = /* @__PURE__ */ new Set([
368
331
  ]);
369
332
  var WORKFLOW_COMMANDS = /* @__PURE__ */ new Set([
370
333
  "analyze",
334
+ "facts",
371
335
  "investigate",
372
336
  "improve",
337
+ "ask",
373
338
  "convert",
374
339
  "index",
375
340
  "evidence"
@@ -484,41 +449,11 @@ async function resolveSelectedSession(args) {
484
449
  `session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
485
450
  );
486
451
  }
487
- function buildAnalysisEngine(model, budgetUsd) {
488
- const tangleKey = process.env.TANGLE_API_KEY;
489
- const openAiKey = process.env.OPENAI_API_KEY;
490
- const apiKey = tangleKey || openAiKey;
491
- if (!apiKey) {
492
- throw new Error(
493
- "--llm needs a model key: TANGLE_API_KEY for the Tangle router (the default endpoint), or OPENAI_API_KEY for OpenAI. Set OPENAI_BASE_URL to target any other OpenAI-compatible gateway. Deterministic analysis needs no key."
494
- );
495
- }
496
- const baseUrl = process.env.OPENAI_BASE_URL || (tangleKey ? TANGLE_ROUTER_BASE_URL : "https://api.openai.com/v1");
497
- const python = process.env.TRACES_PYTHON;
498
- return createDspyRlmTraceEngine({
499
- call: createAnalystModelCall({ apiKey, baseUrl }),
500
- callRef: `traces-cli:${baseUrl}#${model}`,
501
- recordExecution: (observation) => {
502
- analystLog(
503
- `[analyst] model call ${observation.sequence} ${observation.succeeded ? "ok" : "FAIL"} ${observation.model}`,
504
- observation.succeeded ? void 0 : { error: observation.error }
505
- );
506
- },
507
- model,
508
- // Pinned, not defaulted: one findings array from a current coding model
509
- // needs this cap, and glm-5.2 counts reasoning tokens in
510
- // completion_tokens — a smaller cap breaches its cost reservation and the
511
- // fail-closed ledger then refuses every later call in the run.
512
- maxOutputTokens: 16384,
513
- // maxCostUsd defaults to $1 per analyst — a proxy-side ceiling separate
514
- // from --budget. With the larger token cap the per-call reservation grows
515
- // ~4x, so that default binds before the registry's --budget allocation
516
- // and kills analysts mid-run. --budget is the operator's spend authority
517
- // and the registry still splits it across analysts, so forwarding it here
518
- // only stops the engine's own default from cutting runs short.
519
- ...budgetUsd !== void 0 && Number.isFinite(budgetUsd) && budgetUsd > 0 ? { maxCostUsd: budgetUsd } : {},
520
- ...python ? { runner: { command: python } } : {}
521
- });
452
+ function llmAnalysisEngine(args, model) {
453
+ return analysisEngineFromEnv({ model, maxCostUsd: args.budget, log: analystLog });
454
+ }
455
+ function analystModelFor(args) {
456
+ return args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
522
457
  }
523
458
  function requiredBridgeVersion() {
524
459
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -528,11 +463,14 @@ function requiredBridgeVersion() {
528
463
  }
529
464
  return version;
530
465
  }
531
- function assertAgenticAnalystsRan(args, agenticPerAnalyst) {
532
- if (!args.llm) return;
533
- if (!totalAgenticFailureMessage(agenticPerAnalyst)) return;
534
- const message = totalAgenticFailureMessage(agenticPerAnalyst, { requiredBridgeVersion: requiredBridgeVersion() });
535
- throw new Error(message);
466
+ function assertRequestedAnalysesRan(args, result) {
467
+ const messages = [];
468
+ if (args.llm && totalAgenticFailureMessage(result.agenticPerAnalyst)) {
469
+ messages.push(totalAgenticFailureMessage(result.agenticPerAnalyst, { requiredBridgeVersion: requiredBridgeVersion() }));
470
+ }
471
+ const external = externalFailureMessage(result.external);
472
+ if (external) messages.push(external);
473
+ if (messages.length > 0) throw new Error(messages.join("\n"));
536
474
  }
537
475
  async function cmdList(args) {
538
476
  const groups = await discover({ ...args, last: args.last || 20 });
@@ -582,6 +520,16 @@ async function collectOtlpSpans(path) {
582
520
  };
583
521
  }
584
522
  async function collectSpans(args) {
523
+ if (args.sourceBundle) {
524
+ if (!["analyze", "investigate", "improve", "ask"].includes(args.command)) throw new Error("--source-bundle is an analysis option");
525
+ if (args.session || args.otlp || args.input || args.current || args.workflow || args.noContent || args.redactorCmd) {
526
+ throw new Error("--source-bundle cannot be combined with another input, redaction, or metadata-only selection");
527
+ }
528
+ for (const output of [args.out, args.dir, args.otlpOut]) {
529
+ if (output) await assertOutsideSourceBundle(args.sourceBundle, output);
530
+ }
531
+ return collectOtlpSpans(join(args.sourceBundle, "derived", "trace.otlp.jsonl"));
532
+ }
585
533
  if (args.otlp) return collectOtlpSpans(args.otlp);
586
534
  if (args.input) {
587
535
  if (args.workflow) throw new Error("--workflow reads discovered sessions and cannot be combined with an input file");
@@ -647,6 +595,9 @@ function workflowIssueText(issue) {
647
595
  if (issue.kind === "parent-conflict") {
648
596
  return `parent conflict for ${issue.sessionId}: declared ${issue.declaredParentSessionId ?? "none"}, referenced by ${issue.referencedParentSessionIds.join(", ") || "none"}`;
649
597
  }
598
+ if (issue.kind === "unjoined-child") {
599
+ return `unjoined child ${issue.agentPath} spawned by ${issue.parentSessionId}: ${issue.reason}` + (issue.candidates ? ` (${issue.candidates.join(", ")})` : "");
600
+ }
650
601
  return `${issue.kind} ${issue.sessionId}, referenced as ${issue.relation} by ${issue.referencedBySessionId}`;
651
602
  }
652
603
  function warnIncompleteWorkflow(workflow) {
@@ -754,6 +705,30 @@ async function cmdBundleView(args) {
754
705
  `${manifest.view} view \u2192 ${result.directory} (${manifest.files.length} file(s) carried, ${manifest.excluded.length} excluded, ${leakCheck.matches} session-text match(es) against ${leakCheck.sourceSignatures} signature(s) from ${leakCheck.comparedSources.length} excluded source(s))`
755
706
  );
756
707
  }
708
+ async function cmdFacts(args) {
709
+ const format = args.format ?? "json";
710
+ if (format !== "json" && format !== "text") {
711
+ throw new Error(`unknown facts format "${format}" (expected json or text)`);
712
+ }
713
+ const collected = await collectSpans(args);
714
+ if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
715
+ warnIncompleteWorkflow(collected.workflow);
716
+ const report = buildSessionFactsReport(collected.spans, { harness: collected.harness });
717
+ const unread = report.sessions.filter((facts) => facts.recordSpans === 0);
718
+ if (unread.length > 0) {
719
+ throw new Error(
720
+ `no records could be read from ${unread.length} selected session(s): ` + unread.map((facts) => `${facts.sessionId ?? facts.traceId} (${facts.unreadRecords.value ?? 0} unread record(s))`).join(", ") + ". The facts sheet would state zeros the spans cannot support."
721
+ );
722
+ }
723
+ const rendered = format === "json" ? `${JSON.stringify(report, null, 2)}
724
+ ` : renderSessionFacts(report);
725
+ if (args.out) {
726
+ await writeFile(args.out, rendered, "utf8");
727
+ console.log(`session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`);
728
+ return;
729
+ }
730
+ process.stdout.write(rendered);
731
+ }
757
732
  async function cmdInspect(args) {
758
733
  if (!args.input) throw new Error("inspect needs an index file; run `traces index --out session-index.json` first");
759
734
  const index = await readSessionIndexFile(args.input);
@@ -876,7 +851,7 @@ ${renderVerifiedFindingsSection(run)}`;
876
851
  } else {
877
852
  console.log(report);
878
853
  }
879
- assertAgenticAnalystsRan(args, result.agenticPerAnalyst);
854
+ assertRequestedAnalysesRan(args, result);
880
855
  }
881
856
  async function verifyAnalyzeFindings(args, result) {
882
857
  if (args.replayCorpora.length === 0) {
@@ -898,55 +873,24 @@ async function verifyAnalyzeFindings(args, result) {
898
873
  });
899
874
  }
900
875
  async function cmdAnalyzeSupervisorRun(runDir, args) {
901
- const runContextDirs = await findFileRunContextDirs(runDir);
902
- const nested = runContextDirs.length > 0 ? [] : await findSupervisorRunDirs(runDir);
903
- let markdown;
904
- if (runContextDirs.length > 1) {
905
- markdown = renderSupervisorRollupMarkdown(
906
- rollupSupervisorRuns(
907
- await Promise.all(
908
- runContextDirs.map((dir) => analyzeSupervisorRun(fileRunContextSupervisorRunReader(dir)))
909
- )
910
- ),
911
- `Supervisor rollup \u2014 ${runDir}`
912
- );
913
- } else if (runContextDirs.length === 1) {
914
- markdown = renderSupervisorRunMarkdown(
915
- await analyzeSupervisorRun(fileRunContextSupervisorRunReader(runContextDirs[0]))
916
- );
917
- } else if (nested.length > 0) {
918
- markdown = renderSupervisorRollupMarkdown(
919
- rollupSupervisorRuns(await Promise.all(nested.map((dir) => analyzeSupervisorRun(dir)))),
920
- `Supervisor rollup \u2014 ${runDir}`
876
+ const single = await isRuntimeSupervisorRunDir(runDir);
877
+ const runDirs = single ? [runDir] : await findSupervisorRunDirs(runDir);
878
+ if (runDirs.length === 0) {
879
+ throw new Error(
880
+ `no supervisor run found at ${runDir} \u2014 expected a Runtime run containing spawn-journal.jsonl, observer.jsonl, or failure.json, or a parent directory containing such runs`
921
881
  );
922
- } else {
923
- const report = await analyzeSupervisorRun(runDir);
924
- if (isUnavailable(report.orchestration.workersSpawned)) {
925
- throw new Error(
926
- `no supervisor run found at ${runDir} \u2014 expected <runDir>/spawn-journal.jsonl (agent-runtime createFileRunContext), <runDir>/ws/.agent/supervisor/<id> (or the pre-rename <runDir>/ws/.loops/supervisor/<id>), or a parent directory containing such runs`
927
- );
928
- }
929
- markdown = renderSupervisorRunMarkdown(report);
930
882
  }
931
- const runs = Math.max(runContextDirs.length, nested.length, 1);
883
+ const reports = await Promise.all(
884
+ runDirs.map((dir) => analyzeSupervisorRun(fileRunContextSupervisorRunReader(dir)))
885
+ );
886
+ const markdown = single ? renderSupervisorRunMarkdown(reports[0]) : renderSupervisorRollupMarkdown(rollupSupervisorRuns(reports), `Supervisor rollup \u2014 ${runDir}`);
932
887
  if (args.out) {
933
888
  await saveReport(args.out, markdown);
934
- console.log(`supervisor report \u2192 ${args.out} (${runs === 1 ? "1 run" : `${runs} runs`})`);
889
+ console.log(`supervisor report \u2192 ${args.out} (${runDirs.length === 1 ? "1 run" : `${runDirs.length} runs`})`);
935
890
  } else {
936
891
  console.log(markdown);
937
892
  }
938
893
  }
939
- async function findFileRunContextDirs(dir) {
940
- if (await isFileRunContextDir(dir)) return [dir];
941
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
942
- const found = [];
943
- for (const entry of entries) {
944
- if (!entry.isDirectory()) continue;
945
- const child = join(dir, entry.name);
946
- if (await isFileRunContextDir(child)) found.push(child);
947
- }
948
- return found.sort();
949
- }
950
894
  async function collectImportedSpans(args) {
951
895
  if (!args.input) throw new Error("input file missing");
952
896
  const attributes = await loadExportAttributes(args);
@@ -977,6 +921,13 @@ async function collectImportedSpans(args) {
977
921
  };
978
922
  }
979
923
  function analystLog(msg, fields) {
924
+ const rejection = findingRejectionDetail(msg, fields);
925
+ if (rejection !== void 0) {
926
+ process.stderr.write(rejection ? `${msg} \u2014 ${rejection}
927
+ ` : `${msg}
928
+ `);
929
+ return;
930
+ }
980
931
  const error = typeof fields?.error === "string" && fields.error ? fields.error : void 0;
981
932
  const errorClass = typeof fields?.error_class === "string" && fields.error_class ? `${fields.error_class}: ` : "";
982
933
  process.stderr.write(error ? `${msg} \u2014 ${errorClass}${error}
@@ -996,7 +947,7 @@ async function cmdInvestigate(args) {
996
947
  } else {
997
948
  console.log(result.report);
998
949
  }
999
- assertAgenticAnalystsRan(args, result.agenticPerAnalyst);
950
+ assertRequestedAnalysesRan(args, result);
1000
951
  }
1001
952
  function conformanceOptions(collected) {
1002
953
  return {
@@ -1014,9 +965,10 @@ async function investigate(args, options = {}) {
1014
965
  const { spans, harness, cwds, sources, workflow } = collected;
1015
966
  if (spans.length === 0) throw new Error("no spans found for the given selection");
1016
967
  const config = args.config !== void 0 || options.loadDefaultConfig !== false ? await loadTracesConfig(args.config) : void 0;
1017
- const analystModel = args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
1018
- const engine = args.llm ? buildAnalysisEngine(analystModel, args.budget) : void 0;
968
+ const analystModel = analystModelFor(args);
969
+ const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
1019
970
  return runTraceInvestigation(mergeTracesConfig({
971
+ sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
1020
972
  spans,
1021
973
  harness,
1022
974
  sources,
@@ -1038,10 +990,11 @@ async function cmdImprove(args) {
1038
990
  const { spans, harness, cwds, sources, workflow } = collected;
1039
991
  if (spans.length === 0) throw new Error("no spans found for the given selection");
1040
992
  const config = await loadTracesConfig(args.config);
1041
- const analystModel = args.model ?? process.env.TRACES_ANALYST_MODEL ?? DEFAULT_ANALYST_MODEL;
1042
- const engine = args.llm ? buildAnalysisEngine(analystModel, args.budget) : void 0;
993
+ const analystModel = analystModelFor(args);
994
+ const engine = args.llm ? llmAnalysisEngine(args, analystModel) : void 0;
1043
995
  const result = await runTraceImprovement({
1044
996
  ...mergeTracesConfig({
997
+ sourceBundle: args.sourceBundle ? { path: args.sourceBundle } : void 0,
1045
998
  spans,
1046
999
  harness,
1047
1000
  sources,
@@ -1064,7 +1017,76 @@ async function cmdImprove(args) {
1064
1017
  console.log(
1065
1018
  `improvement artifacts \u2192 ${dir} (${result.findings.length} findings with actions and checks, OTLP: ${result.otlpPath})`
1066
1019
  );
1067
- assertAgenticAnalystsRan(args, result.agenticPerAnalyst);
1020
+ assertRequestedAnalysesRan(args, result);
1021
+ }
1022
+ async function cmdAsk(args) {
1023
+ if (args.out) throw new Error("ask writes a directory of artifacts; pass --dir <dir> instead of --out");
1024
+ if (args.llm) throw new Error("ask always uses the model-backed engine; drop --llm");
1025
+ const questions = [
1026
+ ...args.questionsFile ? await loadTraceQuestionsFile(args.questionsFile) : [],
1027
+ ...args.questions.map((question) => ({ question }))
1028
+ ];
1029
+ if (questions.length === 0) {
1030
+ throw new Error('ask needs --question "<text>" (repeatable) or --questions <questions.json>');
1031
+ }
1032
+ normalizeTraceQuestions(questions);
1033
+ if (args.questionBudget !== void 0 && (!Number.isFinite(args.questionBudget) || args.questionBudget <= 0)) {
1034
+ throw new Error("--question-budget must be a positive number of USD");
1035
+ }
1036
+ if (args.budget !== void 0 && (!Number.isFinite(args.budget) || args.budget <= 0)) {
1037
+ throw new Error("--budget must be a positive number of USD");
1038
+ }
1039
+ const engine = analysisEngineFromEnv({
1040
+ model: analystModelFor(args),
1041
+ maxCostUsd: args.questionBudget ?? Math.min(args.budget ?? Infinity, DEFAULT_QUESTION_MAX_COST_USD),
1042
+ log: analystLog
1043
+ });
1044
+ const collected = await collectSpans(args);
1045
+ if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
1046
+ warnIncompleteWorkflow(collected.workflow);
1047
+ const directory = resolve(args.dir ?? await mkdtemp(join(tmpdir(), "traces-ask-")));
1048
+ await mkdir(directory, { recursive: true });
1049
+ const controller = new AbortController();
1050
+ const interrupt = () => controller.abort(new Error("ask interrupted"));
1051
+ process.once("SIGINT", interrupt);
1052
+ let result;
1053
+ try {
1054
+ result = await runTraceQuestions({
1055
+ questions,
1056
+ spans: collected.spans,
1057
+ engine,
1058
+ harness: collected.harness,
1059
+ concurrency: args.concurrency,
1060
+ ...args.budget !== void 0 ? { budgetUsd: args.budget } : {},
1061
+ ...args.sourceBundle ? { sourceBundle: { path: args.sourceBundle } } : {},
1062
+ otlpOutPath: args.otlpOut ?? join(directory, "traces.otlp.jsonl"),
1063
+ signal: controller.signal,
1064
+ log: analystLog
1065
+ });
1066
+ } finally {
1067
+ process.removeListener("SIGINT", interrupt);
1068
+ }
1069
+ const artifacts = await writeTraceQuestionsArtifacts(result, directory);
1070
+ process.stdout.write(result.report);
1071
+ for (const warning of result.warnings) process.stderr.write(`warning: ${warning}
1072
+ `);
1073
+ process.stderr.write(
1074
+ `ask artifacts \u2192 ${artifacts.directory} (${result.totals.answered}/${result.totals.questions} answered, wall ${(result.totals.wallTimeMs / 1e3).toFixed(1)} s, answers: ${artifacts.result})
1075
+ `
1076
+ );
1077
+ if (!result.ok) {
1078
+ const failed = result.questions.filter((answer) => answer.status === "failed");
1079
+ const lines = [
1080
+ `${failed.length} of ${result.questions.length} question(s) failed; the answers file holds every result.`,
1081
+ ...failed.map((answer) => ` ${answer.id}: ${answer.failure?.kind}: ${answer.failure?.message.slice(0, 300)}`)
1082
+ ];
1083
+ if (failed.some((answer) => isBridgeMismatchError(answer.failure?.message ?? ""))) {
1084
+ lines.push(
1085
+ `hint: the DSPy bridge protocol is version-locked \u2014 the TRACES_PYTHON interpreter needs agent-eval-rpc[dspy]==${requiredBridgeVersion()} (matching this package's @tangle-network/agent-eval).`
1086
+ );
1087
+ }
1088
+ throw new Error(lines.join("\n"));
1089
+ }
1068
1090
  }
1069
1091
  function summarizeFindingEvidence(finding) {
1070
1092
  const first = finding.evidence[0];
@@ -1387,6 +1409,17 @@ Commands:
1387
1409
  (--supervisor-run-dir <dir> reports a supervision tree instead)
1388
1410
  investigate Run typed investigation flow, including BYO config + evidence-backed actions
1389
1411
  improve Write findings, evidence, report, and canonical trace artifacts
1412
+ ask Answer free-form questions over the selected sessions with the
1413
+ model-backed engine: questions run concurrently under one budget,
1414
+ every trace:// citation is checked, and answers.json + report.md
1415
+ are written to --dir (exit 1 when any question fails)
1416
+ facts Print the deterministic session-facts sheet for the selected
1417
+ sessions: tool calls excluding synthesized spans, subagent spawns
1418
+ with task names, human turns in order, the final message per task,
1419
+ changed paths, first/last record times, and the harness token total.
1420
+ No model call, no budget, $0. Every fact names the span ids it came
1421
+ from; a fact the spans cannot support is null with its reason.
1422
+ --format json (default) or text (exit 1 when a session cannot be read)
1390
1423
  convert Emit OTLP-JSONL only, to --otlp-out (HALO: use analyze --analyzer halo)
1391
1424
  index Emit a reusable session index JSON for later investigation
1392
1425
  bundle Assemble one session's durable evidence directory: transcript +
@@ -1435,21 +1468,24 @@ Options:
1435
1468
  analyze: report a SUPERVISION TREE instead of harness sessions \u2014
1436
1469
  steers, spawn waves, concurrency, idle wall, cost by role,
1437
1470
  accepted vs rejected. Rolls up when the dir holds many runs.
1438
- watch: same directory, live. Reads the loops layout, the
1439
- agent-runtime createFileRunContext journal, and OTLP span files.
1471
+ analyze reads Runtime journals and recorded startup failures.
1472
+ watch reads a live Runtime journal and OTLP span files.
1440
1473
  --since <t> upload: window, 30m / 2h / 7d or an ISO date (default 24h); analyze: ISO cutoff
1441
1474
  --out <path> Write report to a file
1442
- --dir <path> improve: write artifacts to this directory
1475
+ --dir <path> improve/ask: write artifacts to this directory
1443
1476
  --otlp <file|dir> READ OTLP-JSONL emitted by any system, skipping the adapters.
1444
1477
  A directory reads the OTLP files under it \u2014 only the otlp/
1445
1478
  subdirectory when the producer made one \u2014 and names the JSONL
1446
1479
  that is not OTLP instead of reading it as broken spans.
1447
- Supported by: validate, analyze, investigate, improve, stream.
1480
+ Supported by: validate, analyze, investigate, improve, ask, stream.
1448
1481
  On a WRITING command it is the deprecated spelling of
1449
1482
  --otlp-out; it still works, with a warning, until 0.12.
1483
+ --source-bundle <dir> Analyze a retained full bundle; explicitly grant source-field reads.
1484
+ Available for analyze, investigate, improve, and ask.
1450
1485
  --otlp-out <path> WRITE the OTLP-JSONL artifact here (also evidence
1451
1486
  provenance / dry-run upload preview)
1452
1487
  --format <kind> analyze/export: auto | policy-evidence | sandbox-events | openinference | intelligence-spans | chat-trajectory
1488
+ facts: json (default) | text
1453
1489
  --metadata <json> analyze/export file: attach JSON object fields as span attributes
1454
1490
  --attr <k=v> analyze/export file: attach one span attribute (repeatable)
1455
1491
  --mode <kind> stream: visualizer | findings | agent (default visualizer)
@@ -1466,10 +1502,20 @@ Options:
1466
1502
  agent-eval-rpc[dspy] (TRACES_PYTHON selects the interpreter),
1467
1503
  version-matched to this package's @tangle-network/agent-eval.
1468
1504
  Exits 1 when every agentic analyst fails, with each reason.
1505
+ ask uses the same engine and credentials without --llm.
1469
1506
  --model <id> Model for --llm, HALO, and Hodoscope (default for --llm: ${DEFAULT_ANALYST_MODEL})
1470
1507
  --config <path> investigate/improve/stream: JS config with analysts, liveAnalysts, or external analyzers
1471
- --budget <usd> USD cap for agentic analysts
1472
- --analyzer <id> analyze: also run halo, hodoscope, prime, or an installed command (repeatable)
1508
+ --budget <usd> USD cap for agentic analysts; ask: one ceiling shared by every question
1509
+ --question <text> ask: a question (repeatable, at most ${MAX_TRACE_QUESTION_CHARS} characters)
1510
+ --questions <file> ask: JSON array of questions, strings or
1511
+ { id?, question, instructions?, answerSchema? }
1512
+ --question-budget <usd>
1513
+ ask: provider ceiling for one question (default: the smaller
1514
+ of --budget and $${DEFAULT_QUESTION_MAX_COST_USD})
1515
+ --concurrency <n> ask: questions running at once (default 4);
1516
+ import-codetracebench: trajectories imported at once
1517
+ --analyzer <id> analyze: also run halo, hodoscope, prime, or an installed command (repeatable);
1518
+ exits 1 after writing the report when any requested analyzer fails
1473
1519
  prime posts the full span projection to an OpenAI-compatible bridge
1474
1520
  (TRACES_PRIME_BRIDGE_URL, default http://localhost:4181;
1475
1521
  TRACES_PRIME_MODEL, default prime/zai/glm-5.2; TRACES_PRIME_TIMEOUT_MS)
@@ -1551,6 +1597,12 @@ async function main() {
1551
1597
  case "improve":
1552
1598
  await cmdImprove(args);
1553
1599
  break;
1600
+ case "ask":
1601
+ await cmdAsk(args);
1602
+ break;
1603
+ case "facts":
1604
+ await cmdFacts(args);
1605
+ break;
1554
1606
  case "convert":
1555
1607
  await cmdConvert(args);
1556
1608
  break;