aos-harness 0.5.2 → 0.7.0-rc.2

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 (45) hide show
  1. package/README.md +25 -6
  2. package/package.json +17 -5
  3. package/src/adapter-config.ts +1 -1
  4. package/src/adapter-session.ts +84 -42
  5. package/src/colors.ts +10 -1
  6. package/src/commands/create.ts +5 -0
  7. package/src/commands/init.ts +31 -8
  8. package/src/commands/replay.ts +2 -0
  9. package/src/commands/run.ts +76 -9
  10. package/src/index.ts +0 -0
  11. package/src/utils.ts +135 -10
  12. package/adapters/claude-code/README.md +0 -74
  13. package/adapters/claude-code/package.json +0 -26
  14. package/adapters/claude-code/src/agent-runtime.ts +0 -210
  15. package/adapters/claude-code/src/index.ts +0 -2
  16. package/adapters/claude-code/tsconfig.json +0 -19
  17. package/adapters/codex/README.md +0 -14
  18. package/adapters/codex/package.json +0 -26
  19. package/adapters/codex/src/agent-runtime.ts +0 -219
  20. package/adapters/codex/src/index.ts +0 -2
  21. package/adapters/codex/tsconfig.json +0 -20
  22. package/adapters/gemini/README.md +0 -39
  23. package/adapters/gemini/package.json +0 -26
  24. package/adapters/gemini/src/agent-runtime.ts +0 -245
  25. package/adapters/gemini/src/index.ts +0 -2
  26. package/adapters/gemini/tsconfig.json +0 -19
  27. package/adapters/pi/README.md +0 -53
  28. package/adapters/pi/arbiter-scratchpad.md +0 -21
  29. package/adapters/pi/package.json +0 -36
  30. package/adapters/pi/src/agent-runtime.ts +0 -183
  31. package/adapters/pi/src/event-bus.ts +0 -41
  32. package/adapters/pi/src/index.ts +0 -885
  33. package/adapters/pi/src/ui.ts +0 -242
  34. package/adapters/pi/tsconfig.json +0 -18
  35. package/adapters/shared/README.md +0 -15
  36. package/adapters/shared/package.json +0 -30
  37. package/adapters/shared/src/agent-discovery.ts +0 -71
  38. package/adapters/shared/src/base-agent-runtime.ts +0 -331
  39. package/adapters/shared/src/base-event-bus.ts +0 -133
  40. package/adapters/shared/src/base-workflow.ts +0 -392
  41. package/adapters/shared/src/compose.ts +0 -76
  42. package/adapters/shared/src/index.ts +0 -12
  43. package/adapters/shared/src/terminal-ui.ts +0 -140
  44. package/adapters/shared/src/types.ts +0 -43
  45. package/adapters/shared/tsconfig.json +0 -18
package/README.md CHANGED
@@ -2,26 +2,36 @@
2
2
 
3
3
  **Agentic Orchestration System** — Assemble specialized AI agents into deliberation and execution teams.
4
4
 
5
+ > **Breaking change in 0.6.0:** `aos-harness` no longer bundles adapter code. You must install the adapter(s) for the AI CLI(s) you want to use as separate packages. If you upgrade from 0.5.x and run `aos run` without the matching `@aos-harness/<name>-adapter` installed, the CLI will print an install hint and exit. See [CHANGELOG](../CHANGELOG.md#060) for the full migration note.
6
+
5
7
  ## Prerequisites
6
8
 
7
9
  - [Bun](https://bun.sh) 1.0+
8
10
 
9
- ## Install
11
+ ## Getting Started
12
+
13
+ ### 1. Install the CLI
10
14
 
11
15
  ```bash
12
- bun add -g aos-harness
16
+ npm i -g aos-harness
17
+ # or: bun add -g aos-harness
13
18
  ```
14
19
 
15
- Or run directly:
20
+ ### 2. Install an adapter
21
+
22
+ Pick the AI CLI you'll drive agents with and install the matching adapter. You can install more than one. Versions are lockstep — pin the adapter to the same version as the CLI.
16
23
 
17
24
  ```bash
18
- bunx aos-harness init
25
+ npm i -g @aos-harness/claude-code-adapter # Anthropic's Claude Code
26
+ npm i -g @aos-harness/gemini-adapter # Google's Gemini CLI
27
+ npm i -g @aos-harness/codex-adapter # OpenAI's Codex CLI
28
+ npm i -g @aos-harness/pi-adapter # Pi (pi-ai) — direct model SDK
19
29
  ```
20
30
 
21
- ## Quick Start
31
+ ### 3. Initialize and run
22
32
 
23
33
  ```bash
24
- # Initialize a project
34
+ # Initialize a project (writes .aos/ and copies core/ into the project)
25
35
  aos init
26
36
 
27
37
  # Run a strategic deliberation
@@ -41,6 +51,15 @@ aos create profile my-review
41
51
  aos validate
42
52
  ```
43
53
 
54
+ ## Exit Codes
55
+
56
+ | Code | Meaning |
57
+ |---|---|
58
+ | 0 | Success |
59
+ | 1 | Uncaught runtime error |
60
+ | 2 | Invalid input (unknown adapter, bad path, bad URL, missing adapter package) |
61
+ | 3 | Profile tool-policy error (malformed `tools:` block, flag cannot widen profile) |
62
+
44
63
  ## What It Does
45
64
 
46
65
  AOS Harness orchestrates multiple AI agents with distinct cognitive biases into structured deliberation and execution sessions:
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "aos-harness",
3
- "version": "0.5.2",
3
+ "version": "0.7.0-rc.2",
4
4
  "description": "Agentic Orchestration System — assemble AI agents into deliberation and execution teams",
5
5
  "license": "MIT",
6
+ "publishConfig": { "access": "public" },
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/aos-engineer/aos-harness.git"
@@ -23,12 +24,11 @@
23
24
  "aos": "./src/index.ts"
24
25
  },
25
26
  "engines": {
26
- "bun": ">=1.0.0"
27
+ "bun": ">=1.3.11"
27
28
  },
28
29
  "files": [
29
30
  "src/",
30
31
  "core/",
31
- "adapters/",
32
32
  "README.md"
33
33
  ],
34
34
  "scripts": {
@@ -36,11 +36,23 @@
36
36
  "test": "bun run src/index.ts validate"
37
37
  },
38
38
  "dependencies": {
39
- "@aos-harness/adapter-shared": "0.5.2",
40
- "@aos-harness/runtime": "0.5.2",
39
+ "@aos-harness/adapter-shared": "0.7.0-rc.2",
40
+ "@aos-harness/runtime": "0.7.0-rc.2",
41
41
  "@modelcontextprotocol/sdk": "^1.29.0",
42
42
  "js-yaml": "^4.1.0"
43
43
  },
44
+ "peerDependencies": {
45
+ "@aos-harness/claude-code-adapter": ">=0.6.0 <1.0.0",
46
+ "@aos-harness/codex-adapter": ">=0.6.0 <1.0.0",
47
+ "@aos-harness/gemini-adapter": ">=0.6.0 <1.0.0",
48
+ "@aos-harness/pi-adapter": ">=0.6.0 <1.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@aos-harness/claude-code-adapter": { "optional": true },
52
+ "@aos-harness/codex-adapter": { "optional": true },
53
+ "@aos-harness/gemini-adapter": { "optional": true },
54
+ "@aos-harness/pi-adapter": { "optional": true }
55
+ },
44
56
  "devDependencies": {
45
57
  "@types/js-yaml": "^4.0.9",
46
58
  "typescript": "^5.4.0"
@@ -12,5 +12,5 @@ export interface AdapterConfig {
12
12
  export function readAdapterConfig(root: string): AdapterConfig | null {
13
13
  const p = join(root, ".aos", "adapter.yaml");
14
14
  if (!existsSync(p)) return null;
15
- return yaml.load(readFileSync(p, "utf-8")) as AdapterConfig;
15
+ return yaml.load(readFileSync(p, "utf-8"), { schema: yaml.JSON_SCHEMA }) as AdapterConfig;
16
16
  }
@@ -15,7 +15,7 @@
15
15
  import { join, dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { tmpdir } from "node:os";
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { readFileSync } from "node:fs";
19
19
  import readline from "node:readline";
20
20
  import {
21
21
  BaseEventBus,
@@ -24,6 +24,7 @@ import {
24
24
  composeAdapter,
25
25
  discoverAgents,
26
26
  createFlatAgentsDir,
27
+ type ToolPolicy,
27
28
  } from "@aos-harness/adapter-shared";
28
29
  import { AOSEngine } from "@aos-harness/runtime";
29
30
  import { loadAgent } from "@aos-harness/runtime/config-loader";
@@ -43,6 +44,18 @@ export interface AdapterSessionConfig {
43
44
  workflowConfig: any | null;
44
45
  workflowsDir: string;
45
46
  modelOverrides?: Partial<Record<string, string>>;
47
+ /**
48
+ * Tool policy resolved by the CLI from the profile's `tools:` block narrowed
49
+ * (optionally) by the `--allow-code-execution` flag. Passed straight into
50
+ * BaseWorkflow to gate tool access during the session (spec D3).
51
+ */
52
+ toolPolicy?: ToolPolicy;
53
+ /**
54
+ * Path where BaseWorkflow should append tool-decision audit events
55
+ * (one JSON object per line). Defaults to `<deliberationDir>/transcript.jsonl`
56
+ * when omitted.
57
+ */
58
+ transcriptPath?: string;
46
59
  }
47
60
 
48
61
  const ADAPTER_MAP: Record<string, { package: string; className: string }> = {
@@ -60,8 +73,8 @@ const ADAPTER_MAP: Record<string, { package: string; className: string }> = {
60
73
  },
61
74
  };
62
75
 
63
- // CLI version read once at module load, used in the 0.6.0 deprecation
64
- // warning so the suggested install command pins to the matching version.
76
+ // CLI version read once at module load, used in the missing-adapter
77
+ // install hint and the version-mismatch warning.
65
78
  function readCliVersion(): string {
66
79
  try {
67
80
  const here = dirname(fileURLToPath(import.meta.url));
@@ -73,39 +86,64 @@ function readCliVersion(): string {
73
86
  }
74
87
  const CLI_VERSION = readCliVersion();
75
88
 
76
- // Session-level dedup for the deprecation warning. If the process uses
77
- // multiple adapters, we still only warn once per run.
78
- let deprecationWarnedThisSession = false;
89
+ // Classify an error from a dynamic import() as "package not installed".
90
+ // Bun 1.3.11 throws ResolveMessage with code=ERR_MODULE_NOT_FOUND. Older
91
+ // Bun/Node and edge cases are caught by constructor-name and message-regex
92
+ // fallbacks. Anything that doesn't match these patterns is a real error
93
+ // (syntax error, missing transitive dep, etc.) and must surface with its
94
+ // original stack — never swallowed as "not installed".
95
+ function isModuleNotFound(err: any): boolean {
96
+ if (err?.code === "ERR_MODULE_NOT_FOUND") return true;
97
+ if (err?.code === "MODULE_NOT_FOUND") return true;
98
+ if (err?.constructor?.name === "ResolveMessage") return true;
99
+ const msg = typeof err?.message === "string" ? err.message : "";
100
+ return /Cannot find (module|package)/i.test(msg);
101
+ }
79
102
 
80
- function maybeWarnAdapterDeprecation(pkg: string, projectRoot: string): void {
81
- if (deprecationWarnedThisSession) return;
82
- const flagPath = join(projectRoot, ".aos", "migration-warned-0.6");
83
- if (existsSync(flagPath)) return;
84
- deprecationWarnedThisSession = true;
103
+ function printMissingAdapterError(pkg: string): void {
104
+ const useColor = !!process.stderr.isTTY;
105
+ const red = useColor ? "\x1b[31m" : "";
106
+ const bold = useColor ? "\x1b[1m" : "";
107
+ const reset = useColor ? "\x1b[0m" : "";
108
+ console.error(
109
+ `\n${red}${bold}✗ Adapter not installed: ${pkg}${reset}\n\n` +
110
+ `Install it:\n` +
111
+ ` npm i -g ${pkg} # if aos-harness is installed globally\n` +
112
+ ` npm i ${pkg} # if aos-harness is a project dependency\n\n` +
113
+ `(or use bun / pnpm / yarn equivalents)\n\n` +
114
+ `CLI version: aos-harness@${CLI_VERSION}. Pin the adapter to the same version.\n`,
115
+ );
116
+ }
85
117
 
118
+ // Compare CLI and adapter versions. Under pre-1.0 lockstep, any minor or
119
+ // major drift is a warning — patch drift is silent (expected during
120
+ // quick-turnaround publishes).
121
+ function versionMismatchSeverity(cliVer: string, adapterVer: string): "none" | "warn" {
122
+ const [cliMaj, cliMin] = cliVer.split(".").map(Number);
123
+ const [adaMaj, adaMin] = adapterVer.split(".").map(Number);
124
+ if (Number.isNaN(cliMaj) || Number.isNaN(adaMaj)) return "none";
125
+ if (cliMaj !== adaMaj) return "warn";
126
+ if (cliMin !== adaMin) return "warn";
127
+ return "none";
128
+ }
129
+
130
+ const mismatchWarnedPackages = new Set<string>();
131
+
132
+ function maybeWarnVersionMismatch(pkg: string, adapterVer: string): void {
133
+ if (mismatchWarnedPackages.has(pkg)) return;
134
+ if (versionMismatchSeverity(CLI_VERSION, adapterVer) !== "warn") return;
135
+ mismatchWarnedPackages.add(pkg);
86
136
  const useColor = !!process.stderr.isTTY;
87
137
  const y = useColor ? "\x1b[33m" : "";
88
- const b = useColor ? "\x1b[1m" : "";
89
138
  const r = useColor ? "\x1b[0m" : "";
90
-
91
139
  console.error(
92
- `\n${y}${b}Deprecation: bundled adapters will be removed in aos-harness@0.6.0.${r}\n` +
93
- ` This project is using the bundled copy of ${pkg}.\n` +
94
- ` Install the standalone package to silence this warning and prepare for 0.6.0:\n\n` +
95
- ` npm i -g ${pkg}@${CLI_VERSION}\n` +
96
- ` # or in a project: npm i ${pkg}@${CLI_VERSION}\n\n` +
97
- ` This warning appears once per project. Delete .aos/migration-warned-0.6 to re-enable.\n`,
140
+ `\n${y}⚠ Version mismatch: aos-harness@${CLI_VERSION} and ${pkg}@${adapterVer}${r}\n` +
141
+ ` Adapters are published lockstep with the CLI. Install matching versions:\n` +
142
+ ` npm i -g ${pkg}@${CLI_VERSION}\n`,
98
143
  );
99
-
100
- try {
101
- mkdirSync(dirname(flagPath), { recursive: true });
102
- writeFileSync(flagPath, new Date().toISOString() + "\n");
103
- } catch {
104
- // non-fatal — if we can't write the flag, the warning just repeats next run.
105
- }
106
144
  }
107
145
 
108
- async function loadAdapterRuntime(platform: string, projectRoot: string): Promise<any> {
146
+ async function loadAdapterRuntime(platform: string): Promise<any> {
109
147
  const entry = ADAPTER_MAP[platform];
110
148
  if (!entry) throw new Error(`Unknown adapter: ${platform}`);
111
149
 
@@ -120,21 +158,21 @@ async function loadAdapterRuntime(platform: string, projectRoot: string): Promis
120
158
  }
121
159
  }
122
160
 
161
+ let mod: any;
123
162
  try {
124
- const mod = await import(entry.package);
125
- const resolved = (import.meta as any).resolve?.(entry.package) ?? entry.package;
126
- const version = await readAdapterVersion(resolved);
127
- console.error(`[adapter] loaded ${entry.package}@${version} (standalone)`);
128
- return mod[entry.className];
129
- } catch {
130
- const here = dirname(fileURLToPath(import.meta.url));
131
- const fallback = join(here, "..", "..", "adapters", platform, "src", "index.ts");
132
- const mod = await import(fallback);
133
- const version = await readAdapterVersion(`file://${fallback}`);
134
- console.error(`[adapter] loaded ${entry.package}@${version} (bundled: ${fallback})`);
135
- maybeWarnAdapterDeprecation(entry.package, projectRoot);
136
- return mod[entry.className];
163
+ mod = await import(entry.package);
164
+ } catch (err) {
165
+ if (isModuleNotFound(err)) {
166
+ printMissingAdapterError(entry.package);
167
+ process.exit(2);
168
+ }
169
+ throw err; // real load error — surface it
137
170
  }
171
+ const resolved = (import.meta as any).resolve?.(entry.package) ?? entry.package;
172
+ const version = await readAdapterVersion(resolved);
173
+ console.error(`[adapter] loaded ${entry.package}@${version}`);
174
+ maybeWarnVersionMismatch(entry.package, version);
175
+ return mod[entry.className];
138
176
  }
139
177
 
140
178
  function toolNamesForPlatform(platform: string): { delegate: string; end: string } {
@@ -150,13 +188,17 @@ export async function runAdapterSession(config: AdapterSessionConfig): Promise<v
150
188
  };
151
189
 
152
190
  log("loading adapter runtime");
153
- const RuntimeClass = await loadAdapterRuntime(config.platform, config.root);
191
+ const RuntimeClass = await loadAdapterRuntime(config.platform);
154
192
 
155
193
  // ── Layer composition ──────────────────────────────────────
156
194
  const eventBus = new BaseEventBus();
157
195
  const agentRuntime = new RuntimeClass(eventBus, config.modelOverrides);
158
196
  const ui = new TerminalUI();
159
- const workflow = new BaseWorkflow(agentRuntime, config.root);
197
+ const workflow = new BaseWorkflow(agentRuntime, config.root, {
198
+ toolPolicy: config.toolPolicy,
199
+ transcriptPath:
200
+ config.transcriptPath ?? join(config.deliberationDir, "transcript.jsonl"),
201
+ });
160
202
  const adapter = composeAdapter(agentRuntime, eventBus, ui, workflow);
161
203
  log("layers composed");
162
204
 
package/src/colors.ts CHANGED
@@ -28,7 +28,16 @@ export function parseArgs(argv: string[]): ParsedArgs {
28
28
  while (i < args.length) {
29
29
  const arg = args[i];
30
30
  if (arg.startsWith("--")) {
31
- const key = arg.slice(2);
31
+ const body = arg.slice(2);
32
+ // Support --key=value (inline) as well as --key value (separate arg).
33
+ const eq = body.indexOf("=");
34
+ if (eq !== -1) {
35
+ const key = body.slice(0, eq);
36
+ flags[key] = body.slice(eq + 1);
37
+ i += 1;
38
+ continue;
39
+ }
40
+ const key = body;
32
41
  const next = args[i + 1];
33
42
  if (next && !next.startsWith("--")) {
34
43
  flags[key] = next;
@@ -377,6 +377,11 @@ export async function createCommand(args: ParsedArgs): Promise<void> {
377
377
  }
378
378
 
379
379
  const id = toKebabCase(name);
380
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
381
+ console.error(c.red(`Invalid name "${name}": must kebab-case to /^[a-z0-9][a-z0-9-]*$/`));
382
+ console.error(c.dim(`Allowed characters: a-z, 0-9, hyphen. Must start with a letter or digit.`));
383
+ process.exit(2);
384
+ }
380
385
  const displayName = id
381
386
  .split("-")
382
387
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
@@ -2,10 +2,21 @@
2
2
  * aos init — Initialize AOS configuration in the current project.
3
3
  */
4
4
 
5
- import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { c, type ParsedArgs } from "../colors";
8
- import { getHarnessRoot, getPackageCoreDir } from "../utils";
9
+ import { ADAPTER_ALLOWLIST, getHarnessRoot, getPackageCoreDir, isValidAdapter } from "../utils";
10
+
11
+ function readCliVersion(): string {
12
+ try {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ const raw = readFileSync(join(here, "..", "..", "package.json"), "utf-8");
15
+ return (JSON.parse(raw) as { version: string }).version ?? "unknown";
16
+ } catch {
17
+ return "unknown";
18
+ }
19
+ }
9
20
 
10
21
  const HELP = `
11
22
  ${c.bold("aos init")} — Initialize AOS in the current project
@@ -14,7 +25,7 @@ ${c.bold("USAGE")}
14
25
  aos init [--adapter <adapter>] [--force]
15
26
 
16
27
  ${c.bold("OPTIONS")}
17
- --adapter <name> Adapter to use: pi (default), claude-code, gemini
28
+ --adapter <name> Adapter to use: pi (default), claude-code, gemini, codex
18
29
  --force Reinitialize even if AOS is already set up (overwrites existing core configs)
19
30
 
20
31
  ${c.bold("DESCRIPTION")}
@@ -27,8 +38,6 @@ ${c.bold("EXAMPLES")}
27
38
  aos init --adapter claude-code
28
39
  `;
29
40
 
30
- const VALID_ADAPTERS = ["pi", "claude-code", "gemini"];
31
-
32
41
  function generateConfig(adapter: string): string {
33
42
  return `# AOS Harness Configuration
34
43
  # Generated by: aos init
@@ -53,8 +62,8 @@ export async function initCommand(args: ParsedArgs): Promise<void> {
53
62
 
54
63
  const adapter = (args.flags.adapter as string) || "pi";
55
64
 
56
- if (!VALID_ADAPTERS.includes(adapter)) {
57
- console.error(c.red(`Invalid adapter: "${adapter}". Valid adapters: ${VALID_ADAPTERS.join(", ")}`));
65
+ if (!isValidAdapter(adapter)) {
66
+ console.error(c.red(`Invalid adapter: "${adapter}". Valid adapters: ${ADAPTER_ALLOWLIST.join(", ")}`));
58
67
  process.exit(1);
59
68
  }
60
69
 
@@ -156,4 +165,18 @@ ${c.bold("Customization")}
156
165
  Create domain packs: ${c.cyan("aos create domain <name>")}
157
166
  Validate everything: ${c.cyan("aos validate")}
158
167
  `);
168
+
169
+ // Adapter-install guidance. Starting in 0.6.0 the CLI no longer bundles
170
+ // adapter source — users must install the adapter(s) they want to use.
171
+ const v = readCliVersion();
172
+ console.log(`${c.bold("Next step: install an adapter")}
173
+ Adapters live as separate npm packages. Install the one(s) you'll use:
174
+
175
+ Claude Code: ${c.cyan(`npm i -g @aos-harness/claude-code-adapter@${v}`)}
176
+ Gemini CLI: ${c.cyan(`npm i -g @aos-harness/gemini-adapter@${v}`)}
177
+ Codex CLI: ${c.cyan(`npm i -g @aos-harness/codex-adapter@${v}`)}
178
+ Pi (pi-ai): ${c.cyan(`npm i -g @aos-harness/pi-adapter@${v}`)}
179
+
180
+ ${c.dim("Pick one (or more). Then run `aos run`.")}
181
+ `);
159
182
  }
@@ -169,6 +169,8 @@ export async function replayCommand(args: ParsedArgs): Promise<void> {
169
169
  process.exit(1);
170
170
  }
171
171
 
172
+ // Direct CLI arg — not passed through confinedResolve (spec D4: user is trusted
173
+ // at the CLI boundary). If this ever becomes config-driven, use confinedResolve.
172
174
  const resolved = filePath.startsWith("/") ? filePath : resolve(process.cwd(), filePath);
173
175
  if (!existsSync(resolved)) {
174
176
  console.error(c.red(`Transcript file not found: ${resolved}`));
@@ -5,10 +5,11 @@
5
5
  import { existsSync, readdirSync, mkdirSync } from "node:fs";
6
6
  import { join, resolve, basename } from "node:path";
7
7
  import { c, type ParsedArgs } from "../colors";
8
- import { getHarnessRoot, discoverDirs, promptSelect, getAdapterDir } from "../utils";
8
+ import { getHarnessRoot, discoverDirs, promptSelect, getAdapterDir, ADAPTER_ALLOWLIST, isValidAdapter, validatePlatformUrl, parseAllowCodeExecutionFlag } from "../utils";
9
9
  import type { TranscriptEntry } from "@aos-harness/runtime/types";
10
10
  import { runAdapterSession } from "../adapter-session";
11
11
  import { readAdapterConfig } from "../adapter-config";
12
+ import { buildToolPolicy, type ToolPolicy } from "@aos-harness/adapter-shared";
12
13
 
13
14
  function createEventBuffer(platformUrl: string, sessionId: string) {
14
15
  const buffer: TranscriptEntry[] = [];
@@ -59,6 +60,10 @@ ${c.bold("OPTIONS")}
59
60
  --dry-run Validate config and print simulation summary without launching
60
61
  --workflow-dir <path> Directory containing workflow YAML files (default: core/workflows/)
61
62
  --platform-url <url> Platform API URL for live observability (e.g. http://localhost:3001)
63
+ --allow-code-execution[=<langs>|none]
64
+ Narrow (never widen) the profile's code-execution allowlist
65
+ for this session. Pass \`none\` to force-deny; pass a comma
66
+ list like \`python,bash\` to intersect with the profile.
62
67
 
63
68
  ${c.bold("DESCRIPTION")}
64
69
  Launches a deliberation or execution session using the specified profile.
@@ -85,6 +90,17 @@ export async function runCommand(args: ParsedArgs): Promise<void> {
85
90
  return;
86
91
  }
87
92
 
93
+ // Validate --adapter flag early (spec D2) before any project resolution so
94
+ // unknown names are rejected regardless of workspace state.
95
+ if (args.flags["adapter"]) {
96
+ const flagAdapter = args.flags["adapter"] as string;
97
+ if (!isValidAdapter(flagAdapter)) {
98
+ console.error(c.red(`Unknown adapter: ${flagAdapter}`));
99
+ console.error(c.dim(`Allowed: ${ADAPTER_ALLOWLIST.join(", ")}`));
100
+ process.exit(2);
101
+ }
102
+ }
103
+
88
104
  const root = getHarnessRoot();
89
105
  const coreDir = join(root, "core");
90
106
 
@@ -166,7 +182,33 @@ export async function runCommand(args: ParsedArgs): Promise<void> {
166
182
 
167
183
  // ── Validate brief against profile ───────────────────────────
168
184
  const { loadProfile, loadWorkflow, validateBrief } = await import("@aos-harness/runtime/config-loader");
169
- const profile = loadProfile(profileDir);
185
+ let profile: ReturnType<typeof loadProfile>;
186
+ try {
187
+ profile = loadProfile(profileDir);
188
+ } catch (err: any) {
189
+ // Malformed `tools:` block (and any other profile-schema failure) comes
190
+ // through as a ConfigError. Surface as exit 3 per spec D6 so CI can
191
+ // distinguish policy/config errors from runtime errors (exit 1) and
192
+ // invalid input (exit 2).
193
+ if (err?.name === "ConfigError") {
194
+ console.error(c.red(err.message));
195
+ process.exit(3);
196
+ }
197
+ throw err;
198
+ }
199
+
200
+ // ── Resolve tool policy (spec D3.2) ─────────────────────────
201
+ // Profile's `tools:` block is the ceiling; the CLI flag can narrow but
202
+ // never widen. Any widening attempt from buildToolPolicy is exit 3.
203
+ const allowCodeExec = parseAllowCodeExecutionFlag(args.flags["allow-code-execution"]);
204
+ let toolPolicy: ToolPolicy;
205
+ try {
206
+ toolPolicy = buildToolPolicy(profile.tools!, { allowCodeExecution: allowCodeExec });
207
+ } catch (err: any) {
208
+ console.error(c.red(err.message));
209
+ process.exit(3);
210
+ }
211
+
170
212
  const validation = validateBrief(briefPath, profile.input.required_sections);
171
213
 
172
214
  // ── Detect execution profile (has workflow field) ──────────
@@ -321,19 +363,38 @@ ${c.bold(`AOS ${sessionType} Session`)}
321
363
  }
322
364
  if (args.flags["adapter"]) adapter = args.flags["adapter"] as string;
323
365
 
324
- const adapterName = adapter === "claude-code" ? "claude-code" : adapter;
325
- // Resolve adapter from: 1) project dir, 2) installed package, 3) monorepo
326
- const resolvedAdapterDir = existsSync(join(root, "adapters", adapterName, "src", "index.ts"))
327
- ? join(root, "adapters", adapterName)
328
- : getAdapterDir(adapterName);
366
+ // Validate platform URL (spec D5). Fires for both --platform-url flag
367
+ // and .aos/config.yaml platform.url after both sources are merged.
368
+ if (platformUrl) {
369
+ try {
370
+ validatePlatformUrl(platformUrl);
371
+ } catch (err: any) {
372
+ console.error(c.red(`Invalid platform.url: ${err.message}`));
373
+ process.exit(2);
374
+ }
375
+ }
376
+
377
+ if (!isValidAdapter(adapter)) {
378
+ console.error(c.red(`Unknown adapter: ${adapter}`));
379
+ console.error(c.dim(`Allowed: ${ADAPTER_ALLOWLIST.join(", ")}`));
380
+ process.exit(2);
381
+ }
382
+
383
+ const adapterName = adapter;
384
+ // Resolve from monorepo dev layout (CLI's own import.meta.dir) or installed
385
+ // @aos-harness/<name>-adapter. Project-local override is intentionally absent
386
+ // (spec D1 — workspace-trust hardening).
387
+ const resolvedAdapterDir = getAdapterDir(adapterName);
329
388
 
330
389
  if (adapter === "pi") {
331
390
  const adapterEntry = resolvedAdapterDir ? join(resolvedAdapterDir, "src", "index.ts") : null;
332
391
  if (!adapterEntry || !existsSync(adapterEntry)) {
333
392
  console.error(c.red(`Pi adapter not found.`));
334
393
  console.error(c.yellow("Make sure Pi CLI is installed: https://github.com/pi-agi/pi"));
335
- console.error(c.dim("The adapter should be bundled with aos-harness. Try reinstalling: bun add -g aos-harness"));
336
- process.exit(1);
394
+ console.error(c.dim("Install the adapter package:"));
395
+ console.error(c.dim(" npm i -g @aos-harness/pi-adapter"));
396
+ console.error(c.dim(" # or in a project: npm i @aos-harness/pi-adapter"));
397
+ process.exit(2);
337
398
  }
338
399
 
339
400
  console.log(c.dim(`Launching Pi adapter...`));
@@ -362,6 +423,11 @@ ${c.bold(`AOS ${sessionType} Session`)}
362
423
  env.AOS_WORKFLOW_ID = workflowConfig.id;
363
424
  env.AOS_WORKFLOWS_DIR = workflowsDir;
364
425
  }
426
+ // Pass the resolved ToolPolicy to the Pi adapter as JSON. The Pi adapter
427
+ // runs in a separate process; this env var is the contract. Pi-side
428
+ // consumption (gating executeCode + emitting tool-denied transcript
429
+ // events) is a future follow-up — see adapter-trust-model-plan.md.
430
+ env.AOS_TOOL_POLICY_JSON = JSON.stringify(toolPolicy);
365
431
 
366
432
  const proc = Bun.spawn(["pi", "-e", adapterEntry], {
367
433
  cwd: root,
@@ -390,6 +456,7 @@ ${c.bold(`AOS ${sessionType} Session`)}
390
456
  workflowConfig: isExecutionProfile ? workflowConfig : null,
391
457
  workflowsDir,
392
458
  modelOverrides: adapterConfig?.model_overrides,
459
+ toolPolicy,
393
460
  });
394
461
  }
395
462
  }
package/src/index.ts CHANGED
File without changes