@lsctech/polaris 0.4.0 → 0.4.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.
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Autoresearch scoring engine.
4
+ *
5
+ * Reads run artifacts, evaluates binary gates, and produces a structured
6
+ * diagnosis report:
7
+ *
8
+ * {
9
+ * run_id, cluster_id, evaluated_at,
10
+ * gate_results: GateResult[],
11
+ * failed_gates: string[],
12
+ * score: number (0.0–1.0),
13
+ * diagnosis_hints: { gate, fix_zone, hint }[]
14
+ * }
15
+ *
16
+ * Score = passed_gates / evaluable_gates (skipped gates are not counted).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.loadRunArtifacts = loadRunArtifacts;
20
+ exports.computeScore = computeScore;
21
+ exports.buildDiagnosisHints = buildDiagnosisHints;
22
+ exports.scoreRun = scoreRun;
23
+ const node_fs_1 = require("node:fs");
24
+ const node_path_1 = require("node:path");
25
+ const gates_js_1 = require("./gates.js");
26
+ // ──────────────────────────────────────────────
27
+ // Diagnosis hints table
28
+ // ──────────────────────────────────────────────
29
+ const HINTS = {
30
+ "user-intervened": {
31
+ fix_zone: "worker-prompt / packet-scope",
32
+ hint: "User pushed commits after polaris-finalize. Review the worker packet scope and instructions for ambiguity.",
33
+ },
34
+ "foreman-resent-packet": {
35
+ fix_zone: "foreman-dispatch / dispatch-boundary",
36
+ hint: "Foreman dispatched the same child more than once. Check for state-machine bugs in dispatch-boundary.ts or loop continue logic.",
37
+ },
38
+ "foreman-fixed-worker-output": {
39
+ fix_zone: "worker-prompt / scope-contract",
40
+ hint: "Foreman made corrective commits after child-complete. Tighten the worker packet's allowed_scope and acceptance criteria.",
41
+ },
42
+ "worker-output-required-fixing": {
43
+ fix_zone: "worker-prompt / validation-commands",
44
+ hint: "Post-finalize commits indicate worker output was insufficient. Add stricter validation commands to the worker packet.",
45
+ },
46
+ "validation-failed": {
47
+ fix_zone: "worker-validation / build-setup",
48
+ hint: "One or more children failed validation. Check the validation_commands in the worker packet and the worker's implementation.",
49
+ },
50
+ "worker-went-out-of-scope": {
51
+ fix_zone: "worker-packet / scope-contract",
52
+ hint: "Worker emitted out-of-scope blocks or returned blocked status. Narrow allowed_scope in the worker packet.",
53
+ },
54
+ "foreman-token-burn-over-budget": {
55
+ fix_zone: "bootstrap-context / prompt-size",
56
+ hint: "Foreman bootstrap context exceeds token budget. Reduce current-state.json or worker packet size before dispatch.",
57
+ },
58
+ "state-repair-required": {
59
+ fix_zone: "medic / cluster-state",
60
+ hint: "Medic artifacts detected — state repair was required during this run. Review the medic chart and root cause.",
61
+ },
62
+ };
63
+ // ──────────────────────────────────────────────
64
+ // Artifact loader
65
+ // ──────────────────────────────────────────────
66
+ function findRunDir(repoRoot, runId) {
67
+ // Standard taskchain location
68
+ const taskchainPath = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "runs", runId);
69
+ if ((0, node_fs_1.existsSync)(taskchainPath))
70
+ return taskchainPath;
71
+ // Legacy .polaris/runs location
72
+ const polarisPath = (0, node_path_1.join)(repoRoot, ".polaris", "runs", runId);
73
+ if ((0, node_fs_1.existsSync)(polarisPath))
74
+ return polarisPath;
75
+ return null;
76
+ }
77
+ function findClusterDir(repoRoot, clusterId) {
78
+ const path = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId);
79
+ return (0, node_fs_1.existsSync)(path) ? path : null;
80
+ }
81
+ function readJson(filePath) {
82
+ try {
83
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ function safeReaddir(dir) {
90
+ try {
91
+ return (0, node_fs_1.readdirSync)(dir);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ function readResultPackets(clusterDir) {
98
+ if (!clusterDir)
99
+ return [];
100
+ const resultsDir = (0, node_path_1.join)(clusterDir, "results");
101
+ if (!(0, node_fs_1.existsSync)(resultsDir))
102
+ return [];
103
+ return safeReaddir(resultsDir)
104
+ .filter((f) => f.endsWith(".json"))
105
+ .map((f) => readJson((0, node_path_1.join)(resultsDir, f)))
106
+ .filter((v) => v !== undefined);
107
+ }
108
+ function extractWorkerResultContracts(resultPackets) {
109
+ const contracts = [];
110
+ for (const packet of resultPackets) {
111
+ const rec = packet;
112
+ // WorkerResultContract has packet_hash, worker_id, role as distinguishing fields
113
+ if (rec && typeof rec["packet_hash"] === "string" && typeof rec["worker_id"] === "string") {
114
+ contracts.push(rec);
115
+ }
116
+ }
117
+ return contracts;
118
+ }
119
+ function loadRunArtifacts(repoRoot, runId) {
120
+ const runDir = findRunDir(repoRoot, runId);
121
+ // Derive cluster from current-state.json in the run dir, or fallback to the default
122
+ let clusterId = null;
123
+ let currentState = null;
124
+ // Try run dir's current-state.json first
125
+ if (runDir) {
126
+ const runStatePath = (0, node_path_1.join)(runDir, "current-state.json");
127
+ if ((0, node_fs_1.existsSync)(runStatePath)) {
128
+ currentState = readJson(runStatePath);
129
+ }
130
+ }
131
+ // Fall back to the active current-state.json
132
+ if (!currentState) {
133
+ const taskchainState = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json");
134
+ const polarisState = (0, node_path_1.join)(repoRoot, ".polaris", "runs", "current-state.json");
135
+ const statePath = (0, node_fs_1.existsSync)(taskchainState) ? taskchainState : polarisState;
136
+ if ((0, node_fs_1.existsSync)(statePath)) {
137
+ currentState = readJson(statePath);
138
+ }
139
+ }
140
+ if (currentState && typeof currentState === "object" && currentState !== null) {
141
+ const rec = currentState;
142
+ if (typeof rec["cluster_id"] === "string")
143
+ clusterId = rec["cluster_id"];
144
+ if (typeof rec["run_id"] === "string" && rec["run_id"] !== runId) {
145
+ // State belongs to a different run — don't use it for cluster_id derivation
146
+ clusterId = null;
147
+ }
148
+ }
149
+ const clusterDir = clusterId ? findClusterDir(repoRoot, clusterId) : null;
150
+ // Ledger
151
+ const ledgerPath = (0, node_path_1.join)(repoRoot, ".polaris", "runs", "ledger.jsonl");
152
+ const ledgerEvents = (0, gates_js_1.readJsonLines)(ledgerPath).filter((e) => {
153
+ const rec = e;
154
+ return rec?.["run_id"] === runId;
155
+ });
156
+ // Telemetry — prefer the run dir; some older runs have it in .polaris/runs/<runId>/
157
+ const telemetryPath = runDir ? (0, node_path_1.join)(runDir, "telemetry.jsonl") : null;
158
+ const telemetryEvents = telemetryPath ? (0, gates_js_1.readJsonLines)(telemetryPath) : [];
159
+ // Result packets from cluster results dir
160
+ const resultPackets = readResultPackets(clusterDir);
161
+ // Also check for WorkerResultContracts inside result packets
162
+ const workerResultContracts = extractWorkerResultContracts(resultPackets);
163
+ return {
164
+ runId,
165
+ runDir,
166
+ clusterDir,
167
+ currentState,
168
+ ledgerEvents,
169
+ resultPackets,
170
+ workerResultContracts,
171
+ telemetryEvents,
172
+ };
173
+ }
174
+ // ──────────────────────────────────────────────
175
+ // Scoring
176
+ // ──────────────────────────────────────────────
177
+ function computeScore(gateResults) {
178
+ const evaluable = gateResults.filter((g) => g.outcome !== "skipped");
179
+ if (evaluable.length === 0)
180
+ return 1.0; // nothing to evaluate — no known problems
181
+ const passed = evaluable.filter((g) => g.outcome === "passed").length;
182
+ return passed / evaluable.length;
183
+ }
184
+ function buildDiagnosisHints(failedGateNames) {
185
+ return failedGateNames.map((gate) => {
186
+ const h = HINTS[gate] ?? { fix_zone: "unknown", hint: `Gate ${gate} failed — no hint available.` };
187
+ return { gate, ...h };
188
+ });
189
+ }
190
+ // ──────────────────────────────────────────────
191
+ // Main entry point
192
+ // ──────────────────────────────────────────────
193
+ function scoreRun(repoRoot, runId) {
194
+ const artifacts = loadRunArtifacts(repoRoot, runId);
195
+ const gateResults = gates_js_1.ALL_GATES.map((evaluator) => evaluator(artifacts));
196
+ const failedGates = gateResults.filter((g) => g.outcome === "failed").map((g) => g.gate);
197
+ const score = computeScore(gateResults);
198
+ const diagnosisHints = buildDiagnosisHints(failedGates);
199
+ const clusterId = artifacts.currentState &&
200
+ typeof artifacts.currentState === "object" &&
201
+ !Array.isArray(artifacts.currentState)
202
+ ? artifacts.currentState["cluster_id"] ?? null
203
+ : null;
204
+ return {
205
+ run_id: runId,
206
+ cluster_id: clusterId,
207
+ evaluated_at: new Date().toISOString(),
208
+ gate_results: gateResults,
209
+ failed_gates: failedGates,
210
+ score,
211
+ diagnosis_hints: diagnosisHints,
212
+ };
213
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAutoresearchCommand = createAutoresearchCommand;
4
+ const node_path_1 = require("node:path");
5
+ const commander_1 = require("commander");
6
+ const dev_gate_js_1 = require("../autoresearch/dev-gate.js");
7
+ const score_js_1 = require("../autoresearch/score.js");
8
+ const proposal_js_1 = require("../autoresearch/proposal.js");
9
+ const routing_js_1 = require("../autoresearch/routing.js");
10
+ function createAutoresearchCommand(options) {
11
+ const repoRoot = options.repoRoot;
12
+ const autoresearch = new commander_1.Command("autoresearch")
13
+ .description("Autoresearch tools (dev-gated — Polaris development context only)")
14
+ .showHelpAfterError()
15
+ .showSuggestionAfterError();
16
+ autoresearch
17
+ .command("score <run-id>")
18
+ .description("Score a completed Polaris run against the binary gate scorecard and output a diagnosis report")
19
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
20
+ .option("--json", "Output raw JSON (default: pretty-printed)")
21
+ .action((runId, cmdOptions) => {
22
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
23
+ try {
24
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
25
+ }
26
+ catch (err) {
27
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
28
+ process.exit(1);
29
+ }
30
+ try {
31
+ const report = (0, score_js_1.scoreRun)(root, runId);
32
+ const output = cmdOptions.json
33
+ ? JSON.stringify(report)
34
+ : JSON.stringify(report, null, 2);
35
+ process.stdout.write(`${output}\n`);
36
+ }
37
+ catch (err) {
38
+ process.stderr.write(`autoresearch score error: ${err instanceof Error ? err.message : String(err)}\n`);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ autoresearch
43
+ .command("propose <diagnosis-file>")
44
+ .description("File Linear improvement proposals from a diagnosis report (dev-gated — never auto-applied)")
45
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
46
+ .option("--team <team>", "Linear team name or ID to file issues in", "Polaris")
47
+ .option("--dry-run", "Log what would be created without calling Linear")
48
+ .option("--json", "Output raw JSON (default: pretty-printed)")
49
+ .action(async (diagnosisFile, cmdOptions) => {
50
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
51
+ try {
52
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
53
+ }
54
+ catch (err) {
55
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
56
+ process.exit(1);
57
+ }
58
+ let report;
59
+ try {
60
+ report = (0, proposal_js_1.loadDiagnosisReport)((0, node_path_1.resolve)(diagnosisFile));
61
+ }
62
+ catch (err) {
63
+ process.stderr.write(`autoresearch propose: invalid diagnosis file: ${err instanceof Error ? err.message : String(err)}\n`);
64
+ process.exit(1);
65
+ }
66
+ const proposals = (0, proposal_js_1.buildProposals)(report);
67
+ if (proposals.length === 0) {
68
+ process.stdout.write("No failed gates with fix zone mappings — nothing to propose.\n");
69
+ process.exit(0);
70
+ }
71
+ const apiKey = process.env["LINEAR_API_KEY"];
72
+ if (!apiKey && !cmdOptions.dryRun) {
73
+ process.stderr.write("autoresearch propose: LINEAR_API_KEY environment variable is required.\n");
74
+ process.exit(1);
75
+ }
76
+ try {
77
+ const result = await (0, routing_js_1.routeProposals)(proposals, {
78
+ apiKey: apiKey ?? "",
79
+ teamKey: cmdOptions.team,
80
+ dryRun: cmdOptions.dryRun,
81
+ });
82
+ const output = cmdOptions.json
83
+ ? JSON.stringify(result)
84
+ : JSON.stringify(result, null, 2);
85
+ process.stdout.write(`${output}\n`);
86
+ if (result.total_errors > 0) {
87
+ process.exit(1);
88
+ }
89
+ }
90
+ catch (err) {
91
+ process.stderr.write(`autoresearch propose error: ${err instanceof Error ? err.message : String(err)}\n`);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ return autoresearch;
96
+ }
package/dist/cli/index.js CHANGED
@@ -28,6 +28,7 @@ const medic_js_1 = require("./medic.js");
28
28
  const welfare_js_1 = require("../map/welfare.js");
29
29
  const adopt_command_js_1 = require("./adopt-command.js");
30
30
  const simplicity_js_1 = require("../loop/simplicity.js");
31
+ const autoresearch_js_1 = require("./autoresearch.js");
31
32
  function resolveStateFile(repoRoot, explicit) {
32
33
  if (explicit)
33
34
  return (0, node_path_1.resolve)(explicit);
@@ -109,6 +110,7 @@ function createPolarisCommand(options = {}) {
109
110
  repoRoot,
110
111
  }));
111
112
  program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
113
+ program.addCommand((0, autoresearch_js_1.createAutoresearchCommand)({ repoRoot }));
112
114
  program
113
115
  .command("simplicity")
114
116
  .description("View or override the simplicity discipline mode for the active run")
@@ -102,17 +102,27 @@ function validateWorkerCompletionResult(repoRoot, packet, resultFile) {
102
102
  },
103
103
  };
104
104
  }
105
- function updateCompletionState(state, childId, commit) {
105
+ function updateCompletionState(state, childId, commit, packet, repoRoot) {
106
106
  const completedChildren = Array.from(new Set([...state.completed_children, childId]));
107
107
  const remainingOpenChildren = state.open_children.filter((child) => child !== childId);
108
+ const dispatchRecord = state.open_children_meta?.[childId]?.dispatch_record;
109
+ const resultFile = packet.result_file_contract.result_file;
110
+ const telemetryFile = resolveRepoPath(repoRoot, packet.telemetry_file);
111
+ const packetPath = dispatchRecord?.packet_path ?? "";
112
+ const workerResult = (0, checkpoint_js_1.buildWorkerResultContract)({
113
+ state,
114
+ childId,
115
+ resultFile,
116
+ telemetryFile,
117
+ lastCommit: commit,
118
+ validation: "passed",
119
+ packetHash: packetPath ? (0, checkpoint_js_1.computePacketHashFromPath)(packetPath) : "",
120
+ status: "done",
121
+ nextRecommendedAction: "continue",
122
+ });
108
123
  const completedChildrenResults = {
109
124
  ...(state.completed_children_results ?? {}),
110
- [childId]: {
111
- status: "done",
112
- validation: "passed",
113
- commit,
114
- next_recommended_action: "continue",
115
- },
125
+ [childId]: workerResult,
116
126
  };
117
127
  return {
118
128
  ...state,
@@ -236,7 +246,7 @@ function createWorkerCommand(options) {
236
246
  return;
237
247
  }
238
248
  const state = (0, checkpoint_js_1.readState)(stateFile);
239
- const updatedState = updateCompletionState(state, packet.active_child, validation.result.commit);
249
+ const updatedState = updateCompletionState(state, packet.active_child, validation.result.commit, packet, options.repoRoot);
240
250
  (0, checkpoint_js_1.writeStateAtomic)(stateFile, updatedState);
241
251
  // Sync cluster-state.json so finalize and the parent loop see a consistent view.
242
252
  try {
@@ -71,7 +71,7 @@ function buildBootstrapPacket(state, stateFile, currentStateSha, repoRoot, compl
71
71
  last_completed_step: state.step_cursor,
72
72
  last_completed_child: completedChild,
73
73
  next_step: nextChild ? "03-execute-child" : "CLUSTER-COMPLETE",
74
- open_children: state.open_children,
74
+ open_children: { next_child: state.open_children[0] ?? null, remaining_count: state.open_children.length },
75
75
  artifact_pointers: {
76
76
  current_state: relStateFile,
77
77
  telemetry: relTelemetryFile,
@@ -9,6 +9,10 @@ exports.appendWorkerScopeFidelityEvent = appendWorkerScopeFidelityEvent;
9
9
  exports.appendBoundaryEvent = appendBoundaryEvent;
10
10
  exports.appendAbortEvent = appendAbortEvent;
11
11
  exports.appendStaleDispatchAbortedEvent = appendStaleDispatchAbortedEvent;
12
+ exports.toValidationStatus = toValidationStatus;
13
+ exports.countTelemetryEvents = countTelemetryEvents;
14
+ exports.computePacketHashFromPath = computePacketHashFromPath;
15
+ exports.buildWorkerResultContract = buildWorkerResultContract;
12
16
  exports.readBodyFromClusterSnapshot = readBodyFromClusterSnapshot;
13
17
  const node_fs_1 = require("node:fs");
14
18
  const node_crypto_1 = require("node:crypto");
@@ -268,7 +272,16 @@ function validateState(state) {
268
272
  return errors;
269
273
  }
270
274
  function writeStateAtomic(stateFile, state) {
271
- const content = JSON.stringify(state, null, 2);
275
+ const nextChild = state.open_children[0];
276
+ const slimmedState = state.open_children_meta
277
+ ? {
278
+ ...state,
279
+ open_children_meta: Object.fromEntries(Object.entries(state.open_children_meta).map(([id, meta]) => id === nextChild
280
+ ? [id, meta]
281
+ : [id, (({ body: _b, ...rest }) => rest)(meta)])),
282
+ }
283
+ : state;
284
+ const content = JSON.stringify(slimmedState, null, 2);
272
285
  const tmp = `${stateFile}.tmp`;
273
286
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(stateFile), { recursive: true });
274
287
  (0, node_fs_1.writeFileSync)(tmp, content, "utf-8");
@@ -305,6 +318,112 @@ function appendStaleDispatchAbortedEvent(telemetryFile, event) {
305
318
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
306
319
  (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(timestampedEvent) + "\n", "utf-8");
307
320
  }
321
+ /**
322
+ * Normalize a raw validation value to one of the canonical result states.
323
+ */
324
+ function toValidationStatus(validation) {
325
+ if (typeof validation === "string") {
326
+ const v = validation.trim().toLowerCase();
327
+ if (v === "passed" || v === "failed" || v === "skipped")
328
+ return v;
329
+ if (["pass", "success", "ok"].includes(v))
330
+ return "passed";
331
+ }
332
+ if (validation === true)
333
+ return "passed";
334
+ if (validation === false)
335
+ return "failed";
336
+ if (typeof validation === "object" && validation !== null && !Array.isArray(validation)) {
337
+ const r = validation;
338
+ if (Array.isArray(r["passed"]) && r["passed"].length > 0)
339
+ return "passed";
340
+ if (Array.isArray(r["failed"]) && r["failed"].length > 0)
341
+ return "failed";
342
+ }
343
+ return "skipped";
344
+ }
345
+ /**
346
+ * Count telemetry events of a specific name for a given child.
347
+ * Returns 0 when the telemetry file is missing or unreadable.
348
+ */
349
+ function countTelemetryEvents(telemetryFile, eventName, childId) {
350
+ if (!(0, node_fs_1.existsSync)(telemetryFile))
351
+ return 0;
352
+ try {
353
+ const content = (0, node_fs_1.readFileSync)(telemetryFile, "utf-8").trim();
354
+ if (!content)
355
+ return 0;
356
+ let count = 0;
357
+ for (const line of content.split("\n")) {
358
+ if (!line.trim())
359
+ continue;
360
+ try {
361
+ const event = JSON.parse(line);
362
+ if (event["event"] === eventName && event["child_id"] === childId) {
363
+ count += 1;
364
+ }
365
+ }
366
+ catch {
367
+ // Skip malformed lines
368
+ }
369
+ }
370
+ return count;
371
+ }
372
+ catch {
373
+ return 0;
374
+ }
375
+ }
376
+ /**
377
+ * Compute the SHA-256 hash of a packet file. Returns an empty string when
378
+ * the file is missing or unreadable so evidence never blocks completion.
379
+ */
380
+ function computePacketHashFromPath(packetPath) {
381
+ try {
382
+ const raw = (0, node_fs_1.readFileSync)(packetPath, "utf-8");
383
+ return (0, node_crypto_1.createHash)("sha256").update(raw, "utf-8").digest("hex");
384
+ }
385
+ catch {
386
+ return "";
387
+ }
388
+ }
389
+ /**
390
+ * Build a WorkerResultContract from the durable dispatch state and telemetry.
391
+ * Missing optional values are filled with safe defaults so the record is always
392
+ * complete enough for retroactive scoring.
393
+ */
394
+ function buildWorkerResultContract(args) {
395
+ const dispatchRecord = args.state.open_children_meta?.[args.childId]?.dispatch_record;
396
+ const role = dispatchRecord?.role ?? "worker";
397
+ const provider = dispatchRecord?.provider ?? "unknown";
398
+ const workerId = dispatchRecord?.worker_id ?? dispatchRecord?.dispatch_id ?? "unknown";
399
+ const packetPath = dispatchRecord?.packet_path ?? "";
400
+ const heartbeatCount = countTelemetryEvents(args.telemetryFile, "worker-heartbeat", args.childId);
401
+ const escalationCount = countTelemetryEvents(args.telemetryFile, "worker-blocked", args.childId);
402
+ return {
403
+ child_id: args.childId,
404
+ status: args.status ?? "done",
405
+ validation: toValidationStatus(args.validation),
406
+ commit: args.lastCommit,
407
+ next_recommended_action: args.nextRecommendedAction ?? "continue",
408
+ run_id: args.state.run_id,
409
+ cluster_id: args.state.cluster_id,
410
+ skill_name: args.state.skill ?? null,
411
+ role,
412
+ provider,
413
+ packet_hash: args.packetHash,
414
+ worker_id: workerId,
415
+ escalation_count: escalationCount,
416
+ heartbeat_count: heartbeatCount,
417
+ result_artifact_path: args.resultFile,
418
+ packet_path: packetPath,
419
+ telemetry_path: args.telemetryFile,
420
+ user_intervened: null,
421
+ foreman_intervened: null,
422
+ dispatch_epoch: args.state.dispatch_boundary?.dispatch_epoch,
423
+ session_pointer: null,
424
+ result_data: args.resultData,
425
+ };
426
+ }
308
427
  /**
309
428
  * Reads the issue body for a node from the durable cluster snapshot at
310
429
  * `.polaris/clusters/<clusterId>/clusters.json`. Returns undefined when the
@@ -482,12 +482,20 @@ function runLoopContinue(options) {
482
482
  ...(state.completed_children_results ?? {}),
483
483
  };
484
484
  if (completedChild && completionResultFile) {
485
- updatedCompletedChildrenResults[completedChild] = {
485
+ const telemetryFile = resolveTelemetryFilePath(state, repoRoot);
486
+ const dispatchRecord = state.open_children_meta?.[completedChild]?.dispatch_record;
487
+ const packetPath = dispatchRecord?.packet_path ?? "";
488
+ updatedCompletedChildrenResults[completedChild] = (0, checkpoint_js_1.buildWorkerResultContract)({
489
+ state,
490
+ childId: completedChild,
491
+ resultFile: completionResultFile,
492
+ telemetryFile,
493
+ lastCommit: completionCommit || null,
494
+ validation: completionValidation,
495
+ packetHash: packetPath ? (0, checkpoint_js_1.computePacketHashFromPath)(packetPath) : "",
486
496
  status: "done",
487
- validation: hasValidationEvidence(completionValidation) ? "passed" : "skipped",
488
- commit: completionCommit || null,
489
- next_recommended_action: "continue",
490
- };
497
+ nextRecommendedAction: "continue",
498
+ });
491
499
  }
492
500
  const updatedState = {
493
501
  ...state,