@the-open-engine/zeroshot 6.39.1 → 6.40.0

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 (121) hide show
  1. package/README.md +45 -1
  2. package/cli/agent-provider-boundary.js +19 -0
  3. package/cli/agent-provider-boundary.ts +79 -0
  4. package/cli/export-stream.js +94 -0
  5. package/cli/export-stream.ts +118 -0
  6. package/cli/index.js +132 -61
  7. package/cli/json-export.js +2 -38
  8. package/cli/json-export.ts +2 -56
  9. package/cli/semantic-canonical.js +60 -0
  10. package/cli/semantic-canonical.ts +59 -0
  11. package/cli/semantic-contract.js +97 -0
  12. package/cli/semantic-contract.ts +153 -0
  13. package/cli/semantic-events.js +53 -0
  14. package/cli/semantic-events.ts +65 -0
  15. package/cli/semantic-evidence.js +63 -0
  16. package/cli/semantic-evidence.ts +65 -0
  17. package/cli/semantic-export.js +186 -0
  18. package/cli/semantic-export.ts +245 -0
  19. package/cli/semantic-json.js +69 -0
  20. package/cli/semantic-json.ts +69 -0
  21. package/cli/semantic-line-scanner.js +56 -0
  22. package/cli/semantic-line-scanner.ts +54 -0
  23. package/cli/semantic-parser.js +78 -0
  24. package/cli/semantic-parser.ts +95 -0
  25. package/cli/semantic-provider-line.js +91 -0
  26. package/cli/semantic-provider-line.ts +106 -0
  27. package/cli/trace-evidence.js +86 -0
  28. package/cli/trace-evidence.ts +115 -0
  29. package/cli/trace-export.js +120 -0
  30. package/cli/trace-export.ts +164 -0
  31. package/cli/trace-output-record.js +15 -0
  32. package/cli/trace-output-record.ts +18 -0
  33. package/cli/trace-output.js +98 -0
  34. package/cli/trace-output.ts +119 -0
  35. package/cluster-templates/conductor-bootstrap.json +1 -1
  36. package/lib/agent-cli-provider/adapters/claude.js +1 -1
  37. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  38. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  39. package/lib/agent-cli-provider/adapters/codex.js +26 -1
  40. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  41. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/adapters/omp.js +1 -1
  43. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  44. package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -1
  45. package/lib/agent-cli-provider/log-prefix.js +8 -6
  46. package/lib/agent-cli-provider/log-prefix.js.map +1 -1
  47. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts +4 -0
  48. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts.map +1 -0
  49. package/lib/agent-cli-provider/omp/sdk-environment-policy.js +37 -0
  50. package/lib/agent-cli-provider/omp/sdk-environment-policy.js.map +1 -0
  51. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.d.ts.map +1 -1
  52. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js +4 -15
  53. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js.map +1 -1
  54. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts +2 -0
  55. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts.map +1 -1
  56. package/lib/agent-cli-provider/omp/sdk-process-result.js +2 -2
  57. package/lib/agent-cli-provider/omp/sdk-process-result.js.map +1 -1
  58. package/lib/agent-cli-provider/omp/sdk-process-runner.d.ts.map +1 -1
  59. package/lib/agent-cli-provider/omp/sdk-process-runner.js +6 -1
  60. package/lib/agent-cli-provider/omp/sdk-process-runner.js.map +1 -1
  61. package/lib/agent-cli-provider/provider-registry.d.ts +3 -1
  62. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  63. package/lib/agent-cli-provider/provider-registry.js +9 -2
  64. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  65. package/lib/agent-cli-provider/single-agent-runtime.d.ts +4 -1
  66. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  67. package/lib/agent-cli-provider/single-agent-runtime.js +18 -15
  68. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  69. package/lib/agent-cli-provider/types.d.ts +3 -0
  70. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  71. package/lib/agent-cli-provider/types.js.map +1 -1
  72. package/lib/cluster-worker/engine-adapter-common.js +18 -0
  73. package/lib/cluster-worker/engine-adapter.js +2 -1
  74. package/lib/git-remote-utils.js +22 -5
  75. package/lib/stream-json-parser.js +6 -5
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +7 -4
  78. package/scripts/omp/runtime-identities.js +31 -18
  79. package/scripts/omp/runtime.js +2 -0
  80. package/src/agent/agent-lifecycle.js +17 -1
  81. package/src/agent/agent-task-executor.js +36 -40
  82. package/src/agent/output-extraction-failures.js +73 -0
  83. package/src/agent/output-extraction-failures.ts +100 -0
  84. package/src/agent/output-extraction-json.js +5 -6
  85. package/src/agent/output-extraction-json.ts +4 -7
  86. package/src/agent/output-extraction-types.ts +5 -0
  87. package/src/agent/provider-terminal-failure.js +2 -2
  88. package/src/agent/provider-terminal-failure.ts +4 -2
  89. package/src/agent-cli-provider/adapters/claude.ts +1 -1
  90. package/src/agent-cli-provider/adapters/codex.ts +36 -1
  91. package/src/agent-cli-provider/adapters/omp.ts +2 -1
  92. package/src/agent-cli-provider/log-prefix.ts +12 -7
  93. package/src/agent-cli-provider/omp/sdk-environment-policy.ts +38 -0
  94. package/src/agent-cli-provider/omp/sdk-process-private-runtime.ts +4 -16
  95. package/src/agent-cli-provider/omp/sdk-process-result.ts +4 -2
  96. package/src/agent-cli-provider/omp/sdk-process-runner.ts +7 -1
  97. package/src/agent-cli-provider/provider-registry.ts +10 -2
  98. package/src/agent-cli-provider/single-agent-runtime.ts +24 -15
  99. package/src/agent-cli-provider/types.ts +3 -0
  100. package/src/agents/git-pusher-template.js +61 -71
  101. package/src/attach/socket-paths.js +1 -1
  102. package/src/attach/socket-paths.ts +1 -1
  103. package/src/claude-task-runner.js +9 -9
  104. package/src/foreground-benchmark-run.js +24 -12
  105. package/src/legacy-lib/git-remote-utils.ts +23 -5
  106. package/src/legacy-lib/stream-json-parser.ts +10 -5
  107. package/src/omp-session-verifier.js +8 -5
  108. package/src/orchestrator.js +42 -0
  109. package/src/preflight.js +31 -6
  110. package/src/providers/index.js +4 -22
  111. package/src/task-execution-context.js +22 -0
  112. package/src/task-log-line.d.ts +20 -0
  113. package/src/task-log-line.js +80 -0
  114. package/src/watcher-prompt-channel.js +1 -1
  115. package/src/worktree-claude-config.js +25 -0
  116. package/src/worktree-claude-config.ts +28 -0
  117. package/task-lib/commands/logs.js +5 -8
  118. package/task-lib/runner.js +57 -22
  119. package/task-lib/sdk-watcher-output.js +31 -0
  120. package/task-lib/sdk-watcher.js +4 -11
  121. package/task-lib/watcher-output-runtime.js +30 -16
package/README.md CHANGED
@@ -74,6 +74,36 @@ Every step is written to a crash-safe SQLite ledger. Bring your own provider and
74
74
  <em>One run: classify, execute, verify, and repeat when evidence fails.</em>
75
75
  </div>
76
76
 
77
+ ## Classification and routing
78
+
79
+ The conductor scores every task on complexity (TRIVIAL, SIMPLE, STANDARD, CRITICAL) and type (INQUIRY, TASK, DEBUG) before any code is written, and that score picks the workflow. A junior model runs the pass; when it can't call it, it answers UNCERTAIN and a senior model decides instead.
80
+
81
+ Rules are evaluated top down, first match wins:
82
+
83
+ | Classification | Workflow | Agents |
84
+ | -------------------------------------- | ------------------ | ------------------------------------------------------------- |
85
+ | DEBUG above TRIVIAL, any complexity | `debug-workflow` | investigator, fixer, tester, completion-detector |
86
+ | TRIVIAL TASK or DEBUG, `--pr`/`--ship` | `worker-validator` | worker, 1 validator |
87
+ | TRIVIAL | `single-worker` | worker only, **no validator** |
88
+ | SIMPLE | `worker-validator` | worker, 1 validator |
89
+ | STANDARD | `full-workflow` | planner, worker, 2 validators |
90
+ | CRITICAL | `full-workflow` | planner, worker, meta-coordinator, 4 validators in two stages |
91
+
92
+ TRIVIAL is the row worth knowing about: one worker, no verifier, so the executor–verifier split doesn't apply on that path. CRITICAL is meant to be rare in the other direction, and the conductor is instructed to pick STANDARD whenever it's torn, since CRITICAL spends a senior model and four validators.
93
+
94
+ ## Custom workflows
95
+
96
+ Each workflow above is a JSON file in [`cluster-templates/base-templates/`](cluster-templates/base-templates/), and none of them is privileged. Underneath is a message bus: agents subscribe to topics, publish to topics, and the graph is that wiring.
97
+
98
+ ```bash
99
+ zeroshot config list # available workflows
100
+ zeroshot config show full-workflow # read one
101
+ zeroshot config validate ./mine.json # check yours
102
+ zeroshot run 123 --config ./mine.json # run it
103
+ ```
104
+
105
+ Agent ids, roles, and topic names are free strings, and a trigger can carry a JavaScript predicate deciding whether a message wakes its agent. Cycles are legal, reject-and-retry being one, though `zeroshot config validate` fails a ring of three or more unless something in it carries escape logic. Sub-clusters nest five deep.
106
+
77
107
  ## Providers and issue sources
78
108
 
79
109
  Provider engines come from the registry: **Claude, Codex, bundled Gateway, Gemini, OpenCode, Pi, OMP, Kiro, and Copilot**. Model gateways stay behind the single Gateway provider.
@@ -108,6 +138,7 @@ zeroshot run 123 --pr # worktree + pull request
108
138
  zeroshot run 123 --ship # worktree + PR + merge after approval
109
139
  zeroshot run 123 --pr --pr-body $'## Summary\n\nCustom text\n\n{{issue_reference}}'
110
140
  zeroshot run 123 -d # background run
141
+ zeroshot run 123 --config ./mine.json # custom workflow graph
111
142
 
112
143
  zeroshot list # tasks and clusters (--json)
113
144
  zeroshot status <id> # detailed status (--json)
@@ -115,10 +146,13 @@ zeroshot logs <id> -f # stream logs
115
146
  zeroshot resume <id> [prompt] # resume a stopped or failed run
116
147
  zeroshot stop <id> # graceful stop
117
148
  zeroshot kill <id> # force stop
149
+ zeroshot export <id> --format trace --output run.trace.jsonl
150
+ zeroshot export <id> --format semantic --output run.semantic.jsonl
118
151
 
119
152
  zeroshot providers # provider availability and defaults
120
153
  zeroshot settings # effective settings
121
154
  zeroshot agents list # available agents
155
+ zeroshot config list # workflow graphs (config show / config validate)
122
156
  ```
123
157
 
124
158
  `--pr-body` supplies a deterministic pull-request body for `--pr` and `--ship` runs. The
@@ -126,6 +160,16 @@ template supports `{{issue_number}}`, `{{issue_title}}`, and `{{issue_reference}
126
160
  expand to empty text for tasks without an issue, so manual runs never emit `Closes #unknown`.
127
161
  The unrendered template is retained for detached and resumed runs.
128
162
 
163
+ The `trace` export is a deterministic, provider-neutral research bundle. It preserves the ordered
164
+ cluster ledger, exact selected prompts, and exact raw task-log bytes without interpreting a Claude,
165
+ Codex, Pi, or other provider protocol. Missing evidence is recorded explicitly in its footer. File
166
+ exports are create-only: choose a new output path rather than replacing an existing bundle. Live
167
+ tasks are exported only as explicitly incomplete snapshots.
168
+ The separate `semantic` export runs those task bytes through Zeroshot's existing stateful provider
169
+ adapters and emits bounded `text`, `thinking`, `tool_call`, `tool_result`, and `result` events.
170
+ Zeroshot-owned wrapper and stderr records remain native-only. Parser diagnostics affect only
171
+ semantic completeness; they do not alter the native trace or run.
172
+
129
173
  </details>
130
174
 
131
175
  <details>
@@ -181,7 +225,7 @@ Zeroshot is **Layer 01 · Verification** of [The Open Engine](https://theopeneng
181
225
  | 02 | Constraints: **Opcore** | Sibling · alpha |
182
226
  | 03-05 | Intent · Context · Runtime | In development |
183
227
 
184
- Zeroshot runs the loop: an agent writes the change, and an **independent** verifier decides whether it holds: approve, or a reproducible failure. **Opcore** is the sibling layer, a deterministic, local, read-only **constraints** gate for coding agents. Zeroshot packages Opcore `0.2.1` and uses introduced-change validation so existing repository debt never blocks an otherwise clean change. Verification asks _"does this meet the goal?"_; constraints ask _"is this within tolerance?"_
228
+ Zeroshot runs the loop: an agent writes the change, and **independent** verifiers decide whether it holds, approving it or rejecting it with the specific objections that blocked it. **Opcore** is the sibling layer, a deterministic, local, read-only **constraints** gate for coding agents. Zeroshot packages Opcore `0.2.1` and uses introduced-change validation so existing repository debt never blocks an otherwise clean change. Verification asks _"does this meet the goal?"_; constraints ask _"is this within tolerance?"_
185
229
 
186
230
  Each layer ships the same way: extracted from the platform we run, then opened. **Trust nothing. Verify everything.**
187
231
 
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripTimestampPrefix = exports.getProviderAdapter = void 0;
4
+ function isRuntime(value, method) {
5
+ return (value !== null && typeof value === 'object' && typeof Reflect.get(value, method) === 'function');
6
+ }
7
+ function isAdapterRuntime(value) {
8
+ return isRuntime(value, 'getProviderAdapter');
9
+ }
10
+ function isPrefixRuntime(value) {
11
+ return isRuntime(value, 'stripTimestampPrefix');
12
+ }
13
+ const adapters = require('../lib/agent-cli-provider/adapters');
14
+ const prefixes = require('../lib/agent-cli-provider/log-prefix');
15
+ if (!isAdapterRuntime(adapters) || !isPrefixRuntime(prefixes)) {
16
+ throw new Error('Built agent-provider runtime is unavailable');
17
+ }
18
+ exports.getProviderAdapter = adapters.getProviderAdapter;
19
+ exports.stripTimestampPrefix = prefixes.stripTimestampPrefix;
@@ -0,0 +1,79 @@
1
+ interface TextEvent {
2
+ readonly type: 'text' | 'thinking';
3
+ readonly text: string;
4
+ }
5
+
6
+ interface ToolCallEvent {
7
+ readonly type: 'tool_call';
8
+ readonly toolName: string | null | undefined;
9
+ readonly toolId: string | null | undefined;
10
+ readonly input: unknown;
11
+ }
12
+
13
+ interface ToolResultEvent {
14
+ readonly type: 'tool_result';
15
+ readonly toolId: string | null | undefined;
16
+ readonly content: unknown;
17
+ readonly isError: unknown;
18
+ }
19
+
20
+ interface ResultEvent {
21
+ readonly type: 'result';
22
+ readonly success: boolean;
23
+ readonly result?: unknown;
24
+ readonly error?: unknown;
25
+ readonly cost?: unknown;
26
+ readonly duration?: unknown;
27
+ readonly inputTokens?: number;
28
+ readonly outputTokens?: number;
29
+ readonly cacheReadInputTokens?: number;
30
+ readonly cacheCreationInputTokens?: number;
31
+ readonly modelUsage?: unknown;
32
+ readonly requests?: number;
33
+ readonly usageSource?: unknown;
34
+ readonly usageCompleteness?: unknown;
35
+ readonly invocation?: unknown;
36
+ readonly ompSdk?: unknown;
37
+ }
38
+
39
+ export type OutputEvent = TextEvent | ToolCallEvent | ToolResultEvent | ResultEvent;
40
+ export type ProviderParseResult = OutputEvent | readonly OutputEvent[] | null;
41
+
42
+ export interface ProviderAdapter {
43
+ readonly id: string;
44
+ readonly adapterVersion: string;
45
+ createParserState(): object;
46
+ parseEvent(line: string, state: object): ProviderParseResult;
47
+ finishParsing?(state: object): ProviderParseResult;
48
+ }
49
+
50
+ interface AdapterRuntime {
51
+ getProviderAdapter(provider: string): ProviderAdapter;
52
+ }
53
+
54
+ interface PrefixRuntime {
55
+ stripTimestampPrefix(line: string): string;
56
+ }
57
+
58
+ function isRuntime(value: unknown, method: string): boolean {
59
+ return (
60
+ value !== null && typeof value === 'object' && typeof Reflect.get(value, method) === 'function'
61
+ );
62
+ }
63
+
64
+ function isAdapterRuntime(value: unknown): value is AdapterRuntime {
65
+ return isRuntime(value, 'getProviderAdapter');
66
+ }
67
+
68
+ function isPrefixRuntime(value: unknown): value is PrefixRuntime {
69
+ return isRuntime(value, 'stripTimestampPrefix');
70
+ }
71
+
72
+ const adapters: unknown = require('../lib/agent-cli-provider/adapters');
73
+ const prefixes: unknown = require('../lib/agent-cli-provider/log-prefix');
74
+ if (!isAdapterRuntime(adapters) || !isPrefixRuntime(prefixes)) {
75
+ throw new Error('Built agent-provider runtime is unavailable');
76
+ }
77
+
78
+ export const getProviderAdapter = adapters.getProviderAdapter;
79
+ export const stripTimestampPrefix = prefixes.stripTimestampPrefix;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nullableString = nullableString;
4
+ exports.isRecord = isRecord;
5
+ exports.compareText = compareText;
6
+ exports.sameFileSnapshot = sameFileSnapshot;
7
+ exports.createExclusiveDestination = createExclusiveDestination;
8
+ exports.createReplacingDestination = createReplacingDestination;
9
+ exports.createRecordWriter = createRecordWriter;
10
+ const crypto = require("crypto");
11
+ const fs = require("fs");
12
+ function nullableString(value) {
13
+ return typeof value === 'string' ? value : null;
14
+ }
15
+ function isRecord(value) {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+ function compareText(left, right) {
19
+ if (left < right)
20
+ return -1;
21
+ return left > right ? 1 : 0;
22
+ }
23
+ function sameFileSnapshot(before, after) {
24
+ return (before.dev === after.dev &&
25
+ before.ino === after.ino &&
26
+ before.size === after.size &&
27
+ before.mtimeNs === after.mtimeNs &&
28
+ before.ctimeNs === after.ctimeNs);
29
+ }
30
+ function writeAll(fd, value, label) {
31
+ const bytes = Buffer.from(value);
32
+ let offset = 0;
33
+ while (offset < bytes.length) {
34
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
35
+ if (!Number.isInteger(written) || written <= 0) {
36
+ throw new Error(`${label} export destination stopped accepting bytes`);
37
+ }
38
+ offset += written;
39
+ }
40
+ }
41
+ function streamDestination(stdout, label) {
42
+ if (typeof stdout.fd === 'number' && Number.isInteger(stdout.fd)) {
43
+ const fd = stdout.fd;
44
+ return { close() { }, write: (value) => writeAll(fd, value, label) };
45
+ }
46
+ return {
47
+ close() { },
48
+ write(value) {
49
+ stdout.write(value);
50
+ },
51
+ };
52
+ }
53
+ function createExclusiveDestination(outputPath, stdout, label) {
54
+ if (!outputPath)
55
+ return streamDestination(stdout, label);
56
+ const flags = fs.constants.O_WRONLY |
57
+ fs.constants.O_CREAT |
58
+ fs.constants.O_EXCL |
59
+ (fs.constants.O_NOFOLLOW || 0);
60
+ const fd = fs.openSync(outputPath, flags, 0o600);
61
+ try {
62
+ fs.fchmodSync(fd, 0o600);
63
+ }
64
+ catch (error) {
65
+ fs.closeSync(fd);
66
+ throw error;
67
+ }
68
+ return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, label) };
69
+ }
70
+ function createReplacingDestination(outputPath, stdout) {
71
+ if (!outputPath)
72
+ return streamDestination(stdout, 'JSON');
73
+ const fd = fs.openSync(outputPath, 'w');
74
+ return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, 'JSON') };
75
+ }
76
+ function createRecordWriter(destination) {
77
+ const digest = crypto.createHash('sha256');
78
+ let records = 0;
79
+ const encode = (record) => `${JSON.stringify(record)}\n`;
80
+ return {
81
+ get records() {
82
+ return records;
83
+ },
84
+ write(record) {
85
+ const line = encode(record);
86
+ digest.update(line);
87
+ destination.write(line);
88
+ records += 1;
89
+ },
90
+ finish(record) {
91
+ destination.write(encode({ ...record, preceding_records: records, records_sha256: digest.digest('hex') }));
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,118 @@
1
+ import crypto = require('crypto');
2
+ import fs = require('fs');
3
+
4
+ export interface ExportStream {
5
+ readonly fd?: number;
6
+ write(value: string): unknown;
7
+ }
8
+
9
+ export interface Destination {
10
+ close(): void;
11
+ write(value: string): void;
12
+ }
13
+
14
+ export interface RecordWriter {
15
+ readonly records: number;
16
+ finish(record: Record<string, unknown>): void;
17
+ write(record: Record<string, unknown>): void;
18
+ }
19
+
20
+ export function nullableString(value: unknown): string | null {
21
+ return typeof value === 'string' ? value : null;
22
+ }
23
+
24
+ export function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+
28
+ export function compareText(left: string, right: string): number {
29
+ if (left < right) return -1;
30
+ return left > right ? 1 : 0;
31
+ }
32
+
33
+ export function sameFileSnapshot(before: fs.BigIntStats, after: fs.BigIntStats): boolean {
34
+ return (
35
+ before.dev === after.dev &&
36
+ before.ino === after.ino &&
37
+ before.size === after.size &&
38
+ before.mtimeNs === after.mtimeNs &&
39
+ before.ctimeNs === after.ctimeNs
40
+ );
41
+ }
42
+
43
+ function writeAll(fd: number, value: string, label: string): void {
44
+ const bytes = Buffer.from(value);
45
+ let offset = 0;
46
+ while (offset < bytes.length) {
47
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
48
+ if (!Number.isInteger(written) || written <= 0) {
49
+ throw new Error(`${label} export destination stopped accepting bytes`);
50
+ }
51
+ offset += written;
52
+ }
53
+ }
54
+
55
+ function streamDestination(stdout: ExportStream, label: string): Destination {
56
+ if (typeof stdout.fd === 'number' && Number.isInteger(stdout.fd)) {
57
+ const fd = stdout.fd;
58
+ return { close(): void {}, write: (value) => writeAll(fd, value, label) };
59
+ }
60
+ return {
61
+ close(): void {},
62
+ write(value): void {
63
+ stdout.write(value);
64
+ },
65
+ };
66
+ }
67
+
68
+ export function createExclusiveDestination(
69
+ outputPath: string | null | undefined,
70
+ stdout: ExportStream,
71
+ label: string
72
+ ): Destination {
73
+ if (!outputPath) return streamDestination(stdout, label);
74
+ const flags =
75
+ fs.constants.O_WRONLY |
76
+ fs.constants.O_CREAT |
77
+ fs.constants.O_EXCL |
78
+ (fs.constants.O_NOFOLLOW || 0);
79
+ const fd = fs.openSync(outputPath, flags, 0o600);
80
+ try {
81
+ fs.fchmodSync(fd, 0o600);
82
+ } catch (error) {
83
+ fs.closeSync(fd);
84
+ throw error;
85
+ }
86
+ return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, label) };
87
+ }
88
+
89
+ export function createReplacingDestination(
90
+ outputPath: string | null | undefined,
91
+ stdout: ExportStream
92
+ ): Destination {
93
+ if (!outputPath) return streamDestination(stdout, 'JSON');
94
+ const fd = fs.openSync(outputPath, 'w');
95
+ return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, 'JSON') };
96
+ }
97
+
98
+ export function createRecordWriter(destination: Destination): RecordWriter {
99
+ const digest = crypto.createHash('sha256');
100
+ let records = 0;
101
+ const encode = (record: Record<string, unknown>): string => `${JSON.stringify(record)}\n`;
102
+ return {
103
+ get records(): number {
104
+ return records;
105
+ },
106
+ write(record): void {
107
+ const line = encode(record);
108
+ digest.update(line);
109
+ destination.write(line);
110
+ records += 1;
111
+ },
112
+ finish(record): void {
113
+ destination.write(
114
+ encode({ ...record, preceding_records: records, records_sha256: digest.digest('hex') })
115
+ );
116
+ },
117
+ };
118
+ }
package/cli/index.js CHANGED
@@ -59,6 +59,7 @@ const {
59
59
  } = require('../lib/provider-names');
60
60
  const { commandExists } = require('../lib/provider-detection');
61
61
  const { getProvider, parseProviderChunk } = require('../src/providers');
62
+ const { decodeTaskLogLine } = require('../src/task-log-line');
62
63
  const { readClustersFileSync } = require('../lib/clusters-registry');
63
64
  const { MOUNT_PRESETS, resolveEnvs } = require('../lib/docker-config');
64
65
  const {
@@ -78,7 +79,10 @@ const {
78
79
  resolveEffectiveRunPlan,
79
80
  } = require('../lib/start-cluster');
80
81
  const { requirePreflight } = require('../src/preflight');
82
+ const { resolveTaskExecutionContext } = require('../src/task-execution-context');
81
83
  const {
84
+ buildForegroundResult,
85
+ exitCodeForForegroundResult,
82
86
  exitCodeForResult,
83
87
  isForegroundStatusSettled,
84
88
  writeForegroundResult,
@@ -752,16 +756,25 @@ async function finishForegroundRun({ cluster, orchestrator, clusterId, plan, res
752
756
  plan,
753
757
  Boolean(resultPath)
754
758
  );
755
- if (!resultPath) return;
756
- const receipt = writeForegroundResult({
759
+ if (resultPath) {
760
+ const receipt = writeForegroundResult({
761
+ orchestrator,
762
+ cluster,
763
+ clusterId,
764
+ resultPath,
765
+ cancelled: foreground.cancelled,
766
+ });
767
+ process.exitCode = exitCodeForResult(receipt);
768
+ console.log(chalk.dim(`Result ${receipt.outcome} committed to ${resultPath}`));
769
+ return;
770
+ }
771
+ const result = buildForegroundResult({
757
772
  orchestrator,
758
773
  cluster,
759
774
  clusterId,
760
- resultPath,
761
775
  cancelled: foreground.cancelled,
762
776
  });
763
- process.exitCode = exitCodeForResult(receipt);
764
- console.log(chalk.dim(`Result ${receipt.outcome} committed to ${resultPath}`));
777
+ process.exitCode = exitCodeForForegroundResult(result);
765
778
  } finally {
766
779
  orchestrator.close();
767
780
  }
@@ -1418,19 +1431,10 @@ function startClusterDbPolling(cluster, clusterId, isActive, options) {
1418
1431
  }
1419
1432
 
1420
1433
  function parseIncrementalTaskLogLine(line) {
1421
- const trimmed = line.trim();
1422
- if (!trimmed.startsWith('{')) {
1423
- return null;
1424
- }
1425
-
1426
- let timestamp = Date.now();
1427
- let jsonContent = trimmed;
1428
-
1429
- const timestampMatch = jsonContent.match(/^\[(\d{13})\](.*)$/);
1430
- if (timestampMatch) {
1431
- timestamp = parseInt(timestampMatch[1], 10);
1432
- jsonContent = timestampMatch[2];
1433
- }
1434
+ const decoded = decodeTaskLogLine(line.trim());
1435
+ if (!decoded.providerOutput) return null;
1436
+ const timestamp = decoded.timestamp ?? Date.now();
1437
+ const jsonContent = decoded.content.trim();
1434
1438
 
1435
1439
  if (!jsonContent.startsWith('{')) {
1436
1440
  return null;
@@ -2607,37 +2611,7 @@ function findAgentFromLifecycle(agents, taskId, lifecycleMessages) {
2607
2611
  }
2608
2612
 
2609
2613
  function parseTaskLogLine(line) {
2610
- const trimmed = line.trim();
2611
- if (!trimmed.startsWith('[')) {
2612
- return null;
2613
- }
2614
-
2615
- const parsed = parseTimestampedLine(trimmed);
2616
- if (!parsed.jsonContent.startsWith('{')) {
2617
- return null;
2618
- }
2619
-
2620
- try {
2621
- const json = JSON.parse(parsed.jsonContent);
2622
- if (json.type === 'system' && json.subtype === 'init') {
2623
- return null;
2624
- }
2625
- } catch {
2626
- return null;
2627
- }
2628
-
2629
- return parsed;
2630
- }
2631
-
2632
- function parseTimestampedLine(line) {
2633
- const timestampMatch = line.match(/^\[(\d{13})\](.*)$/);
2634
- if (!timestampMatch) {
2635
- return { timestamp: Date.now(), jsonContent: line };
2636
- }
2637
- return {
2638
- timestamp: parseInt(timestampMatch[1], 10),
2639
- jsonContent: timestampMatch[2],
2640
- };
2614
+ return parseIncrementalTaskLogLine(line);
2641
2615
  }
2642
2616
 
2643
2617
  function buildTaskLogMessage({ taskId, timestamp, jsonContent, cluster, agent, iteration }) {
@@ -3053,6 +3027,7 @@ taskCmd
3053
3027
  requireDocker: false, // Docker not needed for plain tasks
3054
3028
  quiet: false,
3055
3029
  provider: providerOverride,
3030
+ executionContext: resolveTaskExecutionContext(),
3056
3031
  });
3057
3032
 
3058
3033
  // Dynamically import task command (ESM module)
@@ -3392,9 +3367,9 @@ program
3392
3367
  .command('export <cluster-id>')
3393
3368
  .helpGroup('Maintenance:')
3394
3369
  .description('Export cluster conversation')
3395
- .option('-f, --format <format>', 'Export format: json, markdown, html', 'html')
3370
+ .option('-f, --format <format>', 'Export format: json, trace, semantic, markdown, html', 'html')
3396
3371
  .option('-o, --output <file>', 'Output file (auto-generated for html)')
3397
- .action((clusterId, options) => {
3372
+ .action(async (clusterId, options) => {
3398
3373
  try {
3399
3374
  // Get messages from DB
3400
3375
  const Ledger = require('../src/ledger');
@@ -3405,14 +3380,7 @@ program
3405
3380
  throw new Error(`Cluster ${clusterId} not found (no DB file)`);
3406
3381
  }
3407
3382
 
3408
- // JSON export
3409
- if (options.format === 'json') {
3410
- exportClusterJson(dbPath, clusterId, options.output || null);
3411
- if (options.output) {
3412
- console.log(`Exported to ${options.output}`);
3413
- }
3414
- return;
3415
- }
3383
+ if (await exportClusterDataFormat(dbPath, clusterId, options)) return;
3416
3384
 
3417
3385
  const ledger = new Ledger(dbPath);
3418
3386
  const messages = ledger.getAll(clusterId);
@@ -3727,6 +3695,7 @@ program
3727
3695
  requireDocker: false,
3728
3696
  quiet: false,
3729
3697
  provider: providerName,
3698
+ executionContext: resolveTaskExecutionContext(),
3730
3699
  });
3731
3700
 
3732
3701
  // Try resuming as task
@@ -5092,9 +5061,18 @@ function formatToolCall(toolName, input) {
5092
5061
  function formatToolResult(content, isError, toolName, toolInput) {
5093
5062
  if (!content) return isError ? 'error' : 'done';
5094
5063
 
5064
+ let normalizedContent = content;
5065
+ if (typeof content !== 'string') {
5066
+ try {
5067
+ normalizedContent = JSON.stringify(content) ?? String(content);
5068
+ } catch {
5069
+ normalizedContent = String(content);
5070
+ }
5071
+ }
5072
+
5095
5073
  // For errors, show full message
5096
5074
  if (isError) {
5097
- const firstLine = content.split('\n')[0].substring(0, 80);
5075
+ const firstLine = normalizedContent.split('\n')[0].substring(0, 80);
5098
5076
  return chalk.red(firstLine);
5099
5077
  }
5100
5078
 
@@ -5123,7 +5101,7 @@ function formatToolResult(content, isError, toolName, toolInput) {
5123
5101
  }
5124
5102
 
5125
5103
  // For success, show summary
5126
- const lines = content.split('\n').filter((l) => l.trim());
5104
+ const lines = normalizedContent.split('\n').filter((l) => l.trim());
5127
5105
  if (lines.length === 0) return 'done';
5128
5106
  if (lines.length === 1) {
5129
5107
  const line = lines[0].substring(0, 60);
@@ -6179,6 +6157,98 @@ function exportClusterJson(dbPath, clusterId, outputPath) {
6179
6157
  );
6180
6158
  }
6181
6159
 
6160
+ function tryExportClusterTraceSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
6161
+ const Ledger = require('../src/ledger');
6162
+ const { streamClusterTraceExport } = require('./trace-export');
6163
+ const ledger = new Ledger(dbPath, { readonly: true });
6164
+ try {
6165
+ return ledger.withReadSnapshot(() => {
6166
+ if (ledger.needsAgentOutputReconciliation(clusterId)) return false;
6167
+ streamClusterTraceExport({
6168
+ ledger,
6169
+ clusterId,
6170
+ readTask,
6171
+ allowedLogRoot,
6172
+ outputPath,
6173
+ });
6174
+ return true;
6175
+ });
6176
+ } finally {
6177
+ ledger.close();
6178
+ }
6179
+ }
6180
+
6181
+ function exportClusterTrace(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
6182
+ const Ledger = require('../src/ledger');
6183
+ const maxReconciliationAttempts = 3;
6184
+ for (let attempt = 0; attempt <= maxReconciliationAttempts; attempt += 1) {
6185
+ if (tryExportClusterTraceSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot)) {
6186
+ return;
6187
+ }
6188
+ if (attempt === maxReconciliationAttempts) break;
6189
+ const reconciliationLedger = new Ledger(dbPath);
6190
+ reconciliationLedger.close();
6191
+ }
6192
+ throw new Error(
6193
+ `Cluster ${clusterId} output changed during ${maxReconciliationAttempts} reconciliation attempts`
6194
+ );
6195
+ }
6196
+
6197
+ function tryExportClusterSemanticSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
6198
+ const Ledger = require('../src/ledger');
6199
+ const { streamClusterSemanticExport } = require('./semantic-export');
6200
+ const ledger = new Ledger(dbPath, { readonly: true });
6201
+ try {
6202
+ return ledger.withReadSnapshot(() => {
6203
+ if (ledger.needsAgentOutputReconciliation(clusterId)) return false;
6204
+ streamClusterSemanticExport({
6205
+ ledger,
6206
+ clusterId,
6207
+ readTask,
6208
+ allowedLogRoot,
6209
+ outputPath,
6210
+ });
6211
+ return true;
6212
+ });
6213
+ } finally {
6214
+ ledger.close();
6215
+ }
6216
+ }
6217
+
6218
+ function exportClusterSemantic(dbPath, clusterId, outputPath, readTask, allowedLogRoot) {
6219
+ const Ledger = require('../src/ledger');
6220
+ const maxReconciliationAttempts = 3;
6221
+ for (let attempt = 0; attempt <= maxReconciliationAttempts; attempt += 1) {
6222
+ if (tryExportClusterSemanticSnapshot(dbPath, clusterId, outputPath, readTask, allowedLogRoot)) {
6223
+ return;
6224
+ }
6225
+ if (attempt === maxReconciliationAttempts) break;
6226
+ const reconciliationLedger = new Ledger(dbPath);
6227
+ reconciliationLedger.close();
6228
+ }
6229
+ throw new Error(
6230
+ `Cluster ${clusterId} output changed during ${maxReconciliationAttempts} reconciliation attempts`
6231
+ );
6232
+ }
6233
+
6234
+ async function exportClusterDataFormat(dbPath, clusterId, options) {
6235
+ if (options.format === 'json') {
6236
+ exportClusterJson(dbPath, clusterId, options.output || null);
6237
+ if (options.output) console.log(`Exported to ${options.output}`);
6238
+ return true;
6239
+ }
6240
+ if (options.format !== 'trace' && options.format !== 'semantic') return false;
6241
+
6242
+ const [{ getTask }, { LOGS_DIR }] = await Promise.all([
6243
+ import('../task-lib/store.js'),
6244
+ import('../task-lib/config.js'),
6245
+ ]);
6246
+ const exporter = options.format === 'trace' ? exportClusterTrace : exportClusterSemantic;
6247
+ exporter(dbPath, clusterId, options.output || null, getTask, LOGS_DIR);
6248
+ if (options.output) console.log(`Exported ${options.format} to ${options.output}`);
6249
+ return true;
6250
+ }
6251
+
6182
6252
  // Main entry point
6183
6253
  async function main() {
6184
6254
  printLegacyDistroNotice();
@@ -6235,4 +6305,5 @@ module.exports = {
6235
6305
  buildCompletionPrompt,
6236
6306
  resolveRunMode,
6237
6307
  killRunningClusters,
6308
+ parseTaskLogLine,
6238
6309
  };