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.
- package/core/profiles/strategic-council/profile.yaml +1 -1
- package/package.json +7 -7
- package/src/adapter-session.ts +2 -0
- package/src/brief/parse.ts +58 -0
- package/src/brief/prompts.ts +77 -0
- package/src/brief/schema.ts +24 -0
- package/src/brief/template.ts +74 -0
- package/src/brief/types.ts +44 -0
- package/src/brief/validate.ts +97 -0
- package/src/brief/write.ts +35 -0
- package/src/commands/brief.ts +167 -0
- package/src/commands/create.ts +121 -6
- package/src/commands/run.ts +26 -2
- package/src/commands/validate.ts +24 -12
- package/src/index.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aos-harness",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
42
|
-
"@aos-harness/runtime": "0.
|
|
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.
|
|
50
|
-
"@aos-harness/codex-adapter": ">=0.
|
|
51
|
-
"@aos-harness/gemini-adapter": ">=0.
|
|
52
|
-
"@aos-harness/pi-adapter": ">=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": {
|
package/src/adapter-session.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/create.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 "
|
|
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
|
+
}
|
package/src/commands/run.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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;
|