aos-harness 0.8.4 → 0.9.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.
@@ -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.4",
3
+ "version": "0.9.0",
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.4",
42
- "@aos-harness/runtime": "0.8.4",
41
+ "@aos-harness/adapter-shared": "0.9.0",
42
+ "@aos-harness/runtime": "0.9.0",
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.4 <1.0.0",
50
- "@aos-harness/codex-adapter": ">=0.8.4 <1.0.0",
51
- "@aos-harness/gemini-adapter": ">=0.8.4 <1.0.0",
52
- "@aos-harness/pi-adapter": ">=0.8.4 <1.0.0"
49
+ "@aos-harness/claude-code-adapter": ">=0.9.0 <1.0.0",
50
+ "@aos-harness/codex-adapter": ">=0.9.0 <1.0.0",
51
+ "@aos-harness/gemini-adapter": ">=0.9.0 <1.0.0",
52
+ "@aos-harness/pi-adapter": ">=0.9.0 <1.0.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@aos-harness/claude-code-adapter": {
@@ -65,6 +65,7 @@ export interface AdapterSessionConfig {
65
65
  * also batched to `${platformUrl}/api/sessions/:id/events`.
66
66
  */
67
67
  platformUrl?: string;
68
+ agentTimeoutMs?: number;
68
69
  }
69
70
 
70
71
  function createStreamingPrinter() {
@@ -523,6 +524,7 @@ export async function runAdapterSession(config: AdapterSessionConfig): Promise<v
523
524
  response = await adapter.sendMessage(arbiterHandle, kickoff, {
524
525
  extraArgs: mcpArgs,
525
526
  onStream: (partial) => printer.push(partial),
527
+ timeoutMs: config.agentTimeoutMs,
526
528
  });
527
529
  } finally {
528
530
  clearInterval(heartbeat);
@@ -0,0 +1,58 @@
1
+ export interface ParsedSection {
2
+ heading: string;
3
+ level: 1 | 2 | 3;
4
+ body: string;
5
+ startLine: number;
6
+ }
7
+
8
+ const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
9
+ const TITLE_RE = /^#\s+Brief:\s+(.+?)\s*$/;
10
+
11
+ export function parseTitle(markdown: string): string | null {
12
+ for (const line of markdown.split("\n")) {
13
+ const match = line.match(TITLE_RE);
14
+ if (match) return match[1];
15
+ }
16
+ return null;
17
+ }
18
+
19
+ export function parseSections(markdown: string): ParsedSection[] {
20
+ const lines = markdown.split("\n");
21
+ const sections: ParsedSection[] = [];
22
+ let current: ParsedSection | null = null;
23
+
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const line = lines[i];
26
+ const match = line.match(HEADING_RE);
27
+ if (match) {
28
+ const level = match[1].length as 1 | 2 | 3;
29
+ if (level === 1) {
30
+ if (current) {
31
+ sections.push(current);
32
+ current = null;
33
+ }
34
+ continue;
35
+ }
36
+ if (current) sections.push(current);
37
+ current = { heading: match[2], level, body: "", startLine: i };
38
+ } else if (current) {
39
+ current.body += `${line}\n`;
40
+ }
41
+ }
42
+
43
+ if (current) sections.push(current);
44
+ return sections;
45
+ }
46
+
47
+ export function isBodyEmpty(body: string): boolean {
48
+ return body.replace(/<!--[\s\S]*?-->/g, "").trim().length === 0;
49
+ }
50
+
51
+ export function findSection(
52
+ sections: ParsedSection[],
53
+ canonical: string,
54
+ aliases: string[] = [],
55
+ ): ParsedSection | null {
56
+ const candidates = [canonical, ...aliases].map((candidate) => candidate.toLowerCase());
57
+ return sections.find((section) => candidates.includes(section.heading.toLowerCase())) ?? null;
58
+ }
@@ -0,0 +1,77 @@
1
+ import type { BriefKind, BriefSections } from "./types";
2
+
3
+ export interface LineReader {
4
+ readLine(): Promise<string>;
5
+ }
6
+
7
+ export interface PromptLoopOpts {
8
+ reader: LineReader;
9
+ kind?: BriefKind;
10
+ slug?: string;
11
+ title?: string;
12
+ seedText?: string;
13
+ log?: (line: string) => void;
14
+ }
15
+
16
+ export interface PromptLoopResult {
17
+ slug: string;
18
+ kind: BriefKind;
19
+ title: string;
20
+ sections: BriefSections;
21
+ }
22
+
23
+ const SECTION_PROMPTS: Record<BriefKind, Array<{ key: keyof BriefSections; label: string }>> = {
24
+ deliberation: [
25
+ { key: "situation", label: "What's the situation? (multi-line, end with blank line)" },
26
+ { key: "stakes", label: "What's at stake?" },
27
+ { key: "constraints", label: "What constraints apply?" },
28
+ { key: "keyQuestion", label: "What is the single key question for the council?" },
29
+ ],
30
+ execution: [
31
+ { key: "featureVision", label: "What feature or vision are we building?" },
32
+ { key: "context", label: "What context (environment, prior art, repo state)?" },
33
+ { key: "constraints", label: "What constraints apply?" },
34
+ { key: "successCriteria", label: "What success criteria define done?" },
35
+ ],
36
+ };
37
+
38
+ export async function runBriefPromptLoop(opts: PromptLoopOpts): Promise<PromptLoopResult> {
39
+ const log = opts.log ?? ((line: string) => console.log(line));
40
+
41
+ if (opts.seedText) log(`\nYour seed: ${opts.seedText}\n`);
42
+
43
+ const slug = opts.slug ?? (await ask(opts.reader, log, "Slug for this brief (kebab-case)?"));
44
+ let kind: BriefKind;
45
+ if (opts.kind) {
46
+ kind = opts.kind;
47
+ } else {
48
+ log("\nKind?");
49
+ log(" 1. deliberation");
50
+ log(" 2. execution");
51
+ kind = (await ask(opts.reader, log, "Enter number:")).trim() === "2" ? "execution" : "deliberation";
52
+ }
53
+
54
+ const title = opts.title ?? (await ask(opts.reader, log, "One-line title?"));
55
+ const sections: BriefSections = {};
56
+ for (const { key, label } of SECTION_PROMPTS[kind]) {
57
+ sections[key] = await askMultiline(opts.reader, log, label);
58
+ }
59
+
60
+ return { slug, kind, title, sections };
61
+ }
62
+
63
+ async function ask(reader: LineReader, log: (line: string) => void, label: string): Promise<string> {
64
+ log(`\n${label}`);
65
+ return (await reader.readLine()).trim();
66
+ }
67
+
68
+ async function askMultiline(reader: LineReader, log: (line: string) => void, label: string): Promise<string> {
69
+ log(`\n${label}`);
70
+ const lines: string[] = [];
71
+ while (true) {
72
+ const line = await reader.readLine();
73
+ if (line.trim() === "") break;
74
+ lines.push(line);
75
+ }
76
+ return lines.join("\n");
77
+ }
@@ -0,0 +1,24 @@
1
+ import type { BriefKind, BriefSchemaDef } from "./types";
2
+
3
+ export const DELIBERATION_SCHEMA: BriefSchemaDef = {
4
+ requiredSections: ["Situation", "Stakes", "Constraints", "Key Question"],
5
+ optionalSections: ["Background", "Out of scope"],
6
+ aliases: {},
7
+ };
8
+
9
+ export const EXECUTION_SCHEMA: BriefSchemaDef = {
10
+ requiredSections: ["Feature / Vision", "Context", "Constraints", "Success Criteria"],
11
+ optionalSections: ["Stakeholders", "Out of scope", "Open Questions"],
12
+ aliases: {
13
+ "Feature / Vision": ["Vision"],
14
+ },
15
+ };
16
+
17
+ export function briefSchema(kind: BriefKind): BriefSchemaDef {
18
+ return kind === "deliberation" ? DELIBERATION_SCHEMA : EXECUTION_SCHEMA;
19
+ }
20
+
21
+ export const DISCRIMINATING_HEADINGS: Record<BriefKind, string> = {
22
+ deliberation: "Key Question",
23
+ execution: "Success Criteria",
24
+ };
@@ -0,0 +1,74 @@
1
+ import type { BriefKind, BriefSections } from "./types";
2
+ import { briefSchema } from "./schema";
3
+
4
+ const SECTION_KEY_MAP: Record<BriefKind, Record<string, keyof BriefSections>> = {
5
+ deliberation: {
6
+ "Situation": "situation",
7
+ "Stakes": "stakes",
8
+ "Constraints": "constraints",
9
+ "Background": "background",
10
+ "Out of scope": "outOfScope",
11
+ "Key Question": "keyQuestion",
12
+ },
13
+ execution: {
14
+ "Feature / Vision": "featureVision",
15
+ "Context": "context",
16
+ "Constraints": "constraints",
17
+ "Stakeholders": "stakeholders",
18
+ "Out of scope": "outOfScope",
19
+ "Open Questions": "openQuestions",
20
+ "Success Criteria": "successCriteria",
21
+ },
22
+ };
23
+
24
+ const PLACEHOLDER_TEXT: Record<string, string> = {
25
+ "Situation": "Describe what is happening, who is involved, what triggered the decision.",
26
+ "Stakes": "Spell out upside and downside for each path.",
27
+ "Constraints": "List budget, timeline, technical, and regulatory constraints.",
28
+ "Key Question": "State the single decision question for the council.",
29
+ "Background": "Extended context (optional).",
30
+ "Out of scope": "Things explicitly not on the table (optional).",
31
+ "Feature / Vision": "What you're building and why.",
32
+ "Context": "Environment, prior art, repo state.",
33
+ "Success Criteria": "How will you know this is done?",
34
+ "Stakeholders": "Who consumes this output (optional).",
35
+ "Open Questions": "Known unknowns (optional).",
36
+ };
37
+
38
+ export interface RenderOpts {
39
+ kind: BriefKind;
40
+ title?: string;
41
+ prefilled?: BriefSections;
42
+ seedText?: string;
43
+ }
44
+
45
+ export function renderBriefTemplate(opts: RenderOpts): string {
46
+ const lines: string[] = [`# Brief: ${opts.title ?? "TODO"}`, ""];
47
+
48
+ if (opts.seedText) {
49
+ lines.push("<!-- raw idea seed:");
50
+ lines.push(...opts.seedText.split("\n"));
51
+ lines.push("-->", "");
52
+ }
53
+
54
+ const schema = briefSchema(opts.kind);
55
+ const ordered = [...schema.requiredSections];
56
+ for (const optional of schema.optionalSections) {
57
+ if (!ordered.includes(optional)) ordered.push(optional);
58
+ }
59
+
60
+ const keyMap = SECTION_KEY_MAP[opts.kind];
61
+ for (const heading of ordered) {
62
+ lines.push(`## ${heading}`, "");
63
+ const key = keyMap[heading];
64
+ const filled = key ? opts.prefilled?.[key] : undefined;
65
+ if (filled && filled.trim()) {
66
+ lines.push(filled.trim());
67
+ } else {
68
+ lines.push(`<!-- TODO: ${PLACEHOLDER_TEXT[heading] ?? `Describe ${heading.toLowerCase()}.`} -->`);
69
+ }
70
+ lines.push("");
71
+ }
72
+
73
+ return `${lines.join("\n").trimEnd()}\n`;
74
+ }
@@ -0,0 +1,44 @@
1
+ export type BriefKind = "deliberation" | "execution";
2
+
3
+ export type BriefIssueKind =
4
+ | "missing-required"
5
+ | "empty-section"
6
+ | "missing-title"
7
+ | "title-format"
8
+ | "auto-detect-failed"
9
+ | "shape-mismatch-hint";
10
+
11
+ export interface BriefIssue {
12
+ kind: BriefIssueKind;
13
+ section?: string;
14
+ message: string;
15
+ }
16
+
17
+ export interface BriefValidation {
18
+ ok: boolean;
19
+ detectedKind: BriefKind | null;
20
+ errors: BriefIssue[];
21
+ warnings: BriefIssue[];
22
+ }
23
+
24
+ export interface BriefSchemaDef {
25
+ requiredSections: string[];
26
+ optionalSections: string[];
27
+ aliases: Record<string, string[]>;
28
+ }
29
+
30
+ export interface BriefSections {
31
+ title?: string;
32
+ contextFiles?: string;
33
+ situation?: string;
34
+ stakes?: string;
35
+ background?: string;
36
+ outOfScope?: string;
37
+ keyQuestion?: string;
38
+ featureVision?: string;
39
+ context?: string;
40
+ stakeholders?: string;
41
+ openQuestions?: string;
42
+ successCriteria?: string;
43
+ constraints?: string;
44
+ }
@@ -0,0 +1,97 @@
1
+ import type { BriefKind, BriefIssue, BriefValidation } from "./types";
2
+ import { briefSchema, DISCRIMINATING_HEADINGS } from "./schema";
3
+ import { findSection, isBodyEmpty, parseSections, parseTitle, type ParsedSection } from "./parse";
4
+
5
+ export interface ValidateOptions {
6
+ expectedKind?: BriefKind;
7
+ strict?: boolean;
8
+ }
9
+
10
+ export function validateBrief(markdown: string, opts: ValidateOptions = {}): BriefValidation {
11
+ const errors: BriefIssue[] = [];
12
+ const warnings: BriefIssue[] = [];
13
+
14
+ if (!parseTitle(markdown)) {
15
+ errors.push({
16
+ kind: "missing-title",
17
+ message: "Title must be a H1 line starting with `Brief: ` (e.g. `# Brief: API Platform Decision`).",
18
+ });
19
+ }
20
+
21
+ const sections = parseSections(markdown);
22
+ const kind = opts.expectedKind ?? autoDetectKind(sections, errors);
23
+ if (!kind) {
24
+ return { ok: false, detectedKind: null, errors, warnings };
25
+ }
26
+
27
+ const schema = briefSchema(kind);
28
+ for (const required of schema.requiredSections) {
29
+ const aliases = schema.aliases[required] ?? [];
30
+ const found = findSection(sections, required, aliases);
31
+ if (!found) {
32
+ errors.push({
33
+ kind: "missing-required",
34
+ section: required,
35
+ message: `Missing required section: \`## ${required}\`.`,
36
+ });
37
+ continue;
38
+ }
39
+
40
+ if (isBodyEmpty(found.body)) {
41
+ const issue: BriefIssue = {
42
+ kind: "empty-section",
43
+ section: required,
44
+ message: `Section \`## ${required}\` is empty.`,
45
+ };
46
+ if (opts.strict) errors.push(issue);
47
+ else warnings.push(issue);
48
+ }
49
+ }
50
+
51
+ if (opts.expectedKind) {
52
+ const missingCount = errors.filter((issue) => issue.kind === "missing-required").length;
53
+ if (missingCount >= 2) {
54
+ const otherKind: BriefKind = opts.expectedKind === "deliberation" ? "execution" : "deliberation";
55
+ if (findSection(sections, DISCRIMINATING_HEADINGS[otherKind])) {
56
+ errors.push({
57
+ kind: "shape-mismatch-hint",
58
+ message: `This brief looks shaped for \`${otherKind}\`. Either run a \`${otherKind}\` profile or re-author with \`aos create brief --kind ${opts.expectedKind}\`.`,
59
+ });
60
+ }
61
+ }
62
+ }
63
+
64
+ return {
65
+ ok: errors.length === 0,
66
+ detectedKind: kind,
67
+ errors,
68
+ warnings,
69
+ };
70
+ }
71
+
72
+ function autoDetectKind(sections: ParsedSection[], errors: BriefIssue[]): BriefKind | null {
73
+ const deliberationScore = scoreKind("deliberation", sections);
74
+ const executionScore = scoreKind("execution", sections);
75
+ const best = Math.max(deliberationScore, executionScore);
76
+
77
+ if (best < 2 || deliberationScore === executionScore && best < 4) {
78
+ errors.push({
79
+ kind: "auto-detect-failed",
80
+ message:
81
+ "Brief is missing too many required sections to determine kind. Specify `--kind deliberation` or `--kind execution`, or author from scratch with `aos create brief`.",
82
+ });
83
+ return null;
84
+ }
85
+
86
+ if (deliberationScore === executionScore) {
87
+ return "execution";
88
+ }
89
+ return deliberationScore > executionScore ? "deliberation" : "execution";
90
+ }
91
+
92
+ function scoreKind(kind: BriefKind, sections: ParsedSection[]): number {
93
+ const schema = briefSchema(kind);
94
+ return schema.requiredSections.filter((required) =>
95
+ findSection(sections, required, schema.aliases[required] ?? []),
96
+ ).length;
97
+ }
@@ -0,0 +1,35 @@
1
+ import { dirname } from "node:path";
2
+ import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+
4
+ export interface WriteOpts {
5
+ force: boolean;
6
+ }
7
+
8
+ export async function atomicWriteBrief(targetPath: string, content: string, opts: WriteOpts): Promise<void> {
9
+ if (existsSync(targetPath) && !opts.force) {
10
+ throw new Error(`Brief already exists at ${targetPath}. Pass --force to overwrite.`);
11
+ }
12
+
13
+ const parent = dirname(targetPath);
14
+ try {
15
+ mkdirSync(parent, { recursive: true });
16
+ } catch (err: any) {
17
+ if (err.code === "EACCES" || err.code === "EROFS") {
18
+ throw new Error(`Cannot create directory ${parent}: permission denied. Use --out <path> to write somewhere else.`);
19
+ }
20
+ throw err;
21
+ }
22
+
23
+ const tmpPath = `${targetPath}.tmp.${process.pid}`;
24
+ try {
25
+ writeFileSync(tmpPath, content, "utf-8");
26
+ renameSync(tmpPath, targetPath);
27
+ } catch (err) {
28
+ try {
29
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
30
+ } catch {
31
+ // best-effort cleanup
32
+ }
33
+ throw err;
34
+ }
35
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { Buffer } from "node:buffer";
3
+ import { c, type ParsedArgs } from "../colors";
4
+ import type { BriefKind } from "../brief/types";
5
+
6
+ const HELP = `
7
+ ${c.bold("aos brief")} — Work with brief files
8
+
9
+ ${c.bold("USAGE")}
10
+ aos brief <subcommand> [options]
11
+
12
+ ${c.bold("SUBCOMMANDS")}
13
+ ${c.cyan("template")} Render a blank brief template to stdout or --out
14
+ ${c.cyan("validate")} Validate a brief against its schema
15
+ ${c.cyan("save")} Atomically write a brief (used by skills)
16
+
17
+ ${c.bold("EXAMPLES")}
18
+ aos brief template --kind deliberation
19
+ aos brief validate ./briefs/foo/brief.md --kind deliberation
20
+ aos brief save ./briefs/foo/brief.md --kind execution --from-file draft.md
21
+ `;
22
+
23
+ export async function briefCommand(args: ParsedArgs): Promise<void> {
24
+ if (args.flags.help && !args.subcommand) {
25
+ console.log(HELP);
26
+ return;
27
+ }
28
+
29
+ switch (args.subcommand) {
30
+ case "template":
31
+ await templateCommand(args);
32
+ return;
33
+ case "validate":
34
+ await validateCommand(args);
35
+ return;
36
+ case "save":
37
+ await saveCommand(args);
38
+ return;
39
+ case "":
40
+ case undefined:
41
+ case null:
42
+ console.log(HELP);
43
+ return;
44
+ default:
45
+ console.error(c.red(`Unknown brief subcommand: "${args.subcommand}". Run "aos brief --help".`));
46
+ process.exit(2);
47
+ }
48
+ }
49
+
50
+ function parseKind(value: string | boolean | undefined): BriefKind | null {
51
+ return value === "deliberation" || value === "execution" ? value : null;
52
+ }
53
+
54
+ async function templateCommand(args: ParsedArgs): Promise<void> {
55
+ const kind = parseKind(args.flags.kind);
56
+ if (!args.flags.kind) {
57
+ console.error(c.red("Missing --kind. Use --kind deliberation or --kind execution."));
58
+ process.exit(2);
59
+ }
60
+ if (!kind) {
61
+ console.error(c.red(`--kind must be deliberation or execution (got "${args.flags.kind}").`));
62
+ process.exit(2);
63
+ }
64
+
65
+ const { renderBriefTemplate } = await import("../brief/template");
66
+ const out = renderBriefTemplate({
67
+ kind,
68
+ title: (args.flags.title as string | undefined) ?? "TODO",
69
+ seedText: args.flags.idea as string | undefined,
70
+ });
71
+
72
+ const outPath = args.flags.out as string | undefined;
73
+ if (outPath) {
74
+ const { atomicWriteBrief } = await import("../brief/write");
75
+ try {
76
+ await atomicWriteBrief(outPath, out, { force: Boolean(args.flags.force) });
77
+ } catch (err: any) {
78
+ console.error(c.red(err.message));
79
+ process.exit(1);
80
+ }
81
+ console.error(c.dim(`Template written to ${outPath}.`));
82
+ } else {
83
+ process.stdout.write(out);
84
+ }
85
+ }
86
+
87
+ async function validateCommand(args: ParsedArgs): Promise<void> {
88
+ const path = args.positional[1];
89
+ if (!path) {
90
+ console.error(c.red("Missing brief path. Usage: aos brief validate <path> [--kind <k>] [--strict]"));
91
+ process.exit(2);
92
+ }
93
+ if (!existsSync(path)) {
94
+ console.error(c.red(`Brief file not found: ${path}`));
95
+ process.exit(2);
96
+ }
97
+
98
+ const kind = args.flags.kind ? parseKind(args.flags.kind) : undefined;
99
+ if (args.flags.kind && !kind) {
100
+ console.error(c.red(`--kind must be deliberation or execution (got "${args.flags.kind}").`));
101
+ process.exit(2);
102
+ }
103
+
104
+ const { validateBrief } = await import("../brief/validate");
105
+ const result = validateBrief(readFileSync(path, "utf-8"), {
106
+ expectedKind: kind || undefined,
107
+ strict: Boolean(args.flags.strict),
108
+ });
109
+ for (const issue of [...result.errors, ...result.warnings]) {
110
+ const sectionLabel = issue.section ? `[${issue.section}] ` : "";
111
+ const color = issue.kind === "empty-section" && !args.flags.strict ? c.yellow : c.red;
112
+ console.error(color(`${sectionLabel}${issue.message}`));
113
+ }
114
+ process.exit(result.errors.length > 0 ? 1 : 0);
115
+ }
116
+
117
+ async function saveCommand(args: ParsedArgs): Promise<void> {
118
+ const path = args.positional[1];
119
+ if (!path) {
120
+ console.error(c.red("Missing target path. Usage: aos brief save <path> --kind <k> [--from-file <p> | --from-stdin]"));
121
+ process.exit(2);
122
+ }
123
+
124
+ const kind = parseKind(args.flags.kind);
125
+ if (!kind) {
126
+ console.error(c.red("--kind required (deliberation or execution)."));
127
+ process.exit(2);
128
+ }
129
+
130
+ let content: string;
131
+ const fromFile = args.flags["from-file"] as string | undefined;
132
+ const fromStdin = Boolean(args.flags["from-stdin"]);
133
+ if (fromFile) {
134
+ if (!existsSync(fromFile)) {
135
+ console.error(c.red(`--from-file path not found: ${fromFile}`));
136
+ process.exit(2);
137
+ }
138
+ content = readFileSync(fromFile, "utf-8");
139
+ } else if (fromStdin) {
140
+ const chunks: Uint8Array[] = [];
141
+ for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
142
+ content = new TextDecoder().decode(Buffer.concat(chunks));
143
+ } else {
144
+ console.error(c.red("Must pass --from-file <path> or --from-stdin."));
145
+ process.exit(2);
146
+ }
147
+
148
+ const { validateBrief } = await import("../brief/validate");
149
+ const validation = validateBrief(content, { expectedKind: kind, strict: true });
150
+ if (validation.errors.length > 0) {
151
+ console.error(c.red(`Brief validation failed (${validation.errors.length} error${validation.errors.length === 1 ? "" : "s"}):`));
152
+ for (const err of validation.errors) {
153
+ const sectionLabel = err.section ? `[${err.section}] ` : "";
154
+ console.error(c.red(` ${sectionLabel}${err.message}`));
155
+ }
156
+ process.exit(1);
157
+ }
158
+
159
+ const { atomicWriteBrief } = await import("../brief/write");
160
+ try {
161
+ await atomicWriteBrief(path, content, { force: Boolean(args.flags.force) });
162
+ } catch (err: any) {
163
+ console.error(c.red(err.message));
164
+ process.exit(1);
165
+ }
166
+ console.log(c.green(`Brief saved to ${path}`));
167
+ }
@@ -2,19 +2,21 @@
2
2
  * aos create — Scaffold new agents, profiles, and domains.
3
3
  */
4
4
 
5
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
7
+ import { Buffer } from "node:buffer";
7
8
  import { c, type ParsedArgs } from "../colors";
8
9
  import { getHarnessRoot, toKebabCase } from "../utils";
9
10
 
10
11
  const HELP = `
11
- ${c.bold("aos create")} — Scaffold new agents, profiles, domains, and skills
12
+ ${c.bold("aos create")} — Scaffold new agents, profiles, domains, skills, and briefs
12
13
 
13
14
  ${c.bold("USAGE")}
14
15
  aos create agent <name> Create a new custom agent
15
16
  aos create profile <name> Create a new profile
16
17
  aos create domain <name> Create a new domain pack
17
18
  aos create skill <name> Create a new skill definition
19
+ aos create brief [slug] Create a new deliberation or execution brief
18
20
 
19
21
  ${c.bold("DESCRIPTION")}
20
22
  Generates well-structured scaffolds that pass "aos validate" out of the box.
@@ -26,6 +28,8 @@ ${c.bold("EXAMPLES")}
26
28
  aos create profile security-review
27
29
  aos create domain healthcare
28
30
  aos create skill dependency-analysis
31
+ aos create brief my-decision --kind deliberation --non-interactive --title "Decision" \\
32
+ --situation "..." --stakes "..." --constraints "..." --key-question "?"
29
33
  `;
30
34
 
31
35
  // ── Agent scaffold ──────────────────────────────────────────────
@@ -371,12 +375,17 @@ export async function createCommand(args: ParsedArgs): Promise<void> {
371
375
  const type = args.subcommand;
372
376
  const name = args.positional[1];
373
377
 
374
- if (!name) {
378
+ if (!name && type !== "brief") {
375
379
  console.error(c.red(`Missing name. Usage: aos create ${type} <name>`));
376
380
  process.exit(1);
377
381
  }
378
382
 
379
- const id = toKebabCase(name);
383
+ if (type === "brief") {
384
+ await createBrief(args);
385
+ return;
386
+ }
387
+
388
+ const id = toKebabCase(name!);
380
389
  if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
381
390
  console.error(c.red(`Invalid name "${name}": must kebab-case to /^[a-z0-9][a-z0-9-]*$/`));
382
391
  console.error(c.dim(`Allowed characters: a-z, 0-9, hyphen. Must start with a letter or digit.`));
@@ -490,7 +499,113 @@ ${c.bold("Next Steps")}
490
499
  }
491
500
 
492
501
  default:
493
- console.error(c.red(`Unknown type: "${type}". Use "agent", "profile", "domain", or "skill".`));
502
+ console.error(c.red(`Unknown type: "${type}". Use "agent", "profile", "domain", "skill", or "brief".`));
494
503
  process.exit(1);
495
504
  }
496
505
  }
506
+
507
+ async function createBrief(args: ParsedArgs): Promise<void> {
508
+ const { renderBriefTemplate } = await import("../brief/template");
509
+ const { validateBrief } = await import("../brief/validate");
510
+ const { atomicWriteBrief } = await import("../brief/write");
511
+ const { runBriefPromptLoop } = await import("../brief/prompts");
512
+
513
+ const slugPositional = args.positional[1];
514
+ const kindFlag = args.flags.kind as ("deliberation" | "execution" | undefined);
515
+ const titleFlag = args.flags.title as string | undefined;
516
+ const fromNotes = args.flags["from-notes"] as string | undefined;
517
+ const seedText = (args.flags.idea as string | undefined) ??
518
+ (fromNotes ? readFileSync(fromNotes, "utf-8") : undefined);
519
+ const nonInteractive = Boolean(args.flags["non-interactive"]);
520
+ const force = Boolean(args.flags.force);
521
+
522
+ let slug: string;
523
+ let kind: "deliberation" | "execution";
524
+ let title: string;
525
+ let sections: any = {};
526
+
527
+ if (nonInteractive) {
528
+ if (!slugPositional) {
529
+ console.error(c.red("--non-interactive requires <slug> positional."));
530
+ process.exit(2);
531
+ }
532
+ if (kindFlag !== "deliberation" && kindFlag !== "execution") {
533
+ console.error(c.red("--non-interactive requires --kind deliberation or --kind execution."));
534
+ process.exit(2);
535
+ }
536
+ if (!titleFlag) {
537
+ console.error(c.red("--non-interactive requires --title."));
538
+ process.exit(2);
539
+ }
540
+
541
+ slug = toKebabCase(slugPositional);
542
+ kind = kindFlag;
543
+ title = titleFlag;
544
+ sections = kind === "deliberation"
545
+ ? {
546
+ situation: args.flags.situation as string | undefined,
547
+ stakes: args.flags.stakes as string | undefined,
548
+ constraints: args.flags.constraints as string | undefined,
549
+ keyQuestion: args.flags["key-question"] as string | undefined,
550
+ }
551
+ : {
552
+ featureVision: args.flags.feature as string | undefined,
553
+ context: args.flags.context as string | undefined,
554
+ constraints: args.flags.constraints as string | undefined,
555
+ successCriteria: args.flags["success-criteria"] as string | undefined,
556
+ };
557
+ } else {
558
+ const reader = await createStdinLineReader();
559
+ const result = await runBriefPromptLoop({
560
+ reader,
561
+ kind: kindFlag,
562
+ slug: slugPositional ? toKebabCase(slugPositional) : undefined,
563
+ title: titleFlag,
564
+ seedText,
565
+ });
566
+ slug = toKebabCase(result.slug);
567
+ kind = result.kind;
568
+ title = result.title;
569
+ sections = result.sections;
570
+ }
571
+
572
+ const rendered = renderBriefTemplate({ kind, title, prefilled: sections, seedText });
573
+ const validation = validateBrief(rendered, { expectedKind: kind, strict: true });
574
+ if (validation.errors.length > 0) {
575
+ console.error(c.red("Brief incomplete:"));
576
+ for (const err of validation.errors) {
577
+ const sectionLabel = err.section ? `[${err.section}] ` : "";
578
+ console.error(c.red(` ${sectionLabel}${err.message}`));
579
+ }
580
+ process.exit(1);
581
+ }
582
+
583
+ const outFlag = args.flags.out as string | undefined;
584
+ const targetPath = outFlag
585
+ ? resolve(process.cwd(), outFlag)
586
+ : Boolean(args.flags.shared)
587
+ ? join(getHarnessRoot(), "core", "briefs", slug, "brief.md")
588
+ : resolve(process.cwd(), "briefs", slug, "brief.md");
589
+
590
+ try {
591
+ await atomicWriteBrief(targetPath, rendered, { force });
592
+ } catch (err: any) {
593
+ console.error(c.red(err.message));
594
+ process.exit(1);
595
+ }
596
+
597
+ console.log(c.green(`Brief saved to ${targetPath}`));
598
+ console.log(c.dim(`Run with: aos run <profile> --brief ${targetPath}`));
599
+ }
600
+
601
+ async function createStdinLineReader() {
602
+ const chunks: Uint8Array[] = [];
603
+ for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
604
+ const lines = new TextDecoder().decode(Buffer.concat(chunks)).split(/\r?\n/);
605
+ let index = 0;
606
+ return {
607
+ async readLine(): Promise<string> {
608
+ return lines[index++] ?? "";
609
+ },
610
+ };
611
+ }
@@ -236,10 +236,30 @@ export async function runCommand(args: ParsedArgs): Promise<void> {
236
236
  process.exit(3);
237
237
  }
238
238
 
239
- const validation = validateBrief(briefPath, profile.input.required_sections);
240
-
241
239
  // ── Detect execution profile (has workflow field) ──────────
242
240
  const isExecutionProfile = !!profile.workflow;
241
+
242
+ // Brief lint is advisory. The existing profile section validation below
243
+ // remains the blocking compatibility check for run.
244
+ try {
245
+ const briefContent = readFileSync(briefPath, "utf-8");
246
+ const expectedKind: "deliberation" | "execution" = isExecutionProfile ? "execution" : "deliberation";
247
+ const { validateBrief: validateBriefShape } = await import("../brief/validate");
248
+ const briefValidation = validateBriefShape(briefContent, { expectedKind });
249
+ const errCount = briefValidation.errors.length;
250
+ const warnCount = briefValidation.warnings.length;
251
+ if (errCount === 0 && warnCount === 0) {
252
+ console.error(c.dim(`Brief lint: ${expectedKind} brief looks good (0 errors, 0 warnings).`));
253
+ } else {
254
+ console.error(c.yellow(`Brief lint: ${errCount} error${errCount === 1 ? "" : "s"}, ${warnCount} warning${warnCount === 1 ? "" : "s"}.`));
255
+ console.error(c.dim(` Run \`aos brief validate ${briefPath}\` for details, or \`aos create brief\` to author from a template.`));
256
+ }
257
+ } catch (err) {
258
+ console.error(c.dim(`(brief lint skipped: ${(err as Error).message})`));
259
+ }
260
+
261
+ const validation = validateBrief(briefPath, profile.input.required_sections);
262
+
243
263
  let workflowConfig: Awaited<ReturnType<typeof loadWorkflow>> | null = null;
244
264
 
245
265
  if (isExecutionProfile) {
@@ -519,6 +539,10 @@ ${c.bold(`AOS ${sessionType} Session`)}
519
539
  useVendorDefaultModel: runtimeModelConfig.useVendorDefaultModel,
520
540
  toolPolicy,
521
541
  platformUrl: platformUrl ?? undefined,
542
+ agentTimeoutMs:
543
+ typeof profile.error_handling?.agent_timeout_seconds === "number"
544
+ ? profile.error_handling.agent_timeout_seconds * 1000
545
+ : undefined,
522
546
  });
523
547
  }
524
548
  }
@@ -29,7 +29,7 @@ ${c.bold("CHECKS")}
29
29
  - Domain merging produces valid output
30
30
  - Skills load and have required fields (id, name, input, output, etc.)
31
31
  - Skills reference valid compatible agents
32
- - Briefs pass section requirements
32
+ - Briefs are well-formed (deliberation or execution shape)
33
33
  - Template resolution works for all agents
34
34
  - Constraint engine initializes from profiles
35
35
  - Delegation router initializes from profiles
@@ -51,7 +51,7 @@ export async function validateCommand(args: ParsedArgs): Promise<void> {
51
51
  const coreDir = join(root, "core");
52
52
 
53
53
  // Import runtime modules
54
- const { loadAgent, loadProfile, loadDomain, loadWorkflow, loadSkill, validateBrief } = await import("@aos-harness/runtime/config-loader");
54
+ const { loadAgent, loadProfile, loadDomain, loadWorkflow, loadSkill } = await import("@aos-harness/runtime/config-loader");
55
55
  const { applyDomain } = await import("@aos-harness/runtime/domain-merger");
56
56
  const { resolveTemplate } = await import("@aos-harness/runtime/template-resolver");
57
57
  const { ConstraintEngine } = await import("@aos-harness/runtime/constraint-engine");
@@ -246,26 +246,38 @@ export async function validateCommand(args: ParsedArgs): Promise<void> {
246
246
  }
247
247
 
248
248
  // ── 5. Validate briefs ────────────────────────────────────────
249
+ //
250
+ // Briefs are authored per-profile; checking every brief against every
251
+ // profile's required_sections (a cross-product) produces noise — `incident-
252
+ // response` and `strategic-council` have different required sections by
253
+ // design. We only check each brief is well-formed (matches one of the
254
+ // canonical kinds: deliberation or execution). Run-time enforcement of
255
+ // profile-specific required sections happens in `aos run` — both via the
256
+ // brief lint summary and via the runtime config-loader's validateBrief
257
+ // against the chosen profile.
249
258
 
250
259
  console.log(`${c.bold("Validating briefs...")}`);
251
260
 
252
261
  const briefsDir = join(coreDir, "briefs");
253
262
  if (existsSync(briefsDir)) {
263
+ const { readFileSync } = await import("node:fs");
264
+ const { validateBrief: lintBrief } = await import("../brief/validate");
265
+
254
266
  for (const entry of readdirSync(briefsDir, { withFileTypes: true })) {
255
267
  if (!entry.isDirectory()) continue;
256
268
  const briefPath = join(briefsDir, entry.name, "brief.md");
257
269
  if (!existsSync(briefPath)) continue;
258
270
 
259
- // Validate against each profile's required sections
260
- for (const profile of profiles) {
261
- check(`Brief "${entry.name}" for profile "${profile.id}"`, () => {
262
- const result = validateBrief(briefPath, profile.input.required_sections);
263
- if (!result.valid) {
264
- const missingNames = result.missing.map((s: any) => s.heading).join(", ");
265
- throw new Error(`Missing sections: ${missingNames}. Add them to your brief.md and try again.`);
266
- }
267
- });
268
- }
271
+ check(`Brief "${entry.name}" is well-formed`, () => {
272
+ const content = readFileSync(briefPath, "utf-8");
273
+ const result = lintBrief(content);
274
+ if (result.errors.length > 0) {
275
+ throw new Error(result.errors.map((e) => e.message).join(" "));
276
+ }
277
+ if (result.warnings.length > 0) {
278
+ console.log(c.dim(` ${result.warnings.length} warning(s) — run \`aos brief validate ${briefPath}\` for details.`));
279
+ }
280
+ });
269
281
  }
270
282
  }
271
283
 
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { createCommand } from "./commands/create";
16
16
  import { validateCommand } from "./commands/validate";
17
17
  import { listCommand } from "./commands/list";
18
18
  import { replayCommand } from "./commands/replay";
19
+ import { briefCommand } from "./commands/brief";
19
20
  import { c, parseArgs } from "./colors";
20
21
  import { getCliVersion } from "./utils";
21
22
 
@@ -35,6 +36,7 @@ ${c.bold("COMMANDS")}
35
36
  ${c.cyan("create")} profile <name> Scaffold a new profile
36
37
  ${c.cyan("create")} domain <name> Scaffold a new domain
37
38
  ${c.cyan("create")} skill <name> Scaffold a new skill definition
39
+ ${c.cyan("brief")} <sub> Work with brief files (template/validate/save)
38
40
  ${c.cyan("replay")} <transcript.jsonl> Replay a deliberation transcript
39
41
  ${c.cyan("validate")} Validate all agents, profiles, domains, and skills
40
42
  ${c.cyan("list")} List all agents, profiles, domains, and skills
@@ -87,6 +89,9 @@ async function main(): Promise<void> {
87
89
  case "create":
88
90
  await createCommand(parsed);
89
91
  break;
92
+ case "brief":
93
+ await briefCommand(parsed);
94
+ break;
90
95
  case "replay":
91
96
  await replayCommand(parsed);
92
97
  break;