aos-harness 0.8.3 → 0.8.5

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.
@@ -58,7 +58,7 @@ constraints:
58
58
  max: 8
59
59
 
60
60
  error_handling:
61
- agent_timeout_seconds: 120
61
+ agent_timeout_seconds: 3600 # Strategic deliberations can legitimately run 2-60 minutes per agent turn
62
62
  retry_policy:
63
63
  max_retries: 2
64
64
  backoff: exponential
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aos-harness",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Agentic Orchestration System — assemble AI agents into deliberation and execution teams",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -38,18 +38,18 @@
38
38
  "test": "bun run src/index.ts validate"
39
39
  },
40
40
  "dependencies": {
41
- "@aos-harness/adapter-shared": "0.8.3",
42
- "@aos-harness/runtime": "0.8.3",
41
+ "@aos-harness/adapter-shared": "0.8.5",
42
+ "@aos-harness/runtime": "0.8.5",
43
43
  "@clack/prompts": "^1.2.0",
44
44
  "@modelcontextprotocol/sdk": "^1.29.0",
45
45
  "js-yaml": "^4.1.0",
46
46
  "yaml": "^2.8.3"
47
47
  },
48
48
  "peerDependencies": {
49
- "@aos-harness/claude-code-adapter": ">=0.8.3 <1.0.0",
50
- "@aos-harness/codex-adapter": ">=0.8.3 <1.0.0",
51
- "@aos-harness/gemini-adapter": ">=0.8.3 <1.0.0",
52
- "@aos-harness/pi-adapter": ">=0.8.3 <1.0.0"
49
+ "@aos-harness/claude-code-adapter": ">=0.8.5 <1.0.0",
50
+ "@aos-harness/codex-adapter": ">=0.8.5 <1.0.0",
51
+ "@aos-harness/gemini-adapter": ">=0.8.5 <1.0.0",
52
+ "@aos-harness/pi-adapter": ">=0.8.5 <1.0.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@aos-harness/claude-code-adapter": {
@@ -65,6 +65,37 @@ export interface AdapterSessionConfig {
65
65
  * also batched to `${platformUrl}/api/sessions/:id/events`.
66
66
  */
67
67
  platformUrl?: string;
68
+ agentTimeoutMs?: number;
69
+ }
70
+
71
+ function createStreamingPrinter() {
72
+ let printedLength = 0;
73
+ let printedAny = false;
74
+
75
+ return {
76
+ push(partial: string) {
77
+ if (partial.length <= printedLength) return;
78
+ const delta = partial.slice(printedLength);
79
+ printedLength = partial.length;
80
+ if (!delta) return;
81
+ printedAny = true;
82
+ process.stdout.write(delta);
83
+ },
84
+ flushFinal(text: string): boolean {
85
+ if (!printedAny) return false;
86
+ if (text.length > printedLength) {
87
+ process.stdout.write(text.slice(printedLength));
88
+ printedLength = text.length;
89
+ }
90
+ if (!text.endsWith("\n")) {
91
+ process.stdout.write("\n");
92
+ }
93
+ return true;
94
+ },
95
+ hasOutput() {
96
+ return printedAny;
97
+ },
98
+ };
68
99
  }
69
100
 
70
101
  const ADAPTER_MAP: Record<string, { package: string; className: string }> = {
@@ -479,16 +510,34 @@ export async function runAdapterSession(config: AdapterSessionConfig): Promise<v
479
510
  `Tool calls will stream as rounds complete. ` +
480
511
  `Bridge socket: ${sockPath}\n`,
481
512
  );
482
- const response = await adapter.sendMessage(arbiterHandle, kickoff, {
483
- extraArgs: mcpArgs,
484
- });
513
+ const printer = createStreamingPrinter();
514
+ const startedAt = Date.now();
515
+ const heartbeat = setInterval(() => {
516
+ if (printer.hasOutput()) return;
517
+ const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
518
+ console.error(
519
+ `[arbiter] waiting for ${config.platform} response... ${elapsedSeconds}s elapsed`,
520
+ );
521
+ }, 15000);
522
+ let response;
523
+ try {
524
+ response = await adapter.sendMessage(arbiterHandle, kickoff, {
525
+ extraArgs: mcpArgs,
526
+ onStream: (partial) => printer.push(partial),
527
+ timeoutMs: config.agentTimeoutMs,
528
+ });
529
+ } finally {
530
+ clearInterval(heartbeat);
531
+ }
485
532
  if ((response as any).status !== "success") {
486
533
  console.error(
487
534
  `\n[arbiter] call failed: status=${(response as any).status} ` +
488
535
  `error=${(response as any).error ?? "(none)"}`,
489
536
  );
490
537
  }
491
- console.log("\n" + response.text);
538
+ if (!printer.flushFinal(response.text)) {
539
+ console.log("\n" + response.text);
540
+ }
492
541
  } finally {
493
542
  rl.close();
494
543
  await closeBridge();
@@ -2,7 +2,7 @@
2
2
  * aos run — Launch a deliberation or execution session.
3
3
  */
4
4
 
5
- import { existsSync, readdirSync, mkdirSync } from "node:fs";
5
+ import { existsSync, readdirSync, mkdirSync, readFileSync } from "node:fs";
6
6
  import { join, resolve, basename } from "node:path";
7
7
  import { c, type ParsedArgs } from "../colors";
8
8
  import { getHarnessRoot, discoverDirs, promptSelect, getAdapterDir, ADAPTER_ALLOWLIST, isValidAdapter, validatePlatformUrl, parseAllowCodeExecutionFlag } from "../utils";
@@ -10,6 +10,7 @@ import { runAdapterSession } from "../adapter-session";
10
10
  import { buildToolPolicy, type ToolPolicy } from "@aos-harness/adapter-shared";
11
11
  import { getPlatformUrlFromConfig, getRuntimeAdapterModelConfig, resolveAdapterSelection } from "../aos-config";
12
12
  import { backfillAdapterDefaults } from "../config-migration";
13
+ import { ADAPTER_METADATA, scanEnvironment } from "../env-scanner";
13
14
 
14
15
  const HELP = `
15
16
  ${c.bold("aos run")} — Run a deliberation or execution session
@@ -49,6 +50,63 @@ ${c.bold("EXAMPLES")}
49
50
  aos run # interactive profile selection
50
51
  `;
51
52
 
53
+ function readCliVersion(): string {
54
+ try {
55
+ const raw = readFileSync(join(import.meta.dir, "..", "..", "package.json"), "utf-8");
56
+ return (JSON.parse(raw) as { version?: string }).version ?? "unknown";
57
+ } catch {
58
+ return "unknown";
59
+ }
60
+ }
61
+
62
+ function versionMismatchSeverity(cliVer: string, adapterVer?: string): "none" | "warn" {
63
+ if (!adapterVer) return "none";
64
+ const [cliMaj, cliMin] = cliVer.split(".").map(Number);
65
+ const [adaMaj, adaMin] = adapterVer.split(".").map(Number);
66
+ if (Number.isNaN(cliMaj) || Number.isNaN(adaMaj)) return "none";
67
+ if (cliMaj !== adaMaj) return "warn";
68
+ if (cliMin !== adaMin) return "warn";
69
+ return "none";
70
+ }
71
+
72
+ async function probeClaudeExternalApiKey(): Promise<{ ok: boolean; hint?: string }> {
73
+ const proc = Bun.spawn(["claude", "--print", "Reply with OK."], {
74
+ stdin: "ignore",
75
+ stdout: "pipe",
76
+ stderr: "pipe",
77
+ });
78
+
79
+ let timedOut = false;
80
+ const timeout = setTimeout(() => {
81
+ timedOut = true;
82
+ proc.kill();
83
+ }, 15000);
84
+
85
+ try {
86
+ const [stdout, stderr, exitCode] = await Promise.all([
87
+ new Response(proc.stdout).text(),
88
+ new Response(proc.stderr).text(),
89
+ proc.exited,
90
+ ]);
91
+ const combined = `${stdout}\n${stderr}`.trim();
92
+ if (timedOut) {
93
+ return { ok: true, hint: "Claude auth probe timed out; continuing without blocking launch." };
94
+ }
95
+ if (exitCode === 0) {
96
+ return { ok: true };
97
+ }
98
+ if (/invalid api key|fix external api key|api key/i.test(combined)) {
99
+ return {
100
+ ok: false,
101
+ hint: "Claude Code external API-key auth failed. Unset or refresh ANTHROPIC_API_KEY, or switch back to `claude login` auth before running AOS.",
102
+ };
103
+ }
104
+ return { ok: true, hint: combined ? `Claude auth probe returned: ${combined.split("\n")[0]}` : undefined };
105
+ } finally {
106
+ clearTimeout(timeout);
107
+ }
108
+ }
109
+
52
110
  export async function runCommand(args: ParsedArgs): Promise<void> {
53
111
  if (args.flags.help) {
54
112
  console.log(HELP);
@@ -224,7 +282,6 @@ export async function runCommand(args: ParsedArgs): Promise<void> {
224
282
 
225
283
  // ── Dry-run mode ─────────────────────────────────────────────
226
284
  if (args.flags["dry-run"]) {
227
- const { readFileSync } = await import("node:fs");
228
285
  const briefContent = readFileSync(briefPath, "utf-8");
229
286
  const briefSections = briefContent.match(/^##\s+.+/gm) || [];
230
287
 
@@ -301,22 +358,6 @@ ${c.green("All configuration validated successfully. Ready to launch.")}
301
358
  process.exit(0);
302
359
  }
303
360
 
304
- // ── Set up deliberation directory for artifact storage ──────
305
- const sessionId = `${new Date().toISOString().slice(0, 10)}-${profileName}-${Date.now().toString(36)}`;
306
- const deliberationDir = join(root, ".aos", "sessions", sessionId);
307
- mkdirSync(deliberationDir, { recursive: true });
308
-
309
- // ── Launch adapter ───────────────────────────────────────────
310
- const sessionType = isExecutionProfile ? "Execution" : "Deliberation";
311
- console.log(`
312
- ${c.bold(`AOS ${sessionType} Session`)}
313
- Profile: ${c.cyan(profileName!)}
314
- Type: ${isExecutionProfile ? c.magenta("execution") : c.cyan("deliberation")}${isExecutionProfile && workflowConfig ? `\n Workflow: ${c.magenta(workflowConfig.id)} (${workflowConfig.steps.length} steps)` : ""}
315
- Domain: ${c.cyan(domainName || "none")}
316
- Brief: ${c.cyan(briefPath)}
317
- Output: ${c.cyan(deliberationDir)}
318
- `);
319
-
320
361
  // Determine adapter with shared precedence so init/run agree:
321
362
  // --adapter > config v2 > config v1 > .aos/adapter.yaml > default "pi"
322
363
  let platformUrl = (args.flags["platform-url"] as string) || null;
@@ -342,12 +383,69 @@ ${c.bold(`AOS ${sessionType} Session`)}
342
383
  process.exit(2);
343
384
  }
344
385
 
386
+ if (adapter !== "pi") {
387
+ const scan = await scanEnvironment({ cwd: process.cwd() });
388
+ const readiness = scan.adapters[adapter];
389
+ const meta = ADAPTER_METADATA[adapter];
390
+
391
+ if (readiness.status !== "ready") {
392
+ console.error(c.red(`${meta.label} is not ready for \`aos run\`.`));
393
+ console.error(c.red(` ${readiness.statusHint}`));
394
+ if (scan.notes.length > 0) {
395
+ for (const note of scan.notes.filter((entry) => entry.startsWith(`${adapter}:`))) {
396
+ console.error(c.dim(` ${note}`));
397
+ }
398
+ }
399
+ process.exit(2);
400
+ }
401
+
402
+ const cliVersion = readCliVersion();
403
+ if (versionMismatchSeverity(cliVersion, readiness.aosAdapter.version) === "warn") {
404
+ const installHint = readiness.aosAdapter.store === "bun"
405
+ ? `bun add -g ${meta.packageName}@${cliVersion}`
406
+ : `npm i -g ${meta.packageName}@${cliVersion}`;
407
+ console.error(c.yellow(`⚠ Version mismatch before launch: aos-harness@${cliVersion} and ${meta.packageName}@${readiness.aosAdapter.version}`));
408
+ console.error(c.dim(` Install matching versions: ${installHint}`));
409
+ }
410
+
411
+ if (readiness.statusHint.includes("ANTHROPIC_API_KEY")) {
412
+ console.error(c.yellow(`⚠ ${readiness.statusHint}`));
413
+ if (adapter === "claude-code") {
414
+ const authProbe = await probeClaudeExternalApiKey();
415
+ if (!authProbe.ok) {
416
+ console.error(c.red(`Claude Code auth preflight failed.`));
417
+ console.error(c.red(` ${authProbe.hint}`));
418
+ process.exit(2);
419
+ }
420
+ if (authProbe.hint) {
421
+ console.error(c.dim(` ${authProbe.hint}`));
422
+ }
423
+ }
424
+ }
425
+ }
426
+
345
427
  const adapterName = adapter;
346
428
  // Resolve from monorepo dev layout (CLI's own import.meta.dir) or installed
347
429
  // @aos-harness/<name>-adapter. Project-local override is intentionally absent
348
430
  // (spec D1 — workspace-trust hardening).
349
431
  const resolvedAdapterDir = getAdapterDir(adapterName);
350
432
 
433
+ // ── Set up deliberation directory for artifact storage ──────
434
+ const sessionId = `${new Date().toISOString().slice(0, 10)}-${profileName}-${Date.now().toString(36)}`;
435
+ const deliberationDir = join(root, ".aos", "sessions", sessionId);
436
+ mkdirSync(deliberationDir, { recursive: true });
437
+
438
+ // ── Launch adapter ───────────────────────────────────────────
439
+ const sessionType = isExecutionProfile ? "Execution" : "Deliberation";
440
+ console.log(`
441
+ ${c.bold(`AOS ${sessionType} Session`)}
442
+ Profile: ${c.cyan(profileName!)}
443
+ Type: ${isExecutionProfile ? c.magenta("execution") : c.cyan("deliberation")}${isExecutionProfile && workflowConfig ? `\n Workflow: ${c.magenta(workflowConfig.id)} (${workflowConfig.steps.length} steps)` : ""}
444
+ Domain: ${c.cyan(domainName || "none")}
445
+ Brief: ${c.cyan(briefPath)}
446
+ Output: ${c.cyan(deliberationDir)}
447
+ `);
448
+
351
449
  if (adapter === "pi") {
352
450
  const adapterEntry = resolvedAdapterDir ? join(resolvedAdapterDir, "src", "index.ts") : null;
353
451
  if (!adapterEntry || !existsSync(adapterEntry)) {
@@ -421,6 +519,10 @@ ${c.bold(`AOS ${sessionType} Session`)}
421
519
  useVendorDefaultModel: runtimeModelConfig.useVendorDefaultModel,
422
520
  toolPolicy,
423
521
  platformUrl: platformUrl ?? undefined,
522
+ agentTimeoutMs:
523
+ typeof profile.error_handling?.agent_timeout_seconds === "number"
524
+ ? profile.error_handling.agent_timeout_seconds * 1000
525
+ : undefined,
424
526
  });
425
527
  }
426
528
  }
@@ -210,26 +210,42 @@ function scanAdapterPackage(
210
210
  const bunPath = bunGlobalDir ? join(bunGlobalDir, meta.packageName) : null;
211
211
  const npmPath = npmGlobalDir ? join(npmGlobalDir, meta.packageName) : null;
212
212
  const projectLocalPath = join(cwd, "node_modules", meta.packageName);
213
+ const resolvedFrom = resolveAdapterDir(adapter) ?? undefined;
214
+
215
+ if (resolvedFrom) {
216
+ let store: AosAdapterReadiness["store"] = "unknown";
217
+ if (resolvedFrom.includes("/adapters/")) {
218
+ store = "workspace";
219
+ } else if (bunPath && resolvedFrom.startsWith(bunPath)) {
220
+ store = "bun";
221
+ } else if (npmPath && resolvedFrom.startsWith(npmPath)) {
222
+ store = "npm";
223
+ }
224
+
225
+ return {
226
+ installed: true,
227
+ version: readPackageVersion(resolvedFrom),
228
+ store,
229
+ loadable: true,
230
+ resolvedFrom,
231
+ };
232
+ }
213
233
 
214
234
  if (bunPath && existsSync(bunPath)) {
215
- const resolvedFrom = resolveAdapterDir(adapter) ?? undefined;
216
235
  return {
217
236
  installed: true,
218
237
  version: readPackageVersion(bunPath),
219
238
  store: "bun",
220
- loadable: !!resolvedFrom,
221
- resolvedFrom,
239
+ loadable: false,
222
240
  };
223
241
  }
224
242
 
225
243
  if (npmPath && existsSync(npmPath)) {
226
- const resolvedFrom = resolveAdapterDir(adapter) ?? undefined;
227
244
  return {
228
245
  installed: true,
229
246
  version: readPackageVersion(npmPath),
230
247
  store: "npm",
231
- loadable: !!resolvedFrom,
232
- resolvedFrom,
248
+ loadable: false,
233
249
  };
234
250
  }
235
251
 
@@ -242,17 +258,6 @@ function scanAdapterPackage(
242
258
  };
243
259
  }
244
260
 
245
- const resolvedFrom = resolveAdapterDir(adapter) ?? undefined;
246
- if (resolvedFrom) {
247
- return {
248
- installed: true,
249
- version: readPackageVersion(resolvedFrom),
250
- store: resolvedFrom.includes("/adapters/") ? "workspace" : "unknown",
251
- loadable: true,
252
- resolvedFrom,
253
- };
254
- }
255
-
256
261
  return {
257
262
  installed: false,
258
263
  store: "unknown",