ennodia 0.3.0 → 0.4.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.
- package/CONTRIBUTING.md +2 -19
- package/README.md +4 -6
- package/docs/comparisons/agent-frameworks.md +5 -5
- package/docs/comparisons/autogen.md +4 -4
- package/docs/comparisons/chathub.md +4 -4
- package/docs/comparisons/index.md +4 -4
- package/docs/comparisons/langgraph.md +4 -4
- package/docs/comparisons/mixture-of-agents.md +5 -5
- package/docs/comparisons/model-merging.md +5 -5
- package/docs/comparisons/openrouter.md +4 -4
- package/docs/concepts/compositional-audits.md +8 -6
- package/docs/concepts/data-governance.md +5 -9
- package/docs/concepts/how-ennodia-works.md +2 -2
- package/docs/concepts/interfaces-and-core.md +1 -1
- package/docs/concepts/second-opinions.md +5 -5
- package/docs/evidence/index.md +6 -13
- package/docs/evidence/measurement.md +6 -5
- package/docs/getting-started.md +9 -6
- package/docs/guides/agent-skills.md +6 -5
- package/docs/guides/budgets-and-limits.md +8 -6
- package/docs/guides/pragmatic-mode.md +2 -0
- package/docs/guides/recipes.md +1 -4
- package/docs/guides/running-better-audits.md +7 -7
- package/docs/guides/troubleshooting.md +0 -2
- package/docs/guides/understand-results.md +0 -2
- package/docs/index.md +2 -4
- package/docs/install.md +11 -9
- package/docs/reference/controlled-english.md +1 -3
- package/docs/reference/ennodia-io.md +7 -7
- package/docs/reference/mcp-tools.md +69 -15
- package/docs/reference/supported-harnesses.md +9 -4
- package/docs/roadmap.md +0 -2
- package/package.json +3 -10
- package/skills/pragmatic/SKILL.md +2 -0
- package/src/cli.ts +31 -15
- package/src/compare.ts +37 -1
- package/src/core.ts +2 -4
- package/src/diagnosis.ts +48 -1
- package/src/harnesses.ts +54 -25
- package/src/index.ts +4 -0
- package/src/runs.ts +17 -8
- package/src/server.ts +55 -2
- package/src/tasks.ts +145 -37
- package/src/version.ts +1 -1
- package/bench/README.md +0 -34
- package/bench/bug-recall/run.ts +0 -339
- package/bench/bug-recall/scorer.ts +0 -424
- package/bench/fixtures/bug-recall/001-missing-await/input.ts +0 -33
- package/bench/fixtures/bug-recall/001-missing-await/oracle.json +0 -28
- package/bench/fixtures/bug-recall/001-missing-await/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/001-missing-await/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/001-missing-await/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/input.ts +0 -40
- package/bench/fixtures/bug-recall/002-timeout-drain/oracle.json +0 -28
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/input.ts +0 -22
- package/bench/fixtures/bug-recall/003-schema-contract/oracle.json +0 -28
- package/bench/fixtures/bug-recall/003-schema-contract/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/003-schema-contract/responses/ennodia-parallel-compare.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/input.ts +0 -28
- package/bench/fixtures/bug-recall/004-cancel-propagation/oracle.json +0 -28
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/claude-code-solo.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/codex-solo.md +0 -1
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/ennodia-parallel-compare.md +0 -1
- package/bench/results/.gitkeep +0 -1
- package/docs/evidence/building-ennodia.md +0 -66
- package/docs/evidence/docs-drift.md +0 -73
- package/docs/evidence/skill-trial.md +0 -68
- package/docs/reference/benchmarks.md +0 -64
- package/examples/docs-drift/README.md +0 -8
- package/examples/docs-drift/archive/migration.md +0 -4
- package/examples/docs-drift/check.ts +0 -7
- package/examples/docs-drift/config.ts +0 -8
- package/examples/docs-drift/demo.ts +0 -54
- package/examples/docs-drift/recorded-run.json +0 -16
- package/examples/skill-trial/README.md +0 -8
- package/examples/skill-trial/check.ts +0 -15
- package/examples/skill-trial/contract.md +0 -7
- package/examples/skill-trial/recorded-trial.json +0 -42
- package/examples/skill-trial/retry.ts +0 -4
package/src/tasks.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
HarnessRunInput,
|
|
11
11
|
HarnessUsage,
|
|
12
12
|
} from "./harnesses";
|
|
13
|
-
import {
|
|
13
|
+
import { assertHarnessSettingsSupported, DEFAULT_TASK_TIMEOUT_MS, inputGuidanceNotes, withInputGuidance } from "./harnesses";
|
|
14
14
|
import { preview, tailItems } from "./internal";
|
|
15
15
|
import { signalOwnedProcess } from "./process";
|
|
16
16
|
import {
|
|
@@ -19,11 +19,14 @@ import {
|
|
|
19
19
|
type AppliedSkillInfo,
|
|
20
20
|
} from "./skills";
|
|
21
21
|
|
|
22
|
+
/** Ennodia's own instructions are tagged, so workers do not read them as task content. */
|
|
23
|
+
export const EXECUTION_NOTICE_TAG = "ennodia-execution-notice";
|
|
24
|
+
|
|
22
25
|
export type TaskStatus = "running" | "succeeded" | "failed" | "cancelled";
|
|
23
26
|
|
|
24
27
|
export type TaskEvent = {
|
|
25
28
|
at: string;
|
|
26
|
-
type: "started" | "input-guidance" | "stdout" | "stderr" | "exit" | "cancel" | "error";
|
|
29
|
+
type: "started" | "input-guidance" | "restart" | "stdout" | "stderr" | "exit" | "cancel" | "error";
|
|
27
30
|
message?: string;
|
|
28
31
|
};
|
|
29
32
|
|
|
@@ -83,6 +86,9 @@ export type TaskView = {
|
|
|
83
86
|
/** Set when this task ran against an isolated copy of the requested cwd
|
|
84
87
|
* (see HarnessRunInput.isolateCwd), naming the original directory. */
|
|
85
88
|
isolatedFrom?: string;
|
|
89
|
+
/** Starts repeated because another instance of the same CLI held shared
|
|
90
|
+
* local state. Present only when a restart happened. */
|
|
91
|
+
startupRestarts?: number;
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
export type TaskViewOptions = {
|
|
@@ -138,6 +144,11 @@ type InternalTask = Omit<
|
|
|
138
144
|
isolatedCwd?: string;
|
|
139
145
|
extractUsage?: HarnessAdapter["extractUsage"];
|
|
140
146
|
failureReason?: HarnessAdapter["failureReason"];
|
|
147
|
+
/** The exact spawn, kept so a start that met a startup conflict can run again. */
|
|
148
|
+
spawnInput: TaskSpawnInput;
|
|
149
|
+
stdinText?: string;
|
|
150
|
+
startupConflict?: RegExp;
|
|
151
|
+
attemptStartedAtMs: number;
|
|
141
152
|
};
|
|
142
153
|
|
|
143
154
|
export type StartTaskResult = {
|
|
@@ -146,6 +157,8 @@ export type StartTaskResult = {
|
|
|
146
157
|
|
|
147
158
|
export type TaskManagerOptions = {
|
|
148
159
|
drainTimeoutMs?: number;
|
|
160
|
+
/** Base delay before restarting a start that met a startup conflict. */
|
|
161
|
+
startupRestartDelayMs?: number;
|
|
149
162
|
maxTasks?: number;
|
|
150
163
|
spawn?: TaskSpawn;
|
|
151
164
|
removeIsolatedCwd?: (path: string) => void;
|
|
@@ -161,10 +174,16 @@ const DEFAULT_MAX_TASKS = 200;
|
|
|
161
174
|
const MAX_CAPTURE_CHARS = 200_000;
|
|
162
175
|
const MAX_EVENT_MESSAGE_CHARS = 4_000;
|
|
163
176
|
const MAX_EVENTS = 300;
|
|
177
|
+
// A startup conflict shows up within moments of launch. Later failures are
|
|
178
|
+
// not restarted, because the worker may have started real work.
|
|
179
|
+
const STARTUP_CONFLICT_WINDOW_MS = 10_000;
|
|
180
|
+
const MAX_STARTUP_RESTARTS = 3;
|
|
181
|
+
const STARTUP_RESTART_DELAY_MS = 500;
|
|
164
182
|
|
|
165
183
|
export class TaskManager {
|
|
166
184
|
private readonly tasks = new Map<string, InternalTask>();
|
|
167
185
|
private readonly drainTimeoutMs: number;
|
|
186
|
+
private readonly startupRestartDelayMs: number;
|
|
168
187
|
private readonly maxTasks: number;
|
|
169
188
|
private readonly spawn: TaskSpawn;
|
|
170
189
|
private readonly removeIsolatedCwd: (path: string) => void;
|
|
@@ -175,6 +194,7 @@ export class TaskManager {
|
|
|
175
194
|
|
|
176
195
|
constructor(options: TaskManagerOptions = {}) {
|
|
177
196
|
this.drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
197
|
+
this.startupRestartDelayMs = options.startupRestartDelayMs ?? STARTUP_RESTART_DELAY_MS;
|
|
178
198
|
this.maxTasks = Math.max(1, options.maxTasks ?? DEFAULT_MAX_TASKS);
|
|
179
199
|
this.spawn = options.spawn ?? ((input) => Bun.spawn(input));
|
|
180
200
|
this.ownsProcessGroups = !options.spawn && process.platform !== "win32";
|
|
@@ -214,9 +234,7 @@ export class TaskManager {
|
|
|
214
234
|
if (input.persistSession && (!adapter.supportsSessionContinuation || input.isolateCwd)) {
|
|
215
235
|
throw new Error("Persistent sessions require a supported harness and a persistent working directory.");
|
|
216
236
|
}
|
|
217
|
-
|
|
218
|
-
assertNativeSubagentsSupported(adapter, input.nativeSubagents);
|
|
219
|
-
assertNativeSandboxSupported(adapter, input.nativeSandbox);
|
|
237
|
+
assertHarnessSettingsSupported(adapter, input);
|
|
220
238
|
if (adapter.supportsNativeSandbox) input = { ...input, nativeSandbox: input.nativeSandbox ?? "read-only" };
|
|
221
239
|
|
|
222
240
|
const sourceCwd = input.cwd ?? process.cwd();
|
|
@@ -244,26 +262,46 @@ export class TaskManager {
|
|
|
244
262
|
const now = Date.now();
|
|
245
263
|
const deadlineMs = now + timeoutMs;
|
|
246
264
|
const deadlineAt = new Date(deadlineMs).toISOString();
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
const guidance = inputGuidanceNotes(input.prompt, adapter);
|
|
266
|
+
const noticeLines = [
|
|
267
|
+
`Execution allowance: ${timeoutMs} ms from task launch (${input.timeoutMs === undefined ? "Ennodia default" : "caller supplied"}). Deadline (UTC): ${deadlineAt}. This is a hard cutoff, not a completion estimate or a target duration.`,
|
|
268
|
+
"Check actual UTC time before attributing incomplete work to the deadline. Do not report a cutoff merely because the task is difficult or the answer is getting long. Continue within the remaining allowance unless complete or blocked by a demonstrated access or tool failure.",
|
|
269
|
+
`Configured native sandbox: ${input.nativeSandbox ?? "harness-managed; check available permissions"}. Report any mismatch between the assignment and available access before attempting the work.`,
|
|
270
|
+
"Before the cutoff, return useful partial findings, verification performed, remaining work, and a justified continuation budget if needed. Only the caller can authorize a follow-up.",
|
|
271
|
+
"Use only tools and permissions actually available in this session; the caller's tools are not automatically inherited. If access is missing, report the exact tool, file, or permission and its impact. Try a permitted alternative when useful; do not repeat an unchanged failed attempt or claim success without evidence.",
|
|
272
|
+
];
|
|
273
|
+
// An adapter that takes operator instructions gets Ennodia's notices
|
|
274
|
+
// there. Other workers get them as tagged blocks after the task.
|
|
275
|
+
let systemNotice: string | undefined;
|
|
276
|
+
let executionPrompt: string;
|
|
277
|
+
if (adapter.systemNotices) {
|
|
278
|
+
executionPrompt = input.prompt;
|
|
279
|
+
systemNotice = [
|
|
280
|
+
"Ennodia, the local tool that started this session for its caller, sets these limits for the task.",
|
|
281
|
+
...noticeLines,
|
|
282
|
+
...(guidance ? ["When the task needs native media inspection:", ...guidance.map((note) => `- ${note}`)] : []),
|
|
283
|
+
].join("\n");
|
|
284
|
+
} else {
|
|
285
|
+
executionPrompt = `${withInputGuidance(input.prompt, adapter)}
|
|
286
|
+
|
|
287
|
+
<${EXECUTION_NOTICE_TAG}>
|
|
288
|
+
From Ennodia, not part of the task above.
|
|
289
|
+
${noticeLines.join("\n")}
|
|
290
|
+
</${EXECUTION_NOTICE_TAG}>`;
|
|
291
|
+
}
|
|
256
292
|
const augmentedPrompt = input.skills && input.skills.length > 0
|
|
257
293
|
? augmentPrompt(executionPrompt, input.skills, adapter.id)
|
|
258
294
|
: executionPrompt;
|
|
259
295
|
let commandSpec: CommandSpec;
|
|
260
296
|
let cwd: string;
|
|
297
|
+
let spawnInput: TaskSpawnInput;
|
|
261
298
|
let child: TaskProcess;
|
|
262
299
|
try {
|
|
263
300
|
commandSpec = adapter.buildCommand(discovery.commandPath, {
|
|
264
301
|
...input,
|
|
265
302
|
cwd: resolvedInputCwd,
|
|
266
303
|
prompt: augmentedPrompt,
|
|
304
|
+
systemNotice,
|
|
267
305
|
finalMessagePath,
|
|
268
306
|
nativeSessionId: previous?.nativeSessionId,
|
|
269
307
|
});
|
|
@@ -273,7 +311,7 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
273
311
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
274
312
|
}
|
|
275
313
|
|
|
276
|
-
|
|
314
|
+
spawnInput = {
|
|
277
315
|
cmd: [commandSpec.command, ...commandSpec.args],
|
|
278
316
|
cwd,
|
|
279
317
|
env: { ...process.env, ...commandSpec.env },
|
|
@@ -281,7 +319,8 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
281
319
|
stdout: "pipe",
|
|
282
320
|
stderr: "pipe",
|
|
283
321
|
detached: process.platform !== "win32",
|
|
284
|
-
}
|
|
322
|
+
};
|
|
323
|
+
child = this.spawn(spawnInput);
|
|
285
324
|
} catch (error) {
|
|
286
325
|
cleanupFailedStartArtifacts(
|
|
287
326
|
finalMessagePath,
|
|
@@ -307,7 +346,7 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
307
346
|
isolatedFrom,
|
|
308
347
|
command: [
|
|
309
348
|
basename(commandSpec.command),
|
|
310
|
-
...commandSpec.args.map((arg) => arg === augmentedPrompt ? "<prompt>" : arg),
|
|
349
|
+
...commandSpec.args.map((arg) => arg === augmentedPrompt ? "<prompt>" : arg === systemNotice ? "<ennodia-notice>" : arg),
|
|
311
350
|
...(commandSpec.stdin === undefined ? [] : ["<stdin-prompt>"]),
|
|
312
351
|
],
|
|
313
352
|
promptPreview: preview(input.prompt),
|
|
@@ -328,6 +367,10 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
328
367
|
isolatedCwd,
|
|
329
368
|
extractUsage: adapter.extractUsage,
|
|
330
369
|
failureReason: adapter.failureReason,
|
|
370
|
+
spawnInput,
|
|
371
|
+
stdinText: commandSpec.stdin,
|
|
372
|
+
startupConflict: adapter.startupConflict,
|
|
373
|
+
attemptStartedAtMs: Date.now(),
|
|
331
374
|
};
|
|
332
375
|
|
|
333
376
|
task.process = child;
|
|
@@ -339,7 +382,7 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
339
382
|
if (previous) previous.continuationTaskId = task.id;
|
|
340
383
|
this.pruneTasks(task.id);
|
|
341
384
|
this.pushEvent(task, { type: "started", message: `Task started. Execution limit: ${timeoutMs}ms (${task.timeoutSource}); deadline: ${task.deadlineAt}. This is not a completion estimate.` });
|
|
342
|
-
if (
|
|
385
|
+
if (guidance) {
|
|
343
386
|
this.pushEvent(task, {
|
|
344
387
|
type: "input-guidance",
|
|
345
388
|
message: "Media input guidance was included in the worker prompt. Native inspection remains unverified until the worker reports evidence.",
|
|
@@ -366,24 +409,7 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
366
409
|
|
|
367
410
|
task.settled = this.watchExit(task, child);
|
|
368
411
|
void task.settled;
|
|
369
|
-
|
|
370
|
-
if (commandSpec.stdin !== undefined) {
|
|
371
|
-
try {
|
|
372
|
-
const childStdin = child.stdin;
|
|
373
|
-
if (!childStdin) {
|
|
374
|
-
throw new Error("Child stdin pipe was not available.");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
childStdin.write(commandSpec.stdin);
|
|
378
|
-
childStdin.end();
|
|
379
|
-
} catch (error) {
|
|
380
|
-
this.pushEvent(task, {
|
|
381
|
-
type: "error",
|
|
382
|
-
message: error instanceof Error ? error.message : String(error),
|
|
383
|
-
});
|
|
384
|
-
this.terminate(task);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
412
|
+
this.writeStdin(task, child);
|
|
387
413
|
|
|
388
414
|
return { task: this.toView(task) };
|
|
389
415
|
}
|
|
@@ -495,9 +521,16 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
495
521
|
): Promise<void> {
|
|
496
522
|
let terminalStatus: TaskStatus = "failed";
|
|
497
523
|
try {
|
|
498
|
-
|
|
524
|
+
let exitCode = await child.exited;
|
|
499
525
|
if (task.killTimeout) this.signal(task, "SIGKILL");
|
|
500
526
|
await this.waitForOutputDrain(task);
|
|
527
|
+
while (exitCode !== 0 && this.metStartupConflict(task)) {
|
|
528
|
+
const restarted = await this.restartAfterConflict(task);
|
|
529
|
+
if (!restarted) break;
|
|
530
|
+
exitCode = await restarted.exited;
|
|
531
|
+
if (task.killTimeout) this.signal(task, "SIGKILL");
|
|
532
|
+
await this.waitForOutputDrain(task);
|
|
533
|
+
}
|
|
501
534
|
task.exitCode = exitCode;
|
|
502
535
|
|
|
503
536
|
if (task.cancelRequested) {
|
|
@@ -553,6 +586,80 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
553
586
|
}
|
|
554
587
|
}
|
|
555
588
|
|
|
589
|
+
private writeStdin(task: InternalTask, child: TaskProcess): void {
|
|
590
|
+
if (task.stdinText === undefined) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
const childStdin = child.stdin;
|
|
596
|
+
if (!childStdin) {
|
|
597
|
+
throw new Error("Child stdin pipe was not available.");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
childStdin.write(task.stdinText);
|
|
601
|
+
childStdin.end();
|
|
602
|
+
} catch (error) {
|
|
603
|
+
this.pushEvent(task, {
|
|
604
|
+
type: "error",
|
|
605
|
+
message: error instanceof Error ? error.message : String(error),
|
|
606
|
+
});
|
|
607
|
+
this.terminate(task);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** A start that lost a race for the CLI's shared local state did no work,
|
|
612
|
+
* so running the same command again is safe. */
|
|
613
|
+
private metStartupConflict(task: InternalTask): boolean {
|
|
614
|
+
return Boolean(task.startupConflict?.test(task.stderr)) &&
|
|
615
|
+
!task.stdout.trim() &&
|
|
616
|
+
!task.cancelRequested && !task.timedOut && !task.drainTimedOut && !this.shuttingDown &&
|
|
617
|
+
(task.startupRestarts ?? 0) < MAX_STARTUP_RESTARTS &&
|
|
618
|
+
Date.now() - task.attemptStartedAtMs < STARTUP_CONFLICT_WINDOW_MS;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private async restartAfterConflict(task: InternalTask): Promise<TaskProcess | undefined> {
|
|
622
|
+
const restart = (task.startupRestarts ?? 0) + 1;
|
|
623
|
+
// Jitter spreads out tasks that met the same conflict together.
|
|
624
|
+
const delayMs = Math.round(this.startupRestartDelayMs * restart * (1 + Math.random()));
|
|
625
|
+
this.pushEvent(task, {
|
|
626
|
+
type: "restart",
|
|
627
|
+
message: `The CLI stopped during startup because another instance held shared local state. Restart ${restart} of ${MAX_STARTUP_RESTARTS} in ${delayMs}ms. stderr: ${tail(task.stderr, 500).trim()}`,
|
|
628
|
+
});
|
|
629
|
+
await Bun.sleep(delayMs);
|
|
630
|
+
if (task.cancelRequested || task.timedOut || this.shuttingDown) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Clear anything the failed start left in its process group.
|
|
635
|
+
this.signal(task, "SIGKILL");
|
|
636
|
+
let child: TaskProcess;
|
|
637
|
+
try {
|
|
638
|
+
child = this.spawn(task.spawnInput);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
this.pushEvent(task, {
|
|
641
|
+
type: "error",
|
|
642
|
+
message: `Restart failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
643
|
+
});
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
task.forceSignalled = false;
|
|
648
|
+
task.startupRestarts = restart;
|
|
649
|
+
task.stdout = "";
|
|
650
|
+
task.stderr = "";
|
|
651
|
+
task.streamCaptureTruncated = false;
|
|
652
|
+
task.process = child;
|
|
653
|
+
task.pid = child.pid;
|
|
654
|
+
task.attemptStartedAtMs = Date.now();
|
|
655
|
+
task.streamsDone = Promise.all([
|
|
656
|
+
this.pipeStreamSafely(task, "stdout", child.stdout),
|
|
657
|
+
this.pipeStreamSafely(task, "stderr", child.stderr),
|
|
658
|
+
]).then(() => undefined);
|
|
659
|
+
this.writeStdin(task, child);
|
|
660
|
+
return child;
|
|
661
|
+
}
|
|
662
|
+
|
|
556
663
|
private async collectFinalMessage(task: InternalTask): Promise<void> {
|
|
557
664
|
if (!task.finalMessagePath) {
|
|
558
665
|
return;
|
|
@@ -865,6 +972,7 @@ Use only tools and permissions actually available in this session; the caller's
|
|
|
865
972
|
task.stderr.length > (includeOutput ? maxOutputChars : 0)),
|
|
866
973
|
usage: task.usage,
|
|
867
974
|
isolatedFrom: task.isolatedFrom,
|
|
975
|
+
startupRestarts: task.startupRestarts || undefined,
|
|
868
976
|
};
|
|
869
977
|
}
|
|
870
978
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ENNODIA_VERSION = "0.
|
|
1
|
+
export const ENNODIA_VERSION = "0.4.0";
|
package/bench/README.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Benchmarks
|
|
2
|
-
|
|
3
|
-
Ennodia's first release-candidate benchmark is `multi-model-bug-recall`: small
|
|
4
|
-
TypeScript review fixtures with committed oracles. It measures if
|
|
5
|
-
parallel peer review plus Compare recovers more required findings than a solo
|
|
6
|
-
harness answer.
|
|
7
|
-
|
|
8
|
-
The default command is deterministic and uses committed reference outputs:
|
|
9
|
-
|
|
10
|
-
```sh
|
|
11
|
-
bun run bench:bug-recall
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
The live command starts a fresh Ennodia Model Context Protocol (MCP) server and
|
|
15
|
-
runs real harnesses. It is intentionally not part of `bun run verify`.
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
bun run bench:bug-recall:live -- --fixture 001-missing-await
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Useful flags:
|
|
22
|
-
|
|
23
|
-
- `--fixture <id>` runs one fixture. Repeat it to select multiple fixtures.
|
|
24
|
-
- `--harness <id>` adds a solo baseline harness in live mode. Defaults to
|
|
25
|
-
`codex` and `claude-code`.
|
|
26
|
-
- `--judge-harness <id>` and `--advisor-harness <id>` select Compare roles.
|
|
27
|
-
- `--synthesizer-harness <id>` is a deprecated alias for
|
|
28
|
-
`--advisor-harness <id>`.
|
|
29
|
-
- `--timeout-ms <ms>` sets the per-run timeout for live harness calls.
|
|
30
|
-
- `--out <dir>` writes results to a specific directory.
|
|
31
|
-
|
|
32
|
-
Results are written under `bench/results/bug-recall/`. Raw benchmark output is
|
|
33
|
-
gitignored by default because live runs include model output and environment
|
|
34
|
-
metadata.
|