orchestrator-workflow 0.1.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/INSTALL-AGENT.md +84 -0
- package/README.md +95 -0
- package/assets/agents/implementer.md +45 -0
- package/assets/agents/reviewer.md +49 -0
- package/assets/agents/task-slicer.md +44 -0
- package/assets/agents-md-section.md +66 -0
- package/assets/skill/SKILL.md +197 -0
- package/assets/templates/00-goal.md +25 -0
- package/assets/templates/01-plan.md +29 -0
- package/assets/templates/02-tasks.md +33 -0
- package/assets/templates/03-decisions.md +5 -0
- package/assets/templates/04-implementation-summary.md +33 -0
- package/assets/templates/05-review-findings.md +23 -0
- package/assets/templates/06-handoff.md +25 -0
- package/dist/assets.d.ts +17 -0
- package/dist/assets.js +34 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +152 -0
- package/dist/detect.d.ts +9 -0
- package/dist/detect.js +39 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/init.d.ts +29 -0
- package/dist/init.js +187 -0
- package/dist/models.d.ts +26 -0
- package/dist/models.js +72 -0
- package/dist/writers.d.ts +33 -0
- package/dist/writers.js +108 -0
- package/package.json +58 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Implementation Summary
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
not_started | in_progress | done | partial | blocked
|
|
6
|
+
|
|
7
|
+
## Completed Tasks
|
|
8
|
+
|
|
9
|
+
- <!-- T-001 -->
|
|
10
|
+
|
|
11
|
+
## Changed Files
|
|
12
|
+
|
|
13
|
+
| File | Reason |
|
|
14
|
+
|---|---|
|
|
15
|
+
| <!-- path --> | <!-- reason --> |
|
|
16
|
+
|
|
17
|
+
## Test Evidence
|
|
18
|
+
|
|
19
|
+
### Executed
|
|
20
|
+
|
|
21
|
+
- <!-- command/result -->
|
|
22
|
+
|
|
23
|
+
### Added or Updated
|
|
24
|
+
|
|
25
|
+
- <!-- test file -->
|
|
26
|
+
|
|
27
|
+
### Not Executed
|
|
28
|
+
|
|
29
|
+
<!-- Explain why, if applicable. -->
|
|
30
|
+
|
|
31
|
+
## Risks / Notes
|
|
32
|
+
|
|
33
|
+
- <!-- note -->
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Review Findings
|
|
2
|
+
|
|
3
|
+
## Review Summary
|
|
4
|
+
|
|
5
|
+
<!-- Short summary. -->
|
|
6
|
+
|
|
7
|
+
## Findings
|
|
8
|
+
|
|
9
|
+
| Severity | Category | Description | Suggested Fix | Decision |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| low/medium/high/critical | correctness/architecture/security/tests/maintainability/performance/docs | <!-- finding --> | <!-- fix --> | accepted/fix/defer/reject |
|
|
12
|
+
|
|
13
|
+
## Missing Tests
|
|
14
|
+
|
|
15
|
+
- <!-- missing test -->
|
|
16
|
+
|
|
17
|
+
## Residual Risks
|
|
18
|
+
|
|
19
|
+
- <!-- risk -->
|
|
20
|
+
|
|
21
|
+
## Acceptance Recommendation
|
|
22
|
+
|
|
23
|
+
accept | accept_with_notes | fix_required | reject
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Operator Handoff
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
<!-- What changed? -->
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
<!-- Why was this approach chosen? -->
|
|
10
|
+
|
|
11
|
+
## Verification
|
|
12
|
+
|
|
13
|
+
- <!-- tests/checks performed -->
|
|
14
|
+
|
|
15
|
+
## Known Risks
|
|
16
|
+
|
|
17
|
+
- <!-- risk or none -->
|
|
18
|
+
|
|
19
|
+
## Follow-Ups
|
|
20
|
+
|
|
21
|
+
- <!-- next steps or none -->
|
|
22
|
+
|
|
23
|
+
## Final Status
|
|
24
|
+
|
|
25
|
+
accepted | accepted_with_notes | needs_followup | blocked
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Role } from "./models.js";
|
|
2
|
+
/** Resolves from both src/ (tsx dev) and dist/ (built) to the package root. */
|
|
3
|
+
export declare const ASSETS_DIR: string;
|
|
4
|
+
export declare const PACKAGE_VERSION: string;
|
|
5
|
+
export declare function readAsset(relativePath: string): string;
|
|
6
|
+
export declare function listTemplateNames(): string[];
|
|
7
|
+
export interface AgentAsset {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The agent assets are the single source of truth for the role prompts. They
|
|
14
|
+
* carry a minimal `name` + `description` frontmatter; the harness-specific
|
|
15
|
+
* frontmatter (model, mode) is composed at install time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readAgentAsset(role: Role): AgentAsset;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** Resolves from both src/ (tsx dev) and dist/ (built) to the package root. */
|
|
5
|
+
export const ASSETS_DIR = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
6
|
+
export const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
7
|
+
export function readAsset(relativePath) {
|
|
8
|
+
return readFileSync(join(ASSETS_DIR, relativePath), "utf8");
|
|
9
|
+
}
|
|
10
|
+
export function listTemplateNames() {
|
|
11
|
+
return readdirSync(join(ASSETS_DIR, "templates"))
|
|
12
|
+
.filter((name) => name.endsWith(".md"))
|
|
13
|
+
.sort();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The agent assets are the single source of truth for the role prompts. They
|
|
17
|
+
* carry a minimal `name` + `description` frontmatter; the harness-specific
|
|
18
|
+
* frontmatter (model, mode) is composed at install time.
|
|
19
|
+
*/
|
|
20
|
+
export function readAgentAsset(role) {
|
|
21
|
+
const raw = readAsset(join("agents", `${role}.md`));
|
|
22
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n+([\s\S]*)$/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(`Agent asset for "${role}" has no frontmatter block`);
|
|
25
|
+
}
|
|
26
|
+
const [, frontmatter, body] = match;
|
|
27
|
+
const name = frontmatter.match(/^name: (.+)$/m)?.[1]?.trim();
|
|
28
|
+
const descriptionRaw = frontmatter.match(/^description: (.+)$/m)?.[1]?.trim();
|
|
29
|
+
if (!name || !descriptionRaw) {
|
|
30
|
+
throw new Error(`Agent asset for "${role}" is missing name or description`);
|
|
31
|
+
}
|
|
32
|
+
const description = descriptionRaw.replace(/^"(.*)"$/, "$1");
|
|
33
|
+
return { name, description, body };
|
|
34
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { PACKAGE_VERSION } from "./assets.js";
|
|
7
|
+
import { HARNESSES, detectHarnesses, parseHarnessList } from "./detect.js";
|
|
8
|
+
import { DEFAULT_MODELS, MODEL_ALIASES, ROLES, assertValidModelId, parseModelsSpec, } from "./models.js";
|
|
9
|
+
import { readInstalledManifest, runInit } from "./init.js";
|
|
10
|
+
function isInteractive() {
|
|
11
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
12
|
+
}
|
|
13
|
+
async function promptHarnesses(detected, installed) {
|
|
14
|
+
const known = [...new Set([...detected, ...installed])];
|
|
15
|
+
const preselected = known.length > 0 ? known : ["claude"];
|
|
16
|
+
const { harnesses } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: "checkbox",
|
|
19
|
+
name: "harnesses",
|
|
20
|
+
message: "Install adapters for which harnesses?",
|
|
21
|
+
choices: HARNESSES.map((harness) => ({
|
|
22
|
+
name: harness + (detected.includes(harness) ? " (detected)" : ""),
|
|
23
|
+
value: harness,
|
|
24
|
+
checked: preselected.includes(harness),
|
|
25
|
+
})),
|
|
26
|
+
validate: (selection) => selection.length > 0 || "Select at least one harness",
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
return harnesses;
|
|
30
|
+
}
|
|
31
|
+
async function promptModels(base) {
|
|
32
|
+
const models = { ...base };
|
|
33
|
+
for (const role of ROLES) {
|
|
34
|
+
const { choice } = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: "list",
|
|
37
|
+
name: "choice",
|
|
38
|
+
message: `Model for the ${role} subagent:`,
|
|
39
|
+
default: models[role],
|
|
40
|
+
choices: [
|
|
41
|
+
...MODEL_ALIASES.map((alias) => ({
|
|
42
|
+
name: alias === DEFAULT_MODELS[role] ? `${alias} (default)` : alias,
|
|
43
|
+
value: alias,
|
|
44
|
+
})),
|
|
45
|
+
{ name: "custom model id", value: "__custom__" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
if (choice === "__custom__") {
|
|
50
|
+
const { custom } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: "input",
|
|
53
|
+
name: "custom",
|
|
54
|
+
message: `Custom model id for ${role}:`,
|
|
55
|
+
validate: (value) => {
|
|
56
|
+
try {
|
|
57
|
+
assertValidModelId(value.trim());
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
models[role] = custom.trim();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
models[role] = choice;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return models;
|
|
73
|
+
}
|
|
74
|
+
const program = new Command();
|
|
75
|
+
program
|
|
76
|
+
.name("orchestrator-workflow")
|
|
77
|
+
.description("Install an orchestrator-led agent workflow into a repository: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions")
|
|
78
|
+
.version(PACKAGE_VERSION);
|
|
79
|
+
program
|
|
80
|
+
.command("init")
|
|
81
|
+
.description("Install or refresh the workflow kit in a target repository")
|
|
82
|
+
.argument("[dir]", "target repository directory", ".")
|
|
83
|
+
.option("-y, --yes", "accept all defaults and skip prompts")
|
|
84
|
+
.option("-f, --force", "overwrite kit-owned files that have local edits")
|
|
85
|
+
.option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: detected`)
|
|
86
|
+
.option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
|
|
87
|
+
.action(async (dir, opts) => {
|
|
88
|
+
const targetDir = resolve(dir);
|
|
89
|
+
if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) {
|
|
90
|
+
console.error(`Target is not a directory: ${targetDir}`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const interactive = !opts.yes && isInteractive();
|
|
95
|
+
const detected = detectHarnesses(targetDir);
|
|
96
|
+
console.log(detected.length > 0
|
|
97
|
+
? `Detected harness configs: ${detected.join(", ")}`
|
|
98
|
+
: "No existing harness configs detected");
|
|
99
|
+
// A previous install is the baseline; re-runs refresh it instead of
|
|
100
|
+
// resetting harnesses and models to the shipped defaults.
|
|
101
|
+
const previous = readInstalledManifest(targetDir);
|
|
102
|
+
if (previous) {
|
|
103
|
+
const version = previous.version || "unknown version";
|
|
104
|
+
const installedFor = previous.harnesses.length > 0
|
|
105
|
+
? previous.harnesses.join(", ")
|
|
106
|
+
: "none recorded";
|
|
107
|
+
console.log(`Found existing install (${version.startsWith("unknown") ? version : `v${version}`}, harnesses: ${installedFor})`);
|
|
108
|
+
}
|
|
109
|
+
let harnesses;
|
|
110
|
+
if (opts.harness) {
|
|
111
|
+
harnesses = parseHarnessList(opts.harness);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const installed = previous?.harnesses ?? [];
|
|
115
|
+
const fallback = [...new Set([...detected, ...installed])];
|
|
116
|
+
harnesses = interactive
|
|
117
|
+
? await promptHarnesses(detected, installed)
|
|
118
|
+
: fallback.length > 0
|
|
119
|
+
? fallback
|
|
120
|
+
: ["claude"];
|
|
121
|
+
}
|
|
122
|
+
let models = {
|
|
123
|
+
...DEFAULT_MODELS,
|
|
124
|
+
...(previous?.models ?? {}),
|
|
125
|
+
};
|
|
126
|
+
if (opts.models)
|
|
127
|
+
models = parseModelsSpec(opts.models, models);
|
|
128
|
+
if (interactive && !opts.models)
|
|
129
|
+
models = await promptModels(models);
|
|
130
|
+
const report = runInit({
|
|
131
|
+
targetDir,
|
|
132
|
+
harnesses,
|
|
133
|
+
models,
|
|
134
|
+
force: opts.force,
|
|
135
|
+
});
|
|
136
|
+
const show = (label, paths) => {
|
|
137
|
+
if (paths.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
console.log(`${label}:`);
|
|
140
|
+
for (const path of paths)
|
|
141
|
+
console.log(` ${path}`);
|
|
142
|
+
};
|
|
143
|
+
show("Created", report.written);
|
|
144
|
+
show("Updated", report.updated);
|
|
145
|
+
show("Unchanged", report.skipped);
|
|
146
|
+
show("Conflicts (local edits kept, re-run with --force to overwrite)", report.conflicted);
|
|
147
|
+
console.log(`\norchestrator-workflow v${PACKAGE_VERSION} installed for: ${harnesses.join(", ")}`);
|
|
148
|
+
});
|
|
149
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
150
|
+
console.error(error instanceof Error ? error.message : error);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
});
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Harness = "claude" | "codex" | "opencode";
|
|
2
|
+
export declare const HARNESSES: Harness[];
|
|
3
|
+
/**
|
|
4
|
+
* Best-effort detection of which harnesses a target repository already uses.
|
|
5
|
+
* AGENTS.md alone is deliberately not a signal: every supported harness can
|
|
6
|
+
* consume it, so it does not identify one.
|
|
7
|
+
*/
|
|
8
|
+
export declare function detectHarnesses(dir: string): Harness[];
|
|
9
|
+
export declare function parseHarnessList(list: string): Harness[];
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const HARNESSES = ["claude", "codex", "opencode"];
|
|
4
|
+
function anyExists(dir, names) {
|
|
5
|
+
return names.some((name) => existsSync(join(dir, name)));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort detection of which harnesses a target repository already uses.
|
|
9
|
+
* AGENTS.md alone is deliberately not a signal: every supported harness can
|
|
10
|
+
* consume it, so it does not identify one.
|
|
11
|
+
*/
|
|
12
|
+
export function detectHarnesses(dir) {
|
|
13
|
+
const detected = [];
|
|
14
|
+
if (anyExists(dir, [".claude", "CLAUDE.md"]))
|
|
15
|
+
detected.push("claude");
|
|
16
|
+
if (anyExists(dir, [".agents", ".codex"]))
|
|
17
|
+
detected.push("codex");
|
|
18
|
+
if (anyExists(dir, [".opencode", "opencode.json", "opencode.jsonc"])) {
|
|
19
|
+
detected.push("opencode");
|
|
20
|
+
}
|
|
21
|
+
return detected;
|
|
22
|
+
}
|
|
23
|
+
export function parseHarnessList(list) {
|
|
24
|
+
const parsed = [];
|
|
25
|
+
for (const entry of list.split(",")) {
|
|
26
|
+
const name = entry.trim().toLowerCase();
|
|
27
|
+
if (name === "")
|
|
28
|
+
continue;
|
|
29
|
+
if (!HARNESSES.includes(name)) {
|
|
30
|
+
throw new Error(`Unknown harness "${name}"; valid values: ${HARNESSES.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
if (!parsed.includes(name))
|
|
33
|
+
parsed.push(name);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.length === 0) {
|
|
36
|
+
throw new Error("--harness was given but contained no harness names");
|
|
37
|
+
}
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { runInit } from "./init.js";
|
|
2
|
+
export type { InitOptions } from "./init.js";
|
|
3
|
+
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
4
|
+
export type { Harness } from "./detect.js";
|
|
5
|
+
export { DEFAULT_MODELS, MODEL_ALIASES, ROLES, claudeModelValue, opencodeModelValue, parseModelsSpec, } from "./models.js";
|
|
6
|
+
export type { ModelAlias, Role } from "./models.js";
|
|
7
|
+
export type { Report } from "./writers.js";
|
|
8
|
+
export { PACKAGE_VERSION } from "./assets.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { runInit } from "./init.js";
|
|
2
|
+
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
3
|
+
export { DEFAULT_MODELS, MODEL_ALIASES, ROLES, claudeModelValue, opencodeModelValue, parseModelsSpec, } from "./models.js";
|
|
4
|
+
export { PACKAGE_VERSION } from "./assets.js";
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Harness } from "./detect.js";
|
|
2
|
+
import type { Role } from "./models.js";
|
|
3
|
+
import type { Report } from "./writers.js";
|
|
4
|
+
export interface InitOptions {
|
|
5
|
+
targetDir: string;
|
|
6
|
+
harnesses: Harness[];
|
|
7
|
+
models: Record<Role, string>;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface Manifest {
|
|
11
|
+
kit: string;
|
|
12
|
+
version: string;
|
|
13
|
+
harnesses: Harness[];
|
|
14
|
+
models: Record<Role, string>;
|
|
15
|
+
/**
|
|
16
|
+
* sha256 of every kit-owned file as installed. This is how a re-run tells
|
|
17
|
+
* "upstream changed, safe to update" apart from "user edited, conflict".
|
|
18
|
+
*/
|
|
19
|
+
files: Record<string, string>;
|
|
20
|
+
installedAt: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
24
|
+
* by hand (manual agent installs) or damaged, so every field is sanitized;
|
|
25
|
+
* anything invalid degrades to "no record" instead of crashing or leaking
|
|
26
|
+
* unvalidated values into generated frontmatter.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readInstalledManifest(targetDir: string): Manifest | undefined;
|
|
29
|
+
export declare function runInit(options: InitOptions): Report;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PACKAGE_VERSION, listTemplateNames, readAgentAsset, readAsset, } from "./assets.js";
|
|
5
|
+
import { HARNESSES } from "./detect.js";
|
|
6
|
+
import { ROLES, assertValidModelId, claudeModelValue, opencodeModelValue, } from "./models.js";
|
|
7
|
+
import { emptyReport, ensureClaudeImport, installFile, upsertMarkerSection, } from "./writers.js";
|
|
8
|
+
const SKILL_NAME = "orchestrator-workflow";
|
|
9
|
+
const MANIFEST_PATH = join(".ai", "workflow", "manifest.json");
|
|
10
|
+
function sha256(content) {
|
|
11
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads the manifest of a previous install, if any. Manifests can be written
|
|
15
|
+
* by hand (manual agent installs) or damaged, so every field is sanitized;
|
|
16
|
+
* anything invalid degrades to "no record" instead of crashing or leaking
|
|
17
|
+
* unvalidated values into generated frontmatter.
|
|
18
|
+
*/
|
|
19
|
+
export function readInstalledManifest(targetDir) {
|
|
20
|
+
const path = join(targetDir, MANIFEST_PATH);
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return undefined;
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (typeof raw !== "object" || raw === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
const candidate = raw;
|
|
33
|
+
if (candidate.kit !== SKILL_NAME)
|
|
34
|
+
return undefined;
|
|
35
|
+
const harnesses = (Array.isArray(candidate.harnesses) ? candidate.harnesses : []).filter((value) => HARNESSES.includes(value));
|
|
36
|
+
const models = {};
|
|
37
|
+
if (typeof candidate.models === "object" && candidate.models !== null) {
|
|
38
|
+
for (const role of ROLES) {
|
|
39
|
+
const value = candidate.models[role];
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
assertValidModelId(value);
|
|
44
|
+
models[role] = value;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Invalid model ids are dropped; the role falls back to defaults.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const files = {};
|
|
52
|
+
if (typeof candidate.files === "object" && candidate.files !== null) {
|
|
53
|
+
for (const [key, value] of Object.entries(candidate.files)) {
|
|
54
|
+
if (typeof value === "string")
|
|
55
|
+
files[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
kit: SKILL_NAME,
|
|
60
|
+
version: typeof candidate.version === "string" ? candidate.version : "",
|
|
61
|
+
harnesses,
|
|
62
|
+
models: models,
|
|
63
|
+
files,
|
|
64
|
+
installedAt: typeof candidate.installedAt === "string" ? candidate.installedAt : "",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function yamlQuote(value) {
|
|
68
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
69
|
+
}
|
|
70
|
+
function composeClaudeAgent(role, model) {
|
|
71
|
+
const asset = readAgentAsset(role);
|
|
72
|
+
return [
|
|
73
|
+
"---",
|
|
74
|
+
`name: ${asset.name}`,
|
|
75
|
+
`description: ${yamlQuote(asset.description)}`,
|
|
76
|
+
`model: ${claudeModelValue(model)}`,
|
|
77
|
+
"---",
|
|
78
|
+
"",
|
|
79
|
+
asset.body.trimEnd(),
|
|
80
|
+
"",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
function composeOpencodeAgent(role, model) {
|
|
84
|
+
const asset = readAgentAsset(role);
|
|
85
|
+
return [
|
|
86
|
+
"---",
|
|
87
|
+
`description: ${yamlQuote(asset.description)}`,
|
|
88
|
+
"mode: subagent",
|
|
89
|
+
`model: ${opencodeModelValue(model)}`,
|
|
90
|
+
"---",
|
|
91
|
+
"",
|
|
92
|
+
asset.body.trimEnd(),
|
|
93
|
+
"",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
export function runInit(options) {
|
|
97
|
+
const { targetDir } = options;
|
|
98
|
+
if (!existsSync(targetDir)) {
|
|
99
|
+
throw new Error(`Target directory does not exist: ${targetDir}`);
|
|
100
|
+
}
|
|
101
|
+
if (!statSync(targetDir).isDirectory()) {
|
|
102
|
+
throw new Error(`Target is not a directory: ${targetDir}`);
|
|
103
|
+
}
|
|
104
|
+
const force = options.force ?? false;
|
|
105
|
+
const report = emptyReport();
|
|
106
|
+
const previous = readInstalledManifest(targetDir);
|
|
107
|
+
const installedFiles = {};
|
|
108
|
+
/**
|
|
109
|
+
* Installs a kit-owned file. An unedited file (it still matches the hash
|
|
110
|
+
* recorded at install time) is updated in place when the kit content
|
|
111
|
+
* changed; a locally edited file is only overwritten with --force.
|
|
112
|
+
*/
|
|
113
|
+
const installKitFile = (relativePath, content) => {
|
|
114
|
+
const path = join(targetDir, relativePath);
|
|
115
|
+
const recorded = previous?.files?.[relativePath];
|
|
116
|
+
if (existsSync(path)) {
|
|
117
|
+
const existing = readFileSync(path, "utf8");
|
|
118
|
+
const unedited = recorded !== undefined && sha256(existing) === recorded;
|
|
119
|
+
installFile(report, path, content, { force: force || unedited });
|
|
120
|
+
if (readFileSync(path, "utf8") === content) {
|
|
121
|
+
installedFiles[relativePath] = sha256(content);
|
|
122
|
+
}
|
|
123
|
+
else if (recorded !== undefined) {
|
|
124
|
+
// Conflicted: the user's edit stays, and so does the original record.
|
|
125
|
+
installedFiles[relativePath] = recorded;
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
installFile(report, path, content, { force });
|
|
130
|
+
installedFiles[relativePath] = sha256(content);
|
|
131
|
+
};
|
|
132
|
+
for (const name of listTemplateNames()) {
|
|
133
|
+
installKitFile(join(".ai", "workflow", "templates", name), readAsset(join("templates", name)));
|
|
134
|
+
}
|
|
135
|
+
installKitFile(join(".ai", "runs", ".gitkeep"), "");
|
|
136
|
+
// Codex and opencode read AGENTS.md natively; Claude Code gets it via the
|
|
137
|
+
// CLAUDE.md import. The policy section is therefore installed regardless of
|
|
138
|
+
// the harness selection. AGENTS.md and CLAUDE.md are user-owned: only the
|
|
139
|
+
// fenced section and the import line are ever touched.
|
|
140
|
+
upsertMarkerSection(report, join(targetDir, "AGENTS.md"), readAsset("agents-md-section.md"));
|
|
141
|
+
const skill = readAsset(join("skill", "SKILL.md"));
|
|
142
|
+
if (options.harnesses.includes("claude")) {
|
|
143
|
+
installKitFile(join(".claude", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
144
|
+
for (const role of ROLES) {
|
|
145
|
+
installKitFile(join(".claude", "agents", `${role}.md`), composeClaudeAgent(role, options.models[role]));
|
|
146
|
+
}
|
|
147
|
+
ensureClaudeImport(report, join(targetDir, "CLAUDE.md"));
|
|
148
|
+
}
|
|
149
|
+
if (options.harnesses.includes("codex")) {
|
|
150
|
+
installKitFile(join(".agents", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
151
|
+
}
|
|
152
|
+
if (options.harnesses.includes("opencode")) {
|
|
153
|
+
for (const role of ROLES) {
|
|
154
|
+
installKitFile(join(".opencode", "agents", `${role}.md`), composeOpencodeAgent(role, options.models[role]));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// The manifest records applied state, so it is written last and only when
|
|
158
|
+
// something actually differs; a plain re-run stays a byte-for-byte no-op.
|
|
159
|
+
const desired = {
|
|
160
|
+
kit: SKILL_NAME,
|
|
161
|
+
version: PACKAGE_VERSION,
|
|
162
|
+
harnesses: [...options.harnesses].sort(),
|
|
163
|
+
models: options.models,
|
|
164
|
+
files: installedFiles,
|
|
165
|
+
};
|
|
166
|
+
const manifestPath = join(targetDir, MANIFEST_PATH);
|
|
167
|
+
if (previous &&
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
kit: previous.kit,
|
|
170
|
+
version: previous.version,
|
|
171
|
+
harnesses: previous.harnesses,
|
|
172
|
+
models: previous.models,
|
|
173
|
+
files: previous.files,
|
|
174
|
+
}) === JSON.stringify(desired)) {
|
|
175
|
+
report.skipped.push(manifestPath);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const manifest = {
|
|
179
|
+
...desired,
|
|
180
|
+
installedAt: previous?.installedAt || new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
installFile(report, manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
183
|
+
force: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return report;
|
|
187
|
+
}
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Role = "task-slicer" | "implementer" | "reviewer";
|
|
2
|
+
export declare const ROLES: Role[];
|
|
3
|
+
export type ModelAlias = "sonnet" | "opus" | "haiku";
|
|
4
|
+
export declare const MODEL_ALIASES: ModelAlias[];
|
|
5
|
+
/**
|
|
6
|
+
* Per-role defaults. The orchestrator itself runs on the session model and is
|
|
7
|
+
* deliberately not configured here.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_MODELS: Record<Role, string>;
|
|
10
|
+
export declare function isModelAlias(value: string): value is ModelAlias;
|
|
11
|
+
/**
|
|
12
|
+
* Claude Code subagent frontmatter accepts the aliases directly as well as
|
|
13
|
+
* full model ids, so the chosen value passes through unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export declare function claudeModelValue(model: string): string;
|
|
16
|
+
export declare function opencodeModelValue(model: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Model values are interpolated into YAML frontmatter as plain scalars;
|
|
19
|
+
* reject anything that could break out of that position.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertValidModelId(model: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Parses a `--models` spec like `implementer=haiku,reviewer=opus` on top of
|
|
24
|
+
* the given base mapping. Unknown roles and empty values are rejected.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseModelsSpec(spec: string, base: Record<Role, string>): Record<Role, string>;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const ROLES = ["task-slicer", "implementer", "reviewer"];
|
|
2
|
+
export const MODEL_ALIASES = ["sonnet", "opus", "haiku"];
|
|
3
|
+
/**
|
|
4
|
+
* Per-role defaults. The orchestrator itself runs on the session model and is
|
|
5
|
+
* deliberately not configured here.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_MODELS = {
|
|
8
|
+
"task-slicer": "sonnet",
|
|
9
|
+
implementer: "sonnet",
|
|
10
|
+
reviewer: "opus",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* opencode expects fully qualified `provider/model-id` strings (models.dev
|
|
14
|
+
* ids). These are the current Anthropic ids for the three aliases; targets
|
|
15
|
+
* with a different provider setup can pass a custom id instead.
|
|
16
|
+
*/
|
|
17
|
+
const OPENCODE_MODEL_IDS = {
|
|
18
|
+
sonnet: "anthropic/claude-sonnet-4-6",
|
|
19
|
+
opus: "anthropic/claude-opus-4-8",
|
|
20
|
+
haiku: "anthropic/claude-haiku-4-5",
|
|
21
|
+
};
|
|
22
|
+
export function isModelAlias(value) {
|
|
23
|
+
return MODEL_ALIASES.includes(value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Claude Code subagent frontmatter accepts the aliases directly as well as
|
|
27
|
+
* full model ids, so the chosen value passes through unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export function claudeModelValue(model) {
|
|
30
|
+
return model;
|
|
31
|
+
}
|
|
32
|
+
export function opencodeModelValue(model) {
|
|
33
|
+
if (isModelAlias(model))
|
|
34
|
+
return OPENCODE_MODEL_IDS[model];
|
|
35
|
+
return model.includes("/") ? model : `anthropic/${model}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Model values are interpolated into YAML frontmatter as plain scalars;
|
|
39
|
+
* reject anything that could break out of that position.
|
|
40
|
+
*/
|
|
41
|
+
export function assertValidModelId(model) {
|
|
42
|
+
if (model.length === 0) {
|
|
43
|
+
throw new Error("Model id must not be empty");
|
|
44
|
+
}
|
|
45
|
+
if (/[:"'#\n\\]/.test(model) || model !== model.trim()) {
|
|
46
|
+
throw new Error(`Invalid model id "${model}"; expected an alias (${MODEL_ALIASES.join(", ")}) or a plain id like anthropic/claude-opus-4-8`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parses a `--models` spec like `implementer=haiku,reviewer=opus` on top of
|
|
51
|
+
* the given base mapping. Unknown roles and empty values are rejected.
|
|
52
|
+
*/
|
|
53
|
+
export function parseModelsSpec(spec, base) {
|
|
54
|
+
const result = { ...base };
|
|
55
|
+
for (const pair of spec.split(",")) {
|
|
56
|
+
const trimmed = pair.trim();
|
|
57
|
+
if (trimmed === "")
|
|
58
|
+
continue;
|
|
59
|
+
const eq = trimmed.indexOf("=");
|
|
60
|
+
if (eq <= 0 || eq === trimmed.length - 1) {
|
|
61
|
+
throw new Error(`Invalid --models entry "${trimmed}"; expected role=model`);
|
|
62
|
+
}
|
|
63
|
+
const role = trimmed.slice(0, eq).trim();
|
|
64
|
+
const model = trimmed.slice(eq + 1).trim();
|
|
65
|
+
if (!ROLES.includes(role)) {
|
|
66
|
+
throw new Error(`Unknown role "${role}" in --models; valid roles: ${ROLES.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
assertValidModelId(model);
|
|
69
|
+
result[role] = model;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|