fullstack-agentic-flow 2.0.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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/src/cli/args.js +74 -0
- package/dist/src/cli/commands/doctor.js +133 -0
- package/dist/src/cli/commands/init.js +103 -0
- package/dist/src/cli/commands/shared.js +20 -0
- package/dist/src/cli/commands/tools.js +21 -0
- package/dist/src/cli/commands/update.js +21 -0
- package/dist/src/cli/main.js +6 -0
- package/dist/src/cli/output.js +11 -0
- package/dist/src/cli/prompter.js +13 -0
- package/dist/src/cli/report.js +13 -0
- package/dist/src/cli/run.js +48 -0
- package/dist/src/domain/canon-mapping.js +26 -0
- package/dist/src/domain/claude-md-mode.js +17 -0
- package/dist/src/domain/enum-parse.js +9 -0
- package/dist/src/domain/errors.js +13 -0
- package/dist/src/domain/pipeline-config.js +58 -0
- package/dist/src/domain/runtime.js +19 -0
- package/dist/src/domain/tool.js +129 -0
- package/dist/src/domain/topology.js +39 -0
- package/dist/src/install/adapters.js +39 -0
- package/dist/src/install/applier.js +150 -0
- package/dist/src/install/detect.js +70 -0
- package/dist/src/install/managed-section.js +35 -0
- package/dist/src/install/manifest.js +63 -0
- package/dist/src/install/planned-file.js +77 -0
- package/dist/src/install/planner.js +139 -0
- package/dist/src/install/rules-preset.js +37 -0
- package/dist/src/install/setup-guide.js +60 -0
- package/dist/src/io/fs.js +39 -0
- package/dist/src/io/paths.js +6 -0
- package/dist/src/io/shell.js +25 -0
- package/dist/src/registry/commands.js +52 -0
- package/dist/src/registry/subagents.js +52 -0
- package/dist/src/registry/tools.js +99 -0
- package/package.json +53 -0
- package/payload/ai-agents/agents/00-bootstrap.md +473 -0
- package/payload/ai-agents/agents/00a-scaffold.md +295 -0
- package/payload/ai-agents/agents/00b-module-scaffold.md +108 -0
- package/payload/ai-agents/agents/01-intake.md +308 -0
- package/payload/ai-agents/agents/01b-impact-analysis.md +274 -0
- package/payload/ai-agents/agents/02a-dependency-map.md +247 -0
- package/payload/ai-agents/agents/02b-backend-contract.md +434 -0
- package/payload/ai-agents/agents/02c-ui-contract.md +367 -0
- package/payload/ai-agents/agents/02d-interface-seam.md +373 -0
- package/payload/ai-agents/agents/03a-backend-sequence.md +233 -0
- package/payload/ai-agents/agents/03b-ui-sequence.md +230 -0
- package/payload/ai-agents/agents/04a-backend-implementer.md +290 -0
- package/payload/ai-agents/agents/04b-ui-implementer.md +302 -0
- package/payload/ai-agents/agents/05-ci-validator.md +125 -0
- package/payload/ai-agents/agents/06-qa-spec.md +249 -0
- package/payload/ai-agents/agents/07-security.md +228 -0
- package/payload/ai-agents/agents/08-performance.md +208 -0
- package/payload/ai-agents/agents/09a-feature-notes.md +156 -0
- package/payload/ai-agents/agents/09b-release.md +111 -0
- package/payload/ai-agents/architecture-context.template.md +554 -0
- package/payload/ai-agents/archive/README.md +22 -0
- package/payload/ai-agents/canon/backend-canon.md +295 -0
- package/payload/ai-agents/canon/frontend-canon.md +47 -0
- package/payload/ai-agents/canon/typed-data.md +158 -0
- package/payload/ai-agents/observations/README.md +11 -0
- package/payload/ai-agents/scripts/check-test-with-change.mjs +103 -0
- package/payload/ai-agents/scripts/check-typed-boundaries.mjs +329 -0
- package/payload/ai-agents/scripts/lib/config.mjs +63 -0
- package/payload/ai-agents/scripts/lib/glob.mjs +40 -0
- package/payload/ai-agents/state/README.md +108 -0
- package/payload/ai-agents/state/current-stage.md +10 -0
- package/payload/ai-agents/templates/README.md +22 -0
- package/payload/ai-agents/templates/ci/github-actions.rules.yml +35 -0
- package/payload/ai-agents/templates/ci/gitlab-ci.rules.yml +30 -0
- package/payload/ai-agents/toolchain.md +214 -0
- package/payload/commands/bootstrap.md +17 -0
- package/payload/commands/contract.md +64 -0
- package/payload/commands/finalize.md +26 -0
- package/payload/commands/impact.md +25 -0
- package/payload/commands/implement.md +58 -0
- package/payload/commands/intake.md +16 -0
- package/payload/commands/perf.md +20 -0
- package/payload/commands/qa.md +15 -0
- package/payload/commands/release.md +22 -0
- package/payload/commands/resume.md +52 -0
- package/payload/commands/scaffold-module.md +20 -0
- package/payload/commands/scaffold.md +35 -0
- package/payload/commands/seam.md +28 -0
- package/payload/commands/security.md +16 -0
- package/payload/commands/sequence.md +22 -0
- package/payload/commands/status.md +37 -0
- package/payload/legacy/v1-checksums.json +197 -0
- package/payload/root/contracts-README.md +19 -0
- package/payload/root/pipeline-section.md +33 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ClaudeMdMode, parseClaudeMdMode } from '../../domain/claude-md-mode.js';
|
|
2
|
+
import { InvalidOptionError } from '../../domain/errors.js';
|
|
3
|
+
import { PipelineConfig } from '../../domain/pipeline-config.js';
|
|
4
|
+
import { ALL_RUNTIMES, parseRuntimes } from '../../domain/runtime.js';
|
|
5
|
+
import { ALL_TOOL_IDS, StepLocation, parseToolIds } from '../../domain/tool.js';
|
|
6
|
+
import { ALL_TOPOLOGIES, describeTopology, parseTopology } from '../../domain/topology.js';
|
|
7
|
+
import { ApplyOptions } from '../../install/applier.js';
|
|
8
|
+
import { detectStack } from '../../install/detect.js';
|
|
9
|
+
import { TOOL_CATALOG } from '../../registry/tools.js';
|
|
10
|
+
import { onPath, runShell } from '../../io/shell.js';
|
|
11
|
+
import { printApplyReport } from '../report.js';
|
|
12
|
+
import { installFiles, packageVersion } from './shared.js';
|
|
13
|
+
async function askUntilValid(prompter, output, question, fallback, parse) {
|
|
14
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
15
|
+
const answer = await prompter.ask(question);
|
|
16
|
+
try {
|
|
17
|
+
return parse(answer === '' ? fallback : answer);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (!(error instanceof InvalidOptionError))
|
|
21
|
+
throw error;
|
|
22
|
+
output.warn(error.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new InvalidOptionError('answer', 'three invalid attempts', ['a valid value']);
|
|
26
|
+
}
|
|
27
|
+
async function chooseTopology(args, prompter, output) {
|
|
28
|
+
if (args.topology !== null)
|
|
29
|
+
return parseTopology(args.topology);
|
|
30
|
+
if (args.yes) {
|
|
31
|
+
throw new InvalidOptionError('--topology', '(missing)', ALL_TOPOLOGIES);
|
|
32
|
+
}
|
|
33
|
+
output.info('Topologies:');
|
|
34
|
+
for (const topology of ALL_TOPOLOGIES)
|
|
35
|
+
output.info(` ${topology.padEnd(15)} ${describeTopology(topology)}`);
|
|
36
|
+
return askUntilValid(prompter, output, `Topology? [${ALL_TOPOLOGIES.join('|')}]`, '', parseTopology);
|
|
37
|
+
}
|
|
38
|
+
async function chooseRuntimes(args, prompter, output) {
|
|
39
|
+
if (args.runtime !== null)
|
|
40
|
+
return parseRuntimes(args.runtime);
|
|
41
|
+
if (args.yes)
|
|
42
|
+
return ALL_RUNTIMES;
|
|
43
|
+
return askUntilValid(prompter, output, `Runtimes? comma list of ${ALL_RUNTIMES.join(',')} [default: both]`, ALL_RUNTIMES.join(','), parseRuntimes);
|
|
44
|
+
}
|
|
45
|
+
async function chooseTools(args, prompter, output) {
|
|
46
|
+
if (args.tools !== null)
|
|
47
|
+
return parseToolIds(args.tools);
|
|
48
|
+
if (args.yes)
|
|
49
|
+
return ALL_TOOL_IDS;
|
|
50
|
+
output.info('Tools:');
|
|
51
|
+
for (const tool of TOOL_CATALOG.all())
|
|
52
|
+
output.info(` ${tool.id.padEnd(18)} ${tool.purpose}`);
|
|
53
|
+
return askUntilValid(prompter, output, 'Tools? all | none | comma list [default: all]', 'all', parseToolIds);
|
|
54
|
+
}
|
|
55
|
+
function runToolSteps(config, target, output) {
|
|
56
|
+
for (const tool of TOOL_CATALOG.forIds(config.tools)) {
|
|
57
|
+
for (const runtime of config.runtimes) {
|
|
58
|
+
for (const step of tool.stepsFor(runtime)) {
|
|
59
|
+
if (step.location === StepLocation.AgentUi) {
|
|
60
|
+
output.info(`[${tool.name}] do this yourself: ${step.command}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const missing = step.requires.filter((binary) => !onPath(binary));
|
|
64
|
+
if (missing.length > 0) {
|
|
65
|
+
output.warn(`[${tool.name}] skipped "${step.command}" — not on PATH: ${missing.join(', ')}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
output.info(`[${tool.name}] $ ${step.command}`);
|
|
69
|
+
const result = runShell(step.command, target);
|
|
70
|
+
if (!result.succeeded)
|
|
71
|
+
output.warn(`[${tool.name}] exited ${result.exitCode}; see .ai-agents/SETUP.md`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function runInit(args, prompter, output) {
|
|
77
|
+
const detected = await detectStack(args.target);
|
|
78
|
+
output.info(`Target: ${args.target}`);
|
|
79
|
+
output.info(`Detected: ${detected.evidence}${detected.isGitRepository ? '' : ' (not a git repository yet)'}`);
|
|
80
|
+
const topology = await chooseTopology(args, prompter, output);
|
|
81
|
+
const runtimes = await chooseRuntimes(args, prompter, output);
|
|
82
|
+
const tools = await chooseTools(args, prompter, output);
|
|
83
|
+
const config = new PipelineConfig(packageVersion(), topology, runtimes, tools, detected.mapping, new Date().toISOString());
|
|
84
|
+
const claudeMd = args.claudeMd === null ? ClaudeMdMode.Import : parseClaudeMdMode(args.claudeMd);
|
|
85
|
+
const report = await installFiles(args.target, config, new ApplyOptions(args.force, args.dryRun), claudeMd);
|
|
86
|
+
printApplyReport(report, output);
|
|
87
|
+
if (args.dryRun)
|
|
88
|
+
return 0;
|
|
89
|
+
const shellSteps = TOOL_CATALOG.forIds(tools).some((tool) => runtimes.some((runtime) => tool.stepsFor(runtime).length > 0));
|
|
90
|
+
let runSteps = args.runTools;
|
|
91
|
+
if (!runSteps && !args.yes && shellSteps) {
|
|
92
|
+
const answer = await prompter.ask('Run the shell install steps for these tools now? [y/N]');
|
|
93
|
+
runSteps = /^y(es)?$/i.test(answer);
|
|
94
|
+
}
|
|
95
|
+
if (runSteps)
|
|
96
|
+
runToolSteps(config, args.target, output);
|
|
97
|
+
output.info('');
|
|
98
|
+
output.info('Installed. Next:');
|
|
99
|
+
output.info(' 1. Review .ai-agents/SETUP.md for any tool steps still to do');
|
|
100
|
+
output.info(' 2. Empty repo → /scaffold then /bootstrap. Existing code → /bootstrap');
|
|
101
|
+
output.info(' 3. Commit .ai-agents/, .claude/ and/or .agents/, CLAUDE.md/AGENTS.md');
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT, PAYLOAD_DIR } from '../../io/paths.js';
|
|
4
|
+
import { InstallManifest } from '../../install/manifest.js';
|
|
5
|
+
import { applyPlan } from '../../install/applier.js';
|
|
6
|
+
import { buildPlan } from '../../install/planner.js';
|
|
7
|
+
export function packageVersion() {
|
|
8
|
+
const raw = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
9
|
+
const version = typeof raw === 'object' && raw !== null ? raw.version : undefined;
|
|
10
|
+
return typeof version === 'string' ? version : '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
export async function installFiles(target, config, options, claudeMd) {
|
|
13
|
+
const plan = await buildPlan(PAYLOAD_DIR, config, target, claudeMd);
|
|
14
|
+
const legacy = await InstallManifest.loadFile(join(PAYLOAD_DIR, 'legacy', 'v1-checksums.json'));
|
|
15
|
+
const { report, manifest } = await applyPlan(target, plan, await InstallManifest.load(target, legacy), options);
|
|
16
|
+
if (!options.dryRun) {
|
|
17
|
+
await manifest.withVersion(config.pipelineVersion).save(target);
|
|
18
|
+
}
|
|
19
|
+
return report;
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ALL_RUNTIMES } from '../../domain/runtime.js';
|
|
2
|
+
import { TOOL_CATALOG } from '../../registry/tools.js';
|
|
3
|
+
import { parseRuntimes } from '../../domain/runtime.js';
|
|
4
|
+
export function runTools(args, output) {
|
|
5
|
+
const runtimes = args.runtime === null ? ALL_RUNTIMES : parseRuntimes(args.runtime);
|
|
6
|
+
for (const tool of TOOL_CATALOG.all()) {
|
|
7
|
+
output.info(`${tool.name} (${tool.source}, ${tool.role})`);
|
|
8
|
+
output.info(` ${tool.purpose}`);
|
|
9
|
+
for (const runtime of runtimes) {
|
|
10
|
+
const support = tool.supportFor(runtime);
|
|
11
|
+
if (support === null)
|
|
12
|
+
continue;
|
|
13
|
+
for (const step of support.steps)
|
|
14
|
+
output.info(` [${runtime}] ${step.command}${step.verified ? '' : ' (unverified)'}`);
|
|
15
|
+
if (support.caveat.length > 0)
|
|
16
|
+
output.info(` [${runtime}] note: ${support.caveat}`);
|
|
17
|
+
}
|
|
18
|
+
output.info('');
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ClaudeMdMode, parseClaudeMdMode } from '../../domain/claude-md-mode.js';
|
|
3
|
+
import { PipelineConfig } from '../../domain/pipeline-config.js';
|
|
4
|
+
import { ApplyOptions } from '../../install/applier.js';
|
|
5
|
+
import { readTextOrNull } from '../../io/fs.js';
|
|
6
|
+
import { printApplyReport } from '../report.js';
|
|
7
|
+
import { installFiles, packageVersion } from './shared.js';
|
|
8
|
+
export async function runUpdate(args, output) {
|
|
9
|
+
const text = await readTextOrNull(join(args.target, '.ai-agents', 'pipeline.config.json'));
|
|
10
|
+
if (text === null) {
|
|
11
|
+
output.error('No .ai-agents/pipeline.config.json here. Run `npx fullstack-agentic-flow init` first.');
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
const previous = PipelineConfig.parse(text);
|
|
15
|
+
const config = new PipelineConfig(packageVersion(), previous.topology, previous.runtimes, previous.tools, previous.mapping, previous.installedAt);
|
|
16
|
+
output.info(`Updating ${args.target} from pipeline ${previous.pipelineVersion} to ${config.pipelineVersion}.`);
|
|
17
|
+
const claudeMd = args.claudeMd === null ? ClaudeMdMode.Import : parseClaudeMdMode(args.claudeMd);
|
|
18
|
+
const report = await installFiles(args.target, config, new ApplyOptions(args.force, args.dryRun), claudeMd);
|
|
19
|
+
printApplyReport(report, output);
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from './run.js';
|
|
3
|
+
import { ConsoleOutput } from './output.js';
|
|
4
|
+
import { TerminalPrompter } from './prompter.js';
|
|
5
|
+
const code = await runCli(process.argv.slice(2), new TerminalPrompter(), new ConsoleOutput());
|
|
6
|
+
process.exitCode = code;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
export class TerminalPrompter {
|
|
4
|
+
async ask(question) {
|
|
5
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
6
|
+
try {
|
|
7
|
+
return (await rl.question(`${question} `)).trim();
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
rl.close();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OutcomeKind } from '../install/applier.js';
|
|
2
|
+
export function printApplyReport(report, output) {
|
|
3
|
+
output.info(`Files: ${report.count(OutcomeKind.Created)} created, ${report.count(OutcomeKind.Updated)} updated, ` +
|
|
4
|
+
`${report.count(OutcomeKind.Unchanged)} unchanged, ${report.count(OutcomeKind.SeedKept)} kept, ` +
|
|
5
|
+
`${report.count(OutcomeKind.Conflict)} conflicts` +
|
|
6
|
+
(report.count(OutcomeKind.Planned) > 0 ? `, ${report.count(OutcomeKind.Planned)} planned (dry run — nothing written).` : '.'));
|
|
7
|
+
for (const conflict of report.ofKind(OutcomeKind.Conflict)) {
|
|
8
|
+
output.warn(`${conflict.relativePath} — ${conflict.note}; new version written to ${conflict.relativePath}.incoming. Merge it, or re-run with --force.`);
|
|
9
|
+
}
|
|
10
|
+
for (const path of report.obsoletePresent) {
|
|
11
|
+
output.warn(`${path} is from pipeline v1 and was renamed or removed in v2 — delete it once you have merged anything you need.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InvalidOptionError } from '../domain/errors.js';
|
|
2
|
+
import { CliArgs, USAGE } from './args.js';
|
|
3
|
+
import { runDoctor } from './commands/doctor.js';
|
|
4
|
+
import { runInit } from './commands/init.js';
|
|
5
|
+
import { packageVersion } from './commands/shared.js';
|
|
6
|
+
import { runTools } from './commands/tools.js';
|
|
7
|
+
import { runUpdate } from './commands/update.js';
|
|
8
|
+
/** Exit codes: 0 ok, 1 check or operation failed, 2 usage error. */
|
|
9
|
+
export async function runCli(argv, prompter, output) {
|
|
10
|
+
let args;
|
|
11
|
+
try {
|
|
12
|
+
args = CliArgs.parse(argv);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
output.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
output.info(USAGE);
|
|
17
|
+
return 2;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
switch (args.command) {
|
|
21
|
+
case 'init':
|
|
22
|
+
return await runInit(args, prompter, output);
|
|
23
|
+
case 'update':
|
|
24
|
+
return await runUpdate(args, output);
|
|
25
|
+
case 'doctor':
|
|
26
|
+
return await runDoctor(args, output);
|
|
27
|
+
case 'tools':
|
|
28
|
+
return runTools(args, output);
|
|
29
|
+
case 'version':
|
|
30
|
+
output.info(packageVersion());
|
|
31
|
+
return 0;
|
|
32
|
+
case 'help':
|
|
33
|
+
output.info(USAGE);
|
|
34
|
+
return 0;
|
|
35
|
+
default:
|
|
36
|
+
output.error(`Unknown command: ${args.command}`);
|
|
37
|
+
output.info(USAGE);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof InvalidOptionError) {
|
|
43
|
+
output.error(error.message);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parseEnum } from './enum-parse.js';
|
|
2
|
+
/** Which framework mapping of canon/backend-canon.md §7 a repository uses. */
|
|
3
|
+
export var CanonMapping;
|
|
4
|
+
(function (CanonMapping) {
|
|
5
|
+
CanonMapping["Laravel"] = "laravel";
|
|
6
|
+
CanonMapping["NestJs"] = "nestjs";
|
|
7
|
+
CanonMapping["Express"] = "express";
|
|
8
|
+
CanonMapping["Django"] = "django";
|
|
9
|
+
CanonMapping["FastApi"] = "fastapi";
|
|
10
|
+
CanonMapping["Spring"] = "spring";
|
|
11
|
+
CanonMapping["Go"] = "go";
|
|
12
|
+
CanonMapping["Custom"] = "custom";
|
|
13
|
+
})(CanonMapping || (CanonMapping = {}));
|
|
14
|
+
export const ALL_CANON_MAPPINGS = [
|
|
15
|
+
CanonMapping.Laravel,
|
|
16
|
+
CanonMapping.NestJs,
|
|
17
|
+
CanonMapping.Express,
|
|
18
|
+
CanonMapping.Django,
|
|
19
|
+
CanonMapping.FastApi,
|
|
20
|
+
CanonMapping.Spring,
|
|
21
|
+
CanonMapping.Go,
|
|
22
|
+
CanonMapping.Custom,
|
|
23
|
+
];
|
|
24
|
+
export function parseCanonMapping(value) {
|
|
25
|
+
return parseEnum('canon mapping', value, ALL_CANON_MAPPINGS);
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseEnum } from './enum-parse.js';
|
|
2
|
+
/**
|
|
3
|
+
* The pipeline section lives in AGENTS.md, which Codex reads directly and
|
|
4
|
+
* Claude Code reads only when the repository has no CLAUDE.md. This decides
|
|
5
|
+
* how Claude Code reaches it.
|
|
6
|
+
*/
|
|
7
|
+
export var ClaudeMdMode;
|
|
8
|
+
(function (ClaudeMdMode) {
|
|
9
|
+
/** Keep a CLAUDE.md whose managed section is a single `@AGENTS.md` import. */
|
|
10
|
+
ClaudeMdMode["Import"] = "import";
|
|
11
|
+
/** Write no CLAUDE.md. Claude Code reads AGENTS.md directly (v2.1.277+). */
|
|
12
|
+
ClaudeMdMode["None"] = "none";
|
|
13
|
+
})(ClaudeMdMode || (ClaudeMdMode = {}));
|
|
14
|
+
export const ALL_CLAUDE_MD_MODES = [ClaudeMdMode.Import, ClaudeMdMode.None];
|
|
15
|
+
export function parseClaudeMdMode(value) {
|
|
16
|
+
return parseEnum('claude-md', value, ALL_CLAUDE_MD_MODES);
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InvalidOptionError } from './errors.js';
|
|
2
|
+
/** Parse edge for string enums (typed-data T5): the only place a raw string becomes an enum. */
|
|
3
|
+
export function parseEnum(option, value, allowed) {
|
|
4
|
+
const match = allowed.find((candidate) => candidate === value.trim());
|
|
5
|
+
if (match === undefined) {
|
|
6
|
+
throw new InvalidOptionError(option, value, allowed);
|
|
7
|
+
}
|
|
8
|
+
return match;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A user-supplied option (flag, prompt answer, config value) was not valid. */
|
|
2
|
+
export class InvalidOptionError extends Error {
|
|
3
|
+
option;
|
|
4
|
+
value;
|
|
5
|
+
allowed;
|
|
6
|
+
constructor(option, value, allowed) {
|
|
7
|
+
super(`Invalid ${option}: "${value}". Expected one of: ${allowed.join(', ')}.`);
|
|
8
|
+
this.option = option;
|
|
9
|
+
this.value = value;
|
|
10
|
+
this.allowed = allowed;
|
|
11
|
+
this.name = 'InvalidOptionError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { InvalidOptionError } from './errors.js';
|
|
2
|
+
import { parseTopology } from './topology.js';
|
|
3
|
+
import { parseRuntimes } from './runtime.js';
|
|
4
|
+
import { parseToolIds } from './tool.js';
|
|
5
|
+
import { parseCanonMapping } from './canon-mapping.js';
|
|
6
|
+
/** `.ai-agents/pipeline.config.json` — what was installed, read by every agent. */
|
|
7
|
+
export class PipelineConfig {
|
|
8
|
+
pipelineVersion;
|
|
9
|
+
topology;
|
|
10
|
+
runtimes;
|
|
11
|
+
tools;
|
|
12
|
+
mapping;
|
|
13
|
+
installedAt;
|
|
14
|
+
constructor(pipelineVersion, topology, runtimes, tools,
|
|
15
|
+
/** Null until detected or chosen; /scaffold or /bootstrap records it in §5.0. */
|
|
16
|
+
mapping, installedAt) {
|
|
17
|
+
this.pipelineVersion = pipelineVersion;
|
|
18
|
+
this.topology = topology;
|
|
19
|
+
this.runtimes = runtimes;
|
|
20
|
+
this.tools = tools;
|
|
21
|
+
this.mapping = mapping;
|
|
22
|
+
this.installedAt = installedAt;
|
|
23
|
+
}
|
|
24
|
+
toJson() {
|
|
25
|
+
return `${JSON.stringify({
|
|
26
|
+
pipelineVersion: this.pipelineVersion,
|
|
27
|
+
topology: this.topology,
|
|
28
|
+
runtimes: this.runtimes,
|
|
29
|
+
tools: this.tools,
|
|
30
|
+
canonMapping: this.mapping,
|
|
31
|
+
installedAt: this.installedAt,
|
|
32
|
+
}, null, 2)}\n`;
|
|
33
|
+
}
|
|
34
|
+
/** Parse edge (typed-data T5): JSON text in, named type out, or a precise error. */
|
|
35
|
+
static parse(json) {
|
|
36
|
+
const raw = JSON.parse(json);
|
|
37
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
38
|
+
throw new InvalidOptionError('pipeline.config.json', json.slice(0, 40), ['a JSON object']);
|
|
39
|
+
}
|
|
40
|
+
const field = (name) => raw[name];
|
|
41
|
+
const text = (name) => {
|
|
42
|
+
const value = field(name);
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
throw new InvalidOptionError(`pipeline.config.json ${name}`, String(value), ['a string']);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
const list = (name) => {
|
|
49
|
+
const value = field(name);
|
|
50
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
51
|
+
throw new InvalidOptionError(`pipeline.config.json ${name}`, String(value), ['a list of strings']);
|
|
52
|
+
}
|
|
53
|
+
return value.join(',');
|
|
54
|
+
};
|
|
55
|
+
const mapping = field('canonMapping');
|
|
56
|
+
return new PipelineConfig(text('pipelineVersion'), parseTopology(text('topology')), parseRuntimes(list('runtimes')), parseToolIds(list('tools')), typeof mapping === 'string' ? parseCanonMapping(mapping) : null, text('installedAt'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { InvalidOptionError } from './errors.js';
|
|
2
|
+
import { parseEnum } from './enum-parse.js';
|
|
3
|
+
export var Runtime;
|
|
4
|
+
(function (Runtime) {
|
|
5
|
+
Runtime["ClaudeCode"] = "claude-code";
|
|
6
|
+
Runtime["Codex"] = "codex";
|
|
7
|
+
})(Runtime || (Runtime = {}));
|
|
8
|
+
export const ALL_RUNTIMES = [Runtime.ClaudeCode, Runtime.Codex];
|
|
9
|
+
export function parseRuntimes(csv) {
|
|
10
|
+
const parts = csv
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((part) => part.trim())
|
|
13
|
+
.filter((part) => part.length > 0);
|
|
14
|
+
if (parts.length === 0) {
|
|
15
|
+
throw new InvalidOptionError('runtime', csv, ALL_RUNTIMES);
|
|
16
|
+
}
|
|
17
|
+
const parsed = parts.map((part) => parseEnum('runtime', part, ALL_RUNTIMES));
|
|
18
|
+
return ALL_RUNTIMES.filter((runtime) => parsed.includes(runtime));
|
|
19
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parseEnum } from './enum-parse.js';
|
|
2
|
+
export var ToolId;
|
|
3
|
+
(function (ToolId) {
|
|
4
|
+
ToolId["FindSkills"] = "find-skills";
|
|
5
|
+
ToolId["Superpowers"] = "superpowers";
|
|
6
|
+
ToolId["ClaudeMem"] = "claude-mem";
|
|
7
|
+
ToolId["Impeccable"] = "impeccable";
|
|
8
|
+
ToolId["TaskObserver"] = "task-observer";
|
|
9
|
+
ToolId["Ponytail"] = "ponytail";
|
|
10
|
+
ToolId["Headroom"] = "headroom";
|
|
11
|
+
ToolId["CodeReviewGraph"] = "code-review-graph";
|
|
12
|
+
ToolId["Graphify"] = "graphify";
|
|
13
|
+
})(ToolId || (ToolId = {}));
|
|
14
|
+
export const ALL_TOOL_IDS = [
|
|
15
|
+
ToolId.FindSkills,
|
|
16
|
+
ToolId.Superpowers,
|
|
17
|
+
ToolId.ClaudeMem,
|
|
18
|
+
ToolId.Impeccable,
|
|
19
|
+
ToolId.TaskObserver,
|
|
20
|
+
ToolId.Ponytail,
|
|
21
|
+
ToolId.Headroom,
|
|
22
|
+
ToolId.CodeReviewGraph,
|
|
23
|
+
ToolId.Graphify,
|
|
24
|
+
];
|
|
25
|
+
export function parseToolIds(csv) {
|
|
26
|
+
const value = csv.trim();
|
|
27
|
+
if (value === 'all')
|
|
28
|
+
return ALL_TOOL_IDS;
|
|
29
|
+
if (value === 'none' || value === '')
|
|
30
|
+
return [];
|
|
31
|
+
const parsed = value.split(',').map((part) => parseEnum('tool', part, ALL_TOOL_IDS));
|
|
32
|
+
return ALL_TOOL_IDS.filter((id) => parsed.includes(id));
|
|
33
|
+
}
|
|
34
|
+
/** Where an install step has to be run. */
|
|
35
|
+
export var StepLocation;
|
|
36
|
+
(function (StepLocation) {
|
|
37
|
+
/** A shell command the installer can run (with consent). */
|
|
38
|
+
StepLocation["Shell"] = "shell";
|
|
39
|
+
/** Must be done inside the agent's own UI (slash command, plugin menu). */
|
|
40
|
+
StepLocation["AgentUi"] = "agent-ui";
|
|
41
|
+
})(StepLocation || (StepLocation = {}));
|
|
42
|
+
export class InstallStep {
|
|
43
|
+
description;
|
|
44
|
+
command;
|
|
45
|
+
location;
|
|
46
|
+
requires;
|
|
47
|
+
verified;
|
|
48
|
+
constructor(description, command, location,
|
|
49
|
+
/** Binaries that must exist before the command can run, e.g. `uv`, `npx`. */
|
|
50
|
+
requires,
|
|
51
|
+
/** False when the command is our best reading of the tool's docs rather than copied from them. */
|
|
52
|
+
verified) {
|
|
53
|
+
this.description = description;
|
|
54
|
+
this.command = command;
|
|
55
|
+
this.location = location;
|
|
56
|
+
this.requires = requires;
|
|
57
|
+
this.verified = verified;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export var ProbeKind;
|
|
61
|
+
(function (ProbeKind) {
|
|
62
|
+
ProbeKind["Binary"] = "binary";
|
|
63
|
+
/** Any of the paths exists. `~` expands to the home directory; relative paths resolve against the repo. */
|
|
64
|
+
ProbeKind["AnyPath"] = "any-path";
|
|
65
|
+
})(ProbeKind || (ProbeKind = {}));
|
|
66
|
+
export class ToolProbe {
|
|
67
|
+
kind;
|
|
68
|
+
targets;
|
|
69
|
+
constructor(kind, targets) {
|
|
70
|
+
this.kind = kind;
|
|
71
|
+
this.targets = targets;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export var ToolRole;
|
|
75
|
+
(function (ToolRole) {
|
|
76
|
+
ToolRole["Skill"] = "skill";
|
|
77
|
+
ToolRole["Plugin"] = "plugin";
|
|
78
|
+
ToolRole["Proxy"] = "proxy";
|
|
79
|
+
ToolRole["CodeGraph"] = "code-graph";
|
|
80
|
+
})(ToolRole || (ToolRole = {}));
|
|
81
|
+
export class RuntimeSupport {
|
|
82
|
+
runtime;
|
|
83
|
+
steps;
|
|
84
|
+
probe;
|
|
85
|
+
caveat;
|
|
86
|
+
constructor(runtime, steps, probe,
|
|
87
|
+
/** Non-empty when support on this runtime is partial or community-maintained. */
|
|
88
|
+
caveat) {
|
|
89
|
+
this.runtime = runtime;
|
|
90
|
+
this.steps = steps;
|
|
91
|
+
this.probe = probe;
|
|
92
|
+
this.caveat = caveat;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export class ExternalTool {
|
|
96
|
+
id;
|
|
97
|
+
name;
|
|
98
|
+
source;
|
|
99
|
+
role;
|
|
100
|
+
purpose;
|
|
101
|
+
usedBy;
|
|
102
|
+
support;
|
|
103
|
+
ignorePaths;
|
|
104
|
+
usage;
|
|
105
|
+
constructor(id, name, source, role, purpose, usedBy, support,
|
|
106
|
+
/** Lines added to .gitignore when this tool is installed. */
|
|
107
|
+
ignorePaths,
|
|
108
|
+
/** How to use it day to day, for SETUP.md. */
|
|
109
|
+
usage) {
|
|
110
|
+
this.id = id;
|
|
111
|
+
this.name = name;
|
|
112
|
+
this.source = source;
|
|
113
|
+
this.role = role;
|
|
114
|
+
this.purpose = purpose;
|
|
115
|
+
this.usedBy = usedBy;
|
|
116
|
+
this.support = support;
|
|
117
|
+
this.ignorePaths = ignorePaths;
|
|
118
|
+
this.usage = usage;
|
|
119
|
+
}
|
|
120
|
+
supports(runtime) {
|
|
121
|
+
return this.support.some((entry) => entry.runtime === runtime);
|
|
122
|
+
}
|
|
123
|
+
supportFor(runtime) {
|
|
124
|
+
return this.support.find((entry) => entry.runtime === runtime) ?? null;
|
|
125
|
+
}
|
|
126
|
+
stepsFor(runtime) {
|
|
127
|
+
return this.supportFor(runtime)?.steps ?? [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseEnum } from './enum-parse.js';
|
|
2
|
+
export var Topology;
|
|
3
|
+
(function (Topology) {
|
|
4
|
+
/** Server-rendered: controllers hand typed view models to views/pages. */
|
|
5
|
+
Topology["Mvc"] = "mvc";
|
|
6
|
+
/** Backend API and frontend in one repository. */
|
|
7
|
+
Topology["UnifiedApi"] = "unified-api";
|
|
8
|
+
/** Backend only; consumers live in other repositories. */
|
|
9
|
+
Topology["SplitBackend"] = "split-backend";
|
|
10
|
+
/** Frontend only (any framework); the API lives in another repository. */
|
|
11
|
+
Topology["SplitFrontend"] = "split-frontend";
|
|
12
|
+
})(Topology || (Topology = {}));
|
|
13
|
+
export const ALL_TOPOLOGIES = [
|
|
14
|
+
Topology.Mvc,
|
|
15
|
+
Topology.UnifiedApi,
|
|
16
|
+
Topology.SplitBackend,
|
|
17
|
+
Topology.SplitFrontend,
|
|
18
|
+
];
|
|
19
|
+
export function parseTopology(value) {
|
|
20
|
+
return parseEnum('topology', value, ALL_TOPOLOGIES);
|
|
21
|
+
}
|
|
22
|
+
export function describeTopology(topology) {
|
|
23
|
+
switch (topology) {
|
|
24
|
+
case Topology.Mvc:
|
|
25
|
+
return 'MVC — server-rendered pages; the seam is a typed view model per screen';
|
|
26
|
+
case Topology.UnifiedApi:
|
|
27
|
+
return 'Unified API — backend + frontend in one repo over REST/GraphQL/RPC';
|
|
28
|
+
case Topology.SplitBackend:
|
|
29
|
+
return 'Split backend — API only; contracts exported to consumer repos';
|
|
30
|
+
case Topology.SplitFrontend:
|
|
31
|
+
return 'Split frontend — UI only (any framework); contracts imported from the API repo';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function isSplit(topology) {
|
|
35
|
+
return topology === Topology.SplitBackend || topology === Topology.SplitFrontend;
|
|
36
|
+
}
|
|
37
|
+
export function hasUi(topology) {
|
|
38
|
+
return topology !== Topology.SplitBackend;
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const yamlSafe = (text) => text.replace(/\n/g, ' ').replace(/:\s/g, ' — ');
|
|
2
|
+
/** `.claude/commands/{slug}.md` */
|
|
3
|
+
export function renderClaudeCommand(command, body) {
|
|
4
|
+
return `---\ndescription: ${yamlSafe(command.description)}\n---\n\n${body.replace(/\n+$/, '')}\n`;
|
|
5
|
+
}
|
|
6
|
+
/** `.agents/skills/flow-{slug}/SKILL.md` — Codex has no slash-command files; skills trigger on description. */
|
|
7
|
+
export function renderCodexSkill(command, body) {
|
|
8
|
+
const description = yamlSafe(`${command.description}. Use when the user types /${command.slug} or $flow-${command.slug}, or is ${command.triggers}.`);
|
|
9
|
+
return `---\nname: flow-${command.slug}\ndescription: ${description}\n---\n\n# /${command.slug}\n\n${body.replace(/\n+$/, '')}\n`;
|
|
10
|
+
}
|
|
11
|
+
/** `.claude/agents/{name}.md` — a pointer; the instructions live in .ai-agents/agents/. */
|
|
12
|
+
export function renderClaudeSubagent(agent) {
|
|
13
|
+
const prerequisite = agent.requiresContext
|
|
14
|
+
? [
|
|
15
|
+
'Read `.ai-agents/architecture-context.md` first so you understand this',
|
|
16
|
+
"repository's topology, canon mapping, conventions, and rules.",
|
|
17
|
+
'',
|
|
18
|
+
'If `architecture-context.md` is missing or still contains `{PLACEHOLDER}`',
|
|
19
|
+
'text, stop and tell the user to run `/bootstrap` first.',
|
|
20
|
+
].join('\n')
|
|
21
|
+
: [
|
|
22
|
+
'You do NOT require `.ai-agents/architecture-context.md` to exist. You run',
|
|
23
|
+
'before it can be written, or you are the agent that writes it.',
|
|
24
|
+
].join('\n');
|
|
25
|
+
return [
|
|
26
|
+
'---',
|
|
27
|
+
`name: ${agent.name}`,
|
|
28
|
+
`description: ${yamlSafe(agent.description)}`,
|
|
29
|
+
'---',
|
|
30
|
+
'',
|
|
31
|
+
prerequisite,
|
|
32
|
+
'',
|
|
33
|
+
`Then read \`.ai-agents/agents/${agent.agentFile}\` and follow it exactly. That file`,
|
|
34
|
+
'is the source of truth for this agent — do not improvise beyond it.',
|
|
35
|
+
'',
|
|
36
|
+
'Precedence, skills, and fallbacks: `.ai-agents/toolchain.md`.',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|