@lsctech/polaris 0.3.29 → 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.
- package/dist/agent-plugin/args.js +77 -0
- package/dist/agent-plugin/claude-generator.js +158 -0
- package/dist/agent-plugin/commands.js +73 -0
- package/dist/agent-plugin/help.js +69 -0
- package/dist/agent-plugin/sync.js +108 -0
- package/dist/autoresearch/dev-gate.js +51 -0
- package/dist/autoresearch/gates.js +310 -0
- package/dist/autoresearch/index.js +20 -0
- package/dist/autoresearch/proposal.js +121 -0
- package/dist/autoresearch/routing.js +180 -0
- package/dist/autoresearch/score.js +213 -0
- package/dist/cli/adopt-approve.js +124 -4
- package/dist/cli/adopt-assets.js +9 -1
- package/dist/cli/adopt-command.js +123 -11
- package/dist/cli/adopt-instructions.js +17 -2
- package/dist/cli/adopt-smartdocs.js +9 -0
- package/dist/cli/adoption-context.js +46 -0
- package/dist/cli/adoption-plan.js +105 -12
- package/dist/cli/agent-setup.js +47 -0
- package/dist/cli/autoresearch.js +96 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/init.js +69 -1
- package/dist/cli/setup-interview/generate.js +380 -0
- package/dist/cli/setup-interview/report.js +138 -0
- package/dist/cli/setup-interview/runner.js +105 -0
- package/dist/cli/setup-interview/schema.js +16 -0
- package/dist/cli/setup-interview/store.js +73 -0
- package/dist/cli/worker.js +18 -8
- package/dist/finalize/index.js +5 -4
- package/dist/loop/adapters/foreman-dispatch.js +65 -0
- package/dist/loop/adapters/index.js +3 -1
- package/dist/loop/bootstrap-packet.js +1 -1
- package/dist/loop/checkpoint.js +120 -1
- package/dist/loop/continue.js +13 -5
- package/dist/loop/parent.js +129 -1
- package/dist/loop/resume.js +14 -8
- package/dist/loop/worker-packet.js +1 -1
- package/dist/skill-packet/generator.js +149 -1
- package/dist/workspace/.polaris/skills/polaris-tools/tools.js +31 -22
- package/package.json +2 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadInterview = loadInterview;
|
|
4
|
+
exports.saveInterview = saveInterview;
|
|
5
|
+
exports.loadOrCreate = loadOrCreate;
|
|
6
|
+
exports.applyAnswers = applyAnswers;
|
|
7
|
+
exports.markApproved = markApproved;
|
|
8
|
+
exports.nextUnansweredQuestion = nextUnansweredQuestion;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const schema_js_1 = require("./schema.js");
|
|
12
|
+
function interviewPath(repoRoot) {
|
|
13
|
+
return (0, node_path_1.join)(repoRoot, ".polaris", "setup", "interview.json");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load an existing interview record from disk.
|
|
17
|
+
* Returns null if the file does not exist or cannot be parsed.
|
|
18
|
+
*/
|
|
19
|
+
function loadInterview(repoRoot) {
|
|
20
|
+
const path = interviewPath(repoRoot);
|
|
21
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Persist an interview record to .polaris/setup/interview.json. */
|
|
31
|
+
function saveInterview(repoRoot, record) {
|
|
32
|
+
const path = interviewPath(repoRoot);
|
|
33
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
34
|
+
(0, node_fs_1.writeFileSync)(path, `${JSON.stringify(record, null, 2)}\n`, "utf-8");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load an existing record or create a fresh one.
|
|
38
|
+
* Supports --resume: caller passes the return value to continueInterview.
|
|
39
|
+
*/
|
|
40
|
+
function loadOrCreate(repoRoot, now = new Date()) {
|
|
41
|
+
return loadInterview(repoRoot) ?? (0, schema_js_1.createInterviewRecord)(now);
|
|
42
|
+
}
|
|
43
|
+
/** Merge new answers into the record and transition status to "answered" if appropriate. */
|
|
44
|
+
function applyAnswers(record, answers) {
|
|
45
|
+
return {
|
|
46
|
+
...record,
|
|
47
|
+
answers: { ...record.answers, ...answers },
|
|
48
|
+
status: record.status === "approved" ? "approved" : "answered",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Transition record to "approved" status, recording the approval timestamp. */
|
|
52
|
+
function markApproved(record, now = new Date()) {
|
|
53
|
+
return {
|
|
54
|
+
...record,
|
|
55
|
+
status: "approved",
|
|
56
|
+
approved_at: now.toISOString(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return the next unanswered required question key, or null if all are answered.
|
|
61
|
+
* Used to resume at the right question after a partial run.
|
|
62
|
+
*/
|
|
63
|
+
function nextUnansweredQuestion(record) {
|
|
64
|
+
const required = [
|
|
65
|
+
"project_purpose",
|
|
66
|
+
"source_roots",
|
|
67
|
+
"languages",
|
|
68
|
+
"canonical_doc_folders",
|
|
69
|
+
"never_touch",
|
|
70
|
+
"providers_by_role",
|
|
71
|
+
];
|
|
72
|
+
return required.find((key) => record.answers[key] === undefined) ?? null;
|
|
73
|
+
}
|
package/dist/cli/worker.js
CHANGED
|
@@ -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 {
|
package/dist/finalize/index.js
CHANGED
|
@@ -42,7 +42,7 @@ function getBranch(repoRoot) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
function normalizeBranchName(branch) {
|
|
45
|
-
return branch.toLowerCase().replace(/_/g, "-");
|
|
45
|
+
return branch.toLowerCase().replace(/[_/]/g, "-");
|
|
46
46
|
}
|
|
47
47
|
function extractClusterSlug(clusterId) {
|
|
48
48
|
const match = clusterId.match(/([A-Z]+-\d+)/);
|
|
@@ -161,13 +161,14 @@ function checkLibrarianGate(repoRoot, clusterId) {
|
|
|
161
161
|
const filesCommitted = result.files_committed ?? [];
|
|
162
162
|
for (const file of filesCommitted) {
|
|
163
163
|
const absFile = (0, node_path_1.resolve)(repoRoot, file);
|
|
164
|
+
// Allowed takes precedence over prohibited — a specific allowed path wins over a broad prohibition.
|
|
165
|
+
const allowedMatch = allowedWritePaths.find((pattern) => (0, git_custody_js_1.patternMatchesPath)(pattern, absFile));
|
|
166
|
+
if (allowedMatch)
|
|
167
|
+
continue;
|
|
164
168
|
const prohibitedMatch = prohibitedWritePaths.find((pattern) => (0, git_custody_js_1.patternMatchesPath)(pattern, absFile));
|
|
165
169
|
if (prohibitedMatch) {
|
|
166
170
|
return `Librarian wrote to prohibited path: ${file} (matched pattern: ${prohibitedMatch})`;
|
|
167
171
|
}
|
|
168
|
-
const allowedMatch = allowedWritePaths.find((pattern) => (0, git_custody_js_1.patternMatchesPath)(pattern, absFile));
|
|
169
|
-
if (allowedMatch)
|
|
170
|
-
continue; // explicitly allowed
|
|
171
172
|
return `Librarian wrote to out-of-scope path: ${file} (not in allowed_write_paths)`;
|
|
172
173
|
}
|
|
173
174
|
try {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Foreman dispatch — launches the assigned Foreman with a setup-bootstrap
|
|
4
|
+
* packet through the configured execution adapter.
|
|
5
|
+
*
|
|
6
|
+
* Provider selection comes from config (`execution.providerPolicy.foreman`);
|
|
7
|
+
* no provider-specific logic lives here — the adapter handles that.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.dispatchForeman = dispatchForeman;
|
|
11
|
+
const terminal_cli_js_1 = require("./terminal-cli.js");
|
|
12
|
+
/**
|
|
13
|
+
* Validate that the setup-bootstrap packet has checkpoint gate enforcement.
|
|
14
|
+
*
|
|
15
|
+
* This is the "by construction" guard: a packet without a valid checkpoint_gate
|
|
16
|
+
* (or with self_approval_prohibited !== true) cannot be dispatched. Because
|
|
17
|
+
* `generateSetupBootstrapPacket` always sets self_approval_prohibited to true,
|
|
18
|
+
* any packet that fails this check was manually constructed — and is rejected.
|
|
19
|
+
*
|
|
20
|
+
* @throws {Error} if checkpoint_gate is absent or self_approval_prohibited is not true.
|
|
21
|
+
*/
|
|
22
|
+
function assertCheckpointGateEnforced(packet) {
|
|
23
|
+
if (!packet.checkpoint_gate || packet.checkpoint_gate.self_approval_prohibited !== true) {
|
|
24
|
+
throw new Error(`Cannot dispatch setup-bootstrap packet "${packet.packet_id}": ` +
|
|
25
|
+
`checkpoint_gate.self_approval_prohibited must be true. ` +
|
|
26
|
+
`Use generateSetupBootstrapPacket() to produce a valid packet.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wraps a SetupBootstrapPacket into a BootstrapPacket-compatible shape
|
|
31
|
+
* that existing adapters can dispatch. The setup-bootstrap fields are
|
|
32
|
+
* carried as context so the foreman receives the full packet.
|
|
33
|
+
*/
|
|
34
|
+
function toBootstrapPacket(packet) {
|
|
35
|
+
return {
|
|
36
|
+
schema_version: "1.0",
|
|
37
|
+
run_id: `setup-${packet.mode}-${packet.packet_id}`,
|
|
38
|
+
cluster_id: `setup-${packet.mode}`,
|
|
39
|
+
active_child: "",
|
|
40
|
+
state_file: "",
|
|
41
|
+
telemetry_file: "",
|
|
42
|
+
context: {
|
|
43
|
+
setup_bootstrap: packet,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Dispatch the Foreman with the setup-bootstrap packet.
|
|
49
|
+
*
|
|
50
|
+
* Enforces checkpoint gate presence before dispatch — a packet without
|
|
51
|
+
* self_approval_prohibited: true is rejected, making self-approval
|
|
52
|
+
* impossible by construction.
|
|
53
|
+
*
|
|
54
|
+
* Uses the existing TerminalCliAdapter so provider-specific logic stays
|
|
55
|
+
* in provider configs — this function is provider-neutral.
|
|
56
|
+
*/
|
|
57
|
+
async function dispatchForeman(input) {
|
|
58
|
+
assertCheckpointGateEnforced(input.packet);
|
|
59
|
+
const adapter = new terminal_cli_js_1.TerminalCliAdapter(input.executionConfig);
|
|
60
|
+
const bootstrapPacket = toBootstrapPacket(input.packet);
|
|
61
|
+
return adapter.dispatch(bootstrapPacket, {
|
|
62
|
+
provider: input.provider,
|
|
63
|
+
dryRun: input.dryRun,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAdapter = exports.AgentSubtaskAdapter = exports.TerminalCliAdapter = void 0;
|
|
3
|
+
exports.dispatchForeman = exports.createAdapter = exports.AgentSubtaskAdapter = exports.TerminalCliAdapter = void 0;
|
|
4
4
|
var terminal_cli_js_1 = require("./terminal-cli.js");
|
|
5
5
|
Object.defineProperty(exports, "TerminalCliAdapter", { enumerable: true, get: function () { return terminal_cli_js_1.TerminalCliAdapter; } });
|
|
6
6
|
var agent_subtask_js_1 = require("./agent-subtask.js");
|
|
7
7
|
Object.defineProperty(exports, "AgentSubtaskAdapter", { enumerable: true, get: function () { return agent_subtask_js_1.AgentSubtaskAdapter; } });
|
|
8
8
|
var registry_js_1 = require("./registry.js");
|
|
9
9
|
Object.defineProperty(exports, "createAdapter", { enumerable: true, get: function () { return registry_js_1.createAdapter; } });
|
|
10
|
+
var foreman_dispatch_js_1 = require("./foreman-dispatch.js");
|
|
11
|
+
Object.defineProperty(exports, "dispatchForeman", { enumerable: true, get: function () { return foreman_dispatch_js_1.dispatchForeman; } });
|
|
@@ -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,
|
package/dist/loop/checkpoint.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/loop/continue.js
CHANGED
|
@@ -482,12 +482,20 @@ function runLoopContinue(options) {
|
|
|
482
482
|
...(state.completed_children_results ?? {}),
|
|
483
483
|
};
|
|
484
484
|
if (completedChild && completionResultFile) {
|
|
485
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
next_recommended_action: "continue",
|
|
490
|
-
};
|
|
497
|
+
nextRecommendedAction: "continue",
|
|
498
|
+
});
|
|
491
499
|
}
|
|
492
500
|
const updatedState = {
|
|
493
501
|
...state,
|
package/dist/loop/parent.js
CHANGED
|
@@ -110,6 +110,26 @@ function resolveTelemetryFile(state, repoRoot) {
|
|
|
110
110
|
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
111
111
|
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
112
112
|
}
|
|
113
|
+
function estimateTokensFromBytes(bytes) {
|
|
114
|
+
return Math.round(bytes / 4);
|
|
115
|
+
}
|
|
116
|
+
function emitBootstrapContextSize(telemetryFile, runId, childId, stateFile, packet) {
|
|
117
|
+
const stateFileBytes = (0, node_fs_1.statSync)(stateFile).size;
|
|
118
|
+
const bootstrapPacketBytes = Buffer.byteLength(JSON.stringify(packet), "utf-8");
|
|
119
|
+
const stateEstimatedTokens = estimateTokensFromBytes(stateFileBytes);
|
|
120
|
+
const bootstrapEstimatedTokens = estimateTokensFromBytes(bootstrapPacketBytes);
|
|
121
|
+
appendTelemetry(telemetryFile, {
|
|
122
|
+
event: "bootstrap-context-size",
|
|
123
|
+
run_id: runId,
|
|
124
|
+
child_id: childId,
|
|
125
|
+
state_file_bytes: stateFileBytes,
|
|
126
|
+
state_estimated_tokens: stateEstimatedTokens,
|
|
127
|
+
bootstrap_packet_bytes: bootstrapPacketBytes,
|
|
128
|
+
bootstrap_estimated_tokens: bootstrapEstimatedTokens,
|
|
129
|
+
combined_estimated_tokens: stateEstimatedTokens + bootstrapEstimatedTokens,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
113
133
|
/**
|
|
114
134
|
* Select the next open child that is not Done/blocked.
|
|
115
135
|
* Returns null when all children are completed.
|
|
@@ -300,6 +320,74 @@ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, res
|
|
|
300
320
|
function absoluteResultFile(repoRoot, filePath) {
|
|
301
321
|
return (0, node_path_1.isAbsolute)(filePath) ? filePath : (0, node_path_1.resolve)(repoRoot, filePath);
|
|
302
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Flush bodies from open_children_meta to the durable clusters.json snapshot.
|
|
325
|
+
* Called once before the dispatch loop so that writeStateAtomic (which strips
|
|
326
|
+
* body from non-next children) does not cause data loss when the loop reloads
|
|
327
|
+
* state from disk between dispatches.
|
|
328
|
+
*
|
|
329
|
+
* Only writes nodes that have a body in open_children_meta but lack one in the
|
|
330
|
+
* existing snapshot. Never removes or overwrites existing body data.
|
|
331
|
+
* Never throws.
|
|
332
|
+
*/
|
|
333
|
+
function flushBodiesToClusterSnapshot(state, repoRoot) {
|
|
334
|
+
const meta = state.open_children_meta;
|
|
335
|
+
if (!meta)
|
|
336
|
+
return;
|
|
337
|
+
const snapshotPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "clusters.json");
|
|
338
|
+
let snapshot = {};
|
|
339
|
+
try {
|
|
340
|
+
const raw = (0, node_fs_1.readFileSync)(snapshotPath, "utf-8");
|
|
341
|
+
snapshot = JSON.parse(raw);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
// Distinguish "file not found" from "present but corrupted"
|
|
345
|
+
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
|
|
346
|
+
// File absent — start fresh
|
|
347
|
+
snapshot = {
|
|
348
|
+
schemaVersion: "v2",
|
|
349
|
+
source: { id: state.cluster_id, type: "local" },
|
|
350
|
+
nodes: {},
|
|
351
|
+
dependencies: {},
|
|
352
|
+
clusters: { [state.cluster_id]: { id: state.cluster_id, title: state.cluster_id, children: state.open_children } },
|
|
353
|
+
activeCluster: state.cluster_id,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// File present but corrupted — do not overwrite
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (typeof snapshot.nodes !== "object" || snapshot.nodes === null) {
|
|
362
|
+
snapshot.nodes = {};
|
|
363
|
+
}
|
|
364
|
+
let changed = false;
|
|
365
|
+
for (const [id, childMeta] of Object.entries(meta)) {
|
|
366
|
+
const body = childMeta?.body;
|
|
367
|
+
if (!body || body.trim().length === 0)
|
|
368
|
+
continue;
|
|
369
|
+
const existing = snapshot.nodes[id];
|
|
370
|
+
if (existing && existing.body && existing.body.trim().length > 0)
|
|
371
|
+
continue;
|
|
372
|
+
snapshot.nodes[id] = {
|
|
373
|
+
...(existing ?? {}),
|
|
374
|
+
id,
|
|
375
|
+
title: childMeta.title ?? existing?.title ?? id,
|
|
376
|
+
body,
|
|
377
|
+
status: existing?.status ?? "Todo",
|
|
378
|
+
};
|
|
379
|
+
changed = true;
|
|
380
|
+
}
|
|
381
|
+
if (!changed)
|
|
382
|
+
return;
|
|
383
|
+
try {
|
|
384
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(snapshotPath), { recursive: true });
|
|
385
|
+
(0, node_fs_1.writeFileSync)(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// Best-effort — do not fail the loop if snapshot write fails
|
|
389
|
+
}
|
|
390
|
+
}
|
|
303
391
|
function buildClusterArtifactPaths(repoRoot, clusterId, childId, dispatchId) {
|
|
304
392
|
const clusterDir = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId);
|
|
305
393
|
const filename = `${childId}-${dispatchId}.json`;
|
|
@@ -629,6 +717,14 @@ async function runParentLoop(options) {
|
|
|
629
717
|
}
|
|
630
718
|
}
|
|
631
719
|
let childrenDispatched = 0;
|
|
720
|
+
// ── Flush bodies to clusters.json before stripping begins ──────────────
|
|
721
|
+
// writeStateAtomic strips body from all open_children_meta entries except
|
|
722
|
+
// open_children[0]. Flush any bodies present in open_children_meta to the
|
|
723
|
+
// durable cluster snapshot so readBodyFromClusterSnapshot can hydrate them
|
|
724
|
+
// on subsequent iterations after reload from disk.
|
|
725
|
+
if (!dryRun) {
|
|
726
|
+
flushBodiesToClusterSnapshot(state, repoRoot);
|
|
727
|
+
}
|
|
632
728
|
// ── Lifecycle manager: enforce one-active-worker policy ─────────────────
|
|
633
729
|
// forceReleaseAll() clears any orphaned registrations from a previous
|
|
634
730
|
// crashed session. Registrations are session-memory only; they are not
|
|
@@ -986,6 +1082,7 @@ async function runParentLoop(options) {
|
|
|
986
1082
|
dry_run: dryRun,
|
|
987
1083
|
timestamp: new Date().toISOString(),
|
|
988
1084
|
});
|
|
1085
|
+
emitBootstrapContextSize(telemetryFile, state.run_id, nextChild, stateFile, packet);
|
|
989
1086
|
}
|
|
990
1087
|
let dispatchResult;
|
|
991
1088
|
try {
|
|
@@ -1284,6 +1381,10 @@ async function runParentLoop(options) {
|
|
|
1284
1381
|
else {
|
|
1285
1382
|
state = reloadedState;
|
|
1286
1383
|
reloadedStateValid = true;
|
|
1384
|
+
// Flush bodies from worker-updated state to clusters.json before any stripping
|
|
1385
|
+
if (!dryRun) {
|
|
1386
|
+
flushBodiesToClusterSnapshot(state, repoRoot);
|
|
1387
|
+
}
|
|
1287
1388
|
}
|
|
1288
1389
|
}
|
|
1289
1390
|
catch (err) {
|
|
@@ -1410,9 +1511,28 @@ async function runParentLoop(options) {
|
|
|
1410
1511
|
}
|
|
1411
1512
|
}
|
|
1412
1513
|
// workerStatus === "done" has already been validated upstream.
|
|
1514
|
+
// Build the durable Role Evidence Contract for this completed child.
|
|
1515
|
+
const nextRecommendedAction = summaryAsRecord?.['next_recommended_action'] === 'continue' ||
|
|
1516
|
+
summaryAsRecord?.['next_recommended_action'] === 'stop' ||
|
|
1517
|
+
summaryAsRecord?.['next_recommended_action'] === 'investigate'
|
|
1518
|
+
? summaryAsRecord['next_recommended_action']
|
|
1519
|
+
: 'continue';
|
|
1520
|
+
const workerResult = (0, checkpoint_js_1.buildWorkerResultContract)({
|
|
1521
|
+
state,
|
|
1522
|
+
childId: nextChild,
|
|
1523
|
+
resultFile: packet.result_file_contract.result_file,
|
|
1524
|
+
telemetryFile,
|
|
1525
|
+
lastCommit: lastCommit ?? null,
|
|
1526
|
+
validation: validationSummary,
|
|
1527
|
+
packetHash: (0, checkpoint_js_1.computePacketHashFromPath)(packetPath),
|
|
1528
|
+
status: 'done',
|
|
1529
|
+
nextRecommendedAction,
|
|
1530
|
+
resultData: summaryAsRecord?.['result_data'],
|
|
1531
|
+
});
|
|
1413
1532
|
// If the reloaded state already reflects the completed child,
|
|
1414
1533
|
// the worker owns the completion checkpoint and the parent
|
|
1415
|
-
// must not rewrite it
|
|
1534
|
+
// must not rewrite the open/closed lists, but it still records the
|
|
1535
|
+
// Role Evidence Contract so scoring can consume it later.
|
|
1416
1536
|
if (!workerWroteCompletion) {
|
|
1417
1537
|
// ── Dispatch boundary: verify dispatch happened before advancing ──────
|
|
1418
1538
|
// The parent dispatched via adapter, so dispatch_boundary should show
|
|
@@ -1444,6 +1564,10 @@ async function runParentLoop(options) {
|
|
|
1444
1564
|
state = {
|
|
1445
1565
|
...advanced,
|
|
1446
1566
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
|
|
1567
|
+
completed_children_results: {
|
|
1568
|
+
...advanced.completed_children_results,
|
|
1569
|
+
[nextChild]: workerResult,
|
|
1570
|
+
},
|
|
1447
1571
|
};
|
|
1448
1572
|
childrenDispatched += 1;
|
|
1449
1573
|
// Worker did not write its own completion — orchestrator fills the gap.
|
|
@@ -1457,6 +1581,10 @@ async function runParentLoop(options) {
|
|
|
1457
1581
|
state = {
|
|
1458
1582
|
...state,
|
|
1459
1583
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
|
|
1584
|
+
completed_children_results: {
|
|
1585
|
+
...state.completed_children_results,
|
|
1586
|
+
[nextChild]: workerResult,
|
|
1587
|
+
},
|
|
1460
1588
|
};
|
|
1461
1589
|
childrenDispatched += 1;
|
|
1462
1590
|
if (!dryRun) {
|
package/dist/loop/resume.js
CHANGED
|
@@ -61,7 +61,7 @@ const OPEN_CHILD_STATUSES = new Set([
|
|
|
61
61
|
]);
|
|
62
62
|
function findClusterStateForPacket(repoRoot, packet) {
|
|
63
63
|
const clustersDir = (0, node_path_1.resolve)(repoRoot, ".polaris", "clusters");
|
|
64
|
-
const hints = new Set([packet.last_completed_child,
|
|
64
|
+
const hints = new Set([packet.last_completed_child, packet.open_children.next_child].filter((value) => typeof value === "string" && value.length > 0));
|
|
65
65
|
let entries;
|
|
66
66
|
try {
|
|
67
67
|
entries = (0, node_fs_1.readdirSync)(clustersDir, { withFileTypes: true })
|
|
@@ -96,12 +96,16 @@ function rebuildLoopStateFromClusterState(packet, clusterState, repoRoot) {
|
|
|
96
96
|
const completedChildren = clusterState.child_states
|
|
97
97
|
.filter((child) => COMPLETED_CHILD_STATUSES.has(child.status))
|
|
98
98
|
.map((child) => child.id);
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
// Reconstruct full open queue from all open child_states
|
|
100
|
+
const allOpenChildren = clusterState.child_states
|
|
101
|
+
.filter((child) => OPEN_CHILD_STATUSES.has(child.status))
|
|
102
|
+
.map((child) => child.id);
|
|
103
|
+
// Place next_child first if present, then append remaining open children
|
|
104
|
+
const nextChild = packet.open_children.next_child;
|
|
105
|
+
const openChildren = nextChild && clusterChildren.has(nextChild)
|
|
106
|
+
? [nextChild, ...allOpenChildren.filter((id) => id !== nextChild)]
|
|
107
|
+
: allOpenChildren;
|
|
108
|
+
const fallbackOpenChildren = openChildren;
|
|
105
109
|
const activeChild = fallbackOpenChildren.find((childId) => {
|
|
106
110
|
const status = clusterState.child_states.find((child) => child.id === childId)?.status;
|
|
107
111
|
return status === "claimed" || status === "dispatched" || status === "running";
|
|
@@ -177,7 +181,9 @@ function appendResumedLedgerEvent(repoRoot, packet, state) {
|
|
|
177
181
|
: [];
|
|
178
182
|
const openChildren = Array.isArray(state.open_children)
|
|
179
183
|
? state.open_children
|
|
180
|
-
: packet.open_children
|
|
184
|
+
: packet.open_children.next_child
|
|
185
|
+
? [packet.open_children.next_child]
|
|
186
|
+
: [];
|
|
181
187
|
new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH)).append({
|
|
182
188
|
schema_version: 1,
|
|
183
189
|
event_id: (0, node_crypto_1.randomUUID)(),
|
|
@@ -250,7 +250,7 @@ function compileImplPacket(input) {
|
|
|
250
250
|
`Create exactly ONE git commit: [${input.childId}] ${childTitle}.`,
|
|
251
251
|
`Do NOT modify open_children or completed_children in ${input.stateFile}. The parent runtime (loop continue) owns that state transition.`,
|
|
252
252
|
`Append a telemetry event to ${input.telemetryFile}.`,
|
|
253
|
-
`Write compact return JSON to stdout (fields: ${exports.IMPL_RETURN_CONTRACT.join(', ')}).`,
|
|
253
|
+
`Write compact return JSON to stdout (fields: ${exports.IMPL_RETURN_CONTRACT.join(', ')}). next_recommended_action MUST be exactly "continue" on success, "stop" on unresolvable blocker, or "investigate" if manual review is needed.`,
|
|
254
254
|
`TERMINATE SESSION IMMEDIATELY. Do not select or execute the next child.`,
|
|
255
255
|
];
|
|
256
256
|
// Build compact or full prompt; primary_goal uses the structured template.
|