special-agents 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/README.md +69 -0
- package/content/agents/builder.yaml +25 -0
- package/content/agents/planner.yaml +13 -0
- package/content/agents/qa.yaml +16 -0
- package/content/agents/ticket-maker.yaml +11 -0
- package/content/defaults.yaml +13 -0
- package/content/docs/README.md +42 -0
- package/content/docs/admins.md +46 -0
- package/content/docs/ai-costs.md +38 -0
- package/content/docs/ai-evals.md +55 -0
- package/content/docs/ai.md +141 -0
- package/content/docs/api.md +51 -0
- package/content/docs/architecture.md +61 -0
- package/content/docs/business.md +49 -0
- package/content/docs/data-governance.md +67 -0
- package/content/docs/decisions/0000-template.md +29 -0
- package/content/docs/decisions/README.md +30 -0
- package/content/docs/docs.index.yaml +25 -0
- package/content/docs/features.md +41 -0
- package/content/docs/local-cloud.md +58 -0
- package/content/docs/operations.md +69 -0
- package/content/docs/release-checklist.md +56 -0
- package/content/docs/scalability.md +81 -0
- package/content/docs/security.md +82 -0
- package/content/docs/tickets.md +45 -0
- package/content/docs/users.md +43 -0
- package/content/preamble.md +13 -0
- package/content/rules/base/code-quality.md +20 -0
- package/content/rules/base/core.md +17 -0
- package/content/rules/base/definition-of-done.md +21 -0
- package/content/rules/base/git-safety.md +16 -0
- package/content/rules/base/response-expectations.md +18 -0
- package/content/rules/domain/accessibility.md +14 -0
- package/content/rules/domain/ai-cost.md +21 -0
- package/content/rules/domain/ai-evals.md +25 -0
- package/content/rules/domain/ai-governance.md +16 -0
- package/content/rules/domain/ai-reproducibility.md +19 -0
- package/content/rules/domain/ai-safety.md +19 -0
- package/content/rules/domain/data-governance.md +17 -0
- package/content/rules/domain/observability.md +18 -0
- package/content/rules/domain/robustness.md +21 -0
- package/content/rules/domain/scalability.md +18 -0
- package/content/rules/domain/security.md +28 -0
- package/content/rules/packs.index.yaml +177 -0
- package/content/rules/process/api-docs.md +16 -0
- package/content/rules/process/architecture.md +14 -0
- package/content/rules/process/business-docs.md +13 -0
- package/content/rules/process/ci.md +18 -0
- package/content/rules/process/dependencies.md +17 -0
- package/content/rules/process/project-docs.md +35 -0
- package/content/rules/process/release.md +16 -0
- package/content/rules/process/tdd.md +16 -0
- package/content/rules/process/testing.md +28 -0
- package/content/rules/process/tickets.md +17 -0
- package/content/rules/templated/database.md +16 -0
- package/content/rules/templated/infra.md +18 -0
- package/content/rules/templated/stack.md +19 -0
- package/content/skills/better-sqlite3-rebuild/SKILL.md +14 -0
- package/content/skills/grill-me/SKILL.md +10 -0
- package/content/skills/improve-codebase-architecture/REFERENCE.md +78 -0
- package/content/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/content/skills/prd-to-issues/SKILL.md +92 -0
- package/content/skills/tdd/SKILL.md +107 -0
- package/content/skills/tdd/deep-modules.md +33 -0
- package/content/skills/tdd/interface-design.md +31 -0
- package/content/skills/tdd/mocking.md +59 -0
- package/content/skills/tdd/refactoring.md +10 -0
- package/content/skills/tdd/tests.md +61 -0
- package/content/skills/write-a-prd/SKILL.md +74 -0
- package/dist/agents.d.ts +11 -0
- package/dist/agents.js +31 -0
- package/dist/compile.d.ts +79 -0
- package/dist/compile.js +113 -0
- package/dist/content.d.ts +49 -0
- package/dist/content.js +73 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/resolve.d.ts +46 -0
- package/dist/resolve.js +54 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +45 -0
- package/dist/template.d.ts +22 -0
- package/dist/template.js +34 -0
- package/node_modules/rafi-spec/dist/index.d.ts +4 -0
- package/node_modules/rafi-spec/dist/index.js +4 -0
- package/node_modules/rafi-spec/dist/schemas.d.ts +185 -0
- package/node_modules/rafi-spec/dist/schemas.js +95 -0
- package/node_modules/rafi-spec/dist/types.d.ts +111 -0
- package/node_modules/rafi-spec/dist/types.js +6 -0
- package/node_modules/rafi-spec/dist/validate.d.ts +16 -0
- package/node_modules/rafi-spec/dist/validate.js +40 -0
- package/node_modules/rafi-spec/package.json +35 -0
- package/node_modules/rafi-spec/src/index.ts +19 -0
- package/node_modules/rafi-spec/src/schemas.ts +102 -0
- package/node_modules/rafi-spec/src/types.ts +134 -0
- package/node_modules/rafi-spec/src/validate.ts +60 -0
- package/package.json +39 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-a-prd
|
|
3
|
+
description: Generate a PRD from the client brief and write it as a local markdown file in issues/. Use when the user wants to turn a client request into a structured PRD.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This skill will be invoked when the user wants to create a PRD. You may skip steps if you don't consider them necessary.
|
|
7
|
+
|
|
8
|
+
1. Ask the user for a long, detailed description of the problem they want to solve and any potential ideas for solutions.
|
|
9
|
+
|
|
10
|
+
2. Explore the repo to verify their assertions and understand the current state of the codebase.
|
|
11
|
+
|
|
12
|
+
3. Interview the user relentlessly about every aspect of this plan until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.
|
|
13
|
+
|
|
14
|
+
4. Sketch out the major modules you will need to build or modify to complete the implementation. Actively look for opportunities to extract deep modules that can be tested in isolation.
|
|
15
|
+
|
|
16
|
+
A deep module (as opposed to a shallow module) is one which encapsulates a lot of functionality in a simple, testable interface which rarely changes.
|
|
17
|
+
|
|
18
|
+
Check with the user that these modules match their expectations. Check with the user which modules they want tests written for.
|
|
19
|
+
|
|
20
|
+
5. Once you have a complete understanding of the problem and solution, use the template below to write the PRD. The PRD should be written as a local markdown file at `issues/prd.md`. Create the `issues/` directory if it doesn't exist. Do NOT submit a GitHub issue or call any external service.
|
|
21
|
+
|
|
22
|
+
<prd-template>
|
|
23
|
+
|
|
24
|
+
## Problem Statement
|
|
25
|
+
|
|
26
|
+
The problem that the user is facing, from the user's perspective.
|
|
27
|
+
|
|
28
|
+
## Solution
|
|
29
|
+
|
|
30
|
+
The solution to the problem, from the user's perspective.
|
|
31
|
+
|
|
32
|
+
## User Stories
|
|
33
|
+
|
|
34
|
+
A LONG, numbered list of user stories. Each user story should be in the format of:
|
|
35
|
+
|
|
36
|
+
1. As an <actor>, I want a <feature>, so that <benefit>
|
|
37
|
+
|
|
38
|
+
<user-story-example>
|
|
39
|
+
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
|
|
40
|
+
</user-story-example>
|
|
41
|
+
|
|
42
|
+
This list of user stories should be extremely extensive and cover all aspects of the feature.
|
|
43
|
+
|
|
44
|
+
## Implementation Decisions
|
|
45
|
+
|
|
46
|
+
A list of implementation decisions that were made. This can include:
|
|
47
|
+
|
|
48
|
+
- The modules that will be built/modified
|
|
49
|
+
- The interfaces of those modules that will be modified
|
|
50
|
+
- Technical clarifications from the developer
|
|
51
|
+
- Architectural decisions
|
|
52
|
+
- Schema changes
|
|
53
|
+
- API contracts
|
|
54
|
+
- Specific interactions
|
|
55
|
+
|
|
56
|
+
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
|
|
57
|
+
|
|
58
|
+
## Testing Decisions
|
|
59
|
+
|
|
60
|
+
A list of testing decisions that were made. Include:
|
|
61
|
+
|
|
62
|
+
- A description of what makes a good test (only test external behavior, not implementation details)
|
|
63
|
+
- Which modules will be tested
|
|
64
|
+
- Prior art for the tests (i.e. similar types of tests in the codebase)
|
|
65
|
+
|
|
66
|
+
## Out of Scope
|
|
67
|
+
|
|
68
|
+
A description of the things that are out of scope for this PRD.
|
|
69
|
+
|
|
70
|
+
## Further Notes
|
|
71
|
+
|
|
72
|
+
Any further notes about the feature.
|
|
73
|
+
|
|
74
|
+
</prd-template>
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentManifest, type AgentRole } from "rafi-spec";
|
|
2
|
+
/** Absolute path to the bundled `content/agents/` directory. */
|
|
3
|
+
export declare const AGENTS_DIR: string;
|
|
4
|
+
/** The roles Rafi ships, each mapping to an ai-foreman turn-type/command. */
|
|
5
|
+
export declare const AGENT_ROLES: AgentRole[];
|
|
6
|
+
/** Parse an agent-manifest YAML string into a validated AgentManifest. */
|
|
7
|
+
export declare function parseAgentManifest(raw: string): AgentManifest;
|
|
8
|
+
/** Load a single role manifest by name (the file stem under content/agents). */
|
|
9
|
+
export declare function loadAgent(name: string): AgentManifest;
|
|
10
|
+
/** Load every shipped role manifest (in {@link AGENT_ROLES} order). */
|
|
11
|
+
export declare function loadAllAgents(): AgentManifest[];
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent (role) manifest loader. Reads `content/agents/<role>.yaml` — the named
|
|
3
|
+
* compositions of packs + skills that the runtime loads per turn-type. A pure
|
|
4
|
+
* `parseAgentManifest` is split from the filesystem helpers.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { assertAgentManifest } from "rafi-spec";
|
|
10
|
+
import { CONTENT_DIR } from "./content.js";
|
|
11
|
+
/** Absolute path to the bundled `content/agents/` directory. */
|
|
12
|
+
export const AGENTS_DIR = join(CONTENT_DIR, "agents");
|
|
13
|
+
/** The roles Rafi ships, each mapping to an ai-foreman turn-type/command. */
|
|
14
|
+
export const AGENT_ROLES = ["builder", "qa", "planner", "ticket-maker"];
|
|
15
|
+
/** Parse an agent-manifest YAML string into a validated AgentManifest. */
|
|
16
|
+
export function parseAgentManifest(raw) {
|
|
17
|
+
const data = parseYaml(raw);
|
|
18
|
+
assertAgentManifest(data); // throws "Invalid agent manifest: …"
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
/** Load a single role manifest by name (the file stem under content/agents). */
|
|
22
|
+
export function loadAgent(name) {
|
|
23
|
+
const path = join(AGENTS_DIR, `${name}.yaml`);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
throw new Error(`unknown agent: ${name}`);
|
|
26
|
+
return parseAgentManifest(readFileSync(path, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
/** Load every shipped role manifest (in {@link AGENT_ROLES} order). */
|
|
29
|
+
export function loadAllAgents() {
|
|
30
|
+
return AGENT_ROLES.map((role) => loadAgent(role));
|
|
31
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type Defaults, type LoadedPack } from "./content.js";
|
|
2
|
+
import { type ConditionFlags, type ResolvableManifest } from "./resolve.js";
|
|
3
|
+
import type { AgentManifest, AgentRole, EffortLevel } from "rafi-spec";
|
|
4
|
+
export interface CompileOptions {
|
|
5
|
+
/** Stack/flags to render with. Defaults to the bundled `defaults.yaml`. */
|
|
6
|
+
defaults?: Defaults;
|
|
7
|
+
}
|
|
8
|
+
/** Render one pack body, substituting `{{vars}}` and resolving `{{#if flag}}` blocks. */
|
|
9
|
+
export declare function renderPackBody(pack: Pick<LoadedPack, "body">, defaults: Defaults): string;
|
|
10
|
+
/**
|
|
11
|
+
* The full flattened rules doc: preamble + all packs (index order), rendered.
|
|
12
|
+
* Includes every pack regardless of condition — this is the canonical "everything"
|
|
13
|
+
* document; role/flag filtering is the resolver's job.
|
|
14
|
+
*/
|
|
15
|
+
export declare function composeRulesMarkdown(opts?: CompileOptions): string;
|
|
16
|
+
/** Options controlling how a role's packs are resolved and rendered. */
|
|
17
|
+
export interface AgentComposeOptions {
|
|
18
|
+
/** Stack/flags to render with. Defaults to the bundled `defaults.yaml`. */
|
|
19
|
+
defaults?: Defaults;
|
|
20
|
+
/** Which conditional pack groups to include (ai/frontend/cloud/backend). */
|
|
21
|
+
conditions?: ConditionFlags;
|
|
22
|
+
/** Drop `supersededByForeman` packs (the foreman tracker owns them). */
|
|
23
|
+
foremanActive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Render a role's system text: its resolved packs (listed + enabled conditionals,
|
|
27
|
+
* deduped, in manifest order) concatenated and rendered with the defaults. Unlike
|
|
28
|
+
* {@link composeRulesMarkdown} there is no preamble — this is appended to the
|
|
29
|
+
* harness's own system prompt, not used as a standalone rules doc.
|
|
30
|
+
*/
|
|
31
|
+
export declare function composeAgentSystem(manifest: ResolvableManifest, opts?: AgentComposeOptions): string;
|
|
32
|
+
/** A composed role bundle, ready for the runtime to load. */
|
|
33
|
+
export interface ComposedAgent {
|
|
34
|
+
manifest: AgentManifest;
|
|
35
|
+
/** The rendered role system text (pack bodies). */
|
|
36
|
+
system: string;
|
|
37
|
+
/** Skill names the role preloads. */
|
|
38
|
+
skills: string[];
|
|
39
|
+
/** Model override, or null to inherit the runtime's `--model`. */
|
|
40
|
+
model: string | null;
|
|
41
|
+
/** Effort override, or null to inherit the runtime's `--effort`. */
|
|
42
|
+
effort: EffortLevel | null;
|
|
43
|
+
}
|
|
44
|
+
/** Load a role manifest and compose its full bundle (system text + metadata). */
|
|
45
|
+
export declare function getAgent(role: string, opts?: AgentComposeOptions): ComposedAgent;
|
|
46
|
+
/**
|
|
47
|
+
* Build the generated header comment that records which conditional pack groups
|
|
48
|
+
* are active. Written at the top of `AGENTS.md` and `CLAUDE.md` so the choice
|
|
49
|
+
* is never invisible.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildConditionsHeader(flags: {
|
|
52
|
+
usesAI: boolean;
|
|
53
|
+
hasFrontend: boolean;
|
|
54
|
+
runsInCloud: boolean;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Write `<targetDir>/AGENTS.md` — the flattened Codex rules document.
|
|
58
|
+
* Format: one-line conditions header + preamble + all packs rendered with defaults.
|
|
59
|
+
*/
|
|
60
|
+
export declare function emitAgentsMd(targetDir: string, opts?: CompileOptions): void;
|
|
61
|
+
/**
|
|
62
|
+
* Write `<targetDir>/CLAUDE.md` — the lean Claude entrypoint that imports `AGENTS.md`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function emitClaudeMd(targetDir: string, opts?: CompileOptions): void;
|
|
65
|
+
export interface EmitOptions extends AgentComposeOptions {
|
|
66
|
+
/** Roles to emit. Defaults to all four. */
|
|
67
|
+
roles?: AgentRole[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write `.rafi/compiled/<role>/system.md` + `meta.json` for each role.
|
|
71
|
+
* Foreman's `roles.ts` reads these at runtime to load the composed bundle.
|
|
72
|
+
*/
|
|
73
|
+
export declare function emitCompiledBundles(targetDir: string, opts?: EmitOptions): void;
|
|
74
|
+
/**
|
|
75
|
+
* Write lean Claude subagent files to `<targetDir>/.claude/agents/<role>.md`.
|
|
76
|
+
* Each file has YAML front-matter (name, description) followed by the role's
|
|
77
|
+
* composed system text.
|
|
78
|
+
*/
|
|
79
|
+
export declare function emitClaudeAgents(targetDir: string, opts?: EmitOptions): void;
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition — assembles harness artifacts from the bundled packs.
|
|
3
|
+
*
|
|
4
|
+
* `composeRulesMarkdown` produces the flattened rules document (Codex's `AGENTS.md`
|
|
5
|
+
* form): the preamble followed by every pack in index order, with templated packs
|
|
6
|
+
* rendered against the stack/flags. Because each pack body is a contiguous slice of
|
|
7
|
+
* the source between headings, concatenating them after the preamble reproduces the
|
|
8
|
+
* canonical rules doc byte-for-byte — the Phase 3 golden gate.
|
|
9
|
+
*
|
|
10
|
+
* Per-role and lean-Claude emission build on this and the resolver (see resolve.ts).
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { render } from "./template.js";
|
|
15
|
+
import { loadAllPacks, loadDefaults, loadPack, loadPacksIndex, loadPreamble, } from "./content.js";
|
|
16
|
+
import { resolveAgentPacks } from "./resolve.js";
|
|
17
|
+
import { loadAgent, AGENT_ROLES } from "./agents.js";
|
|
18
|
+
/** Render one pack body, substituting `{{vars}}` and resolving `{{#if flag}}` blocks. */
|
|
19
|
+
export function renderPackBody(pack, defaults) {
|
|
20
|
+
return render(pack.body, { vars: defaults.stack, flags: defaults.flags });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The full flattened rules doc: preamble + all packs (index order), rendered.
|
|
24
|
+
* Includes every pack regardless of condition — this is the canonical "everything"
|
|
25
|
+
* document; role/flag filtering is the resolver's job.
|
|
26
|
+
*/
|
|
27
|
+
export function composeRulesMarkdown(opts = {}) {
|
|
28
|
+
const defaults = opts.defaults ?? loadDefaults();
|
|
29
|
+
const body = loadAllPacks()
|
|
30
|
+
.map((pack) => renderPackBody(pack, defaults))
|
|
31
|
+
.join("");
|
|
32
|
+
return loadPreamble() + body;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Render a role's system text: its resolved packs (listed + enabled conditionals,
|
|
36
|
+
* deduped, in manifest order) concatenated and rendered with the defaults. Unlike
|
|
37
|
+
* {@link composeRulesMarkdown} there is no preamble — this is appended to the
|
|
38
|
+
* harness's own system prompt, not used as a standalone rules doc.
|
|
39
|
+
*/
|
|
40
|
+
export function composeAgentSystem(manifest, opts = {}) {
|
|
41
|
+
const defaults = opts.defaults ?? loadDefaults();
|
|
42
|
+
const index = loadPacksIndex();
|
|
43
|
+
const entries = resolveAgentPacks(manifest, { conditions: opts.conditions ?? {}, foremanActive: opts.foremanActive }, index);
|
|
44
|
+
return entries.map((e) => renderPackBody(loadPack(e.path), defaults)).join("");
|
|
45
|
+
}
|
|
46
|
+
/** Load a role manifest and compose its full bundle (system text + metadata). */
|
|
47
|
+
export function getAgent(role, opts = {}) {
|
|
48
|
+
const manifest = loadAgent(role);
|
|
49
|
+
return {
|
|
50
|
+
manifest,
|
|
51
|
+
system: composeAgentSystem(manifest, opts),
|
|
52
|
+
skills: manifest.skills,
|
|
53
|
+
model: manifest.model ?? null,
|
|
54
|
+
effort: manifest.effort ?? null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build the generated header comment that records which conditional pack groups
|
|
59
|
+
* are active. Written at the top of `AGENTS.md` and `CLAUDE.md` so the choice
|
|
60
|
+
* is never invisible.
|
|
61
|
+
*/
|
|
62
|
+
export function buildConditionsHeader(flags) {
|
|
63
|
+
return (`# rafi: ai=${flags.usesAI ? "on" : "off"}` +
|
|
64
|
+
` frontend=${flags.hasFrontend ? "on" : "off"}` +
|
|
65
|
+
` cloud=${flags.runsInCloud ? "on" : "off"}\n`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write `<targetDir>/AGENTS.md` — the flattened Codex rules document.
|
|
69
|
+
* Format: one-line conditions header + preamble + all packs rendered with defaults.
|
|
70
|
+
*/
|
|
71
|
+
export function emitAgentsMd(targetDir, opts = {}) {
|
|
72
|
+
const defaults = opts.defaults ?? loadDefaults();
|
|
73
|
+
const header = buildConditionsHeader(defaults.flags);
|
|
74
|
+
writeFileSync(join(targetDir, "AGENTS.md"), header + composeRulesMarkdown({ defaults }), "utf8");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write `<targetDir>/CLAUDE.md` — the lean Claude entrypoint that imports `AGENTS.md`.
|
|
78
|
+
*/
|
|
79
|
+
export function emitClaudeMd(targetDir, opts = {}) {
|
|
80
|
+
const defaults = opts.defaults ?? loadDefaults();
|
|
81
|
+
const header = buildConditionsHeader(defaults.flags);
|
|
82
|
+
writeFileSync(join(targetDir, "CLAUDE.md"), header + "@AGENTS.md\n", "utf8");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Write `.rafi/compiled/<role>/system.md` + `meta.json` for each role.
|
|
86
|
+
* Foreman's `roles.ts` reads these at runtime to load the composed bundle.
|
|
87
|
+
*/
|
|
88
|
+
export function emitCompiledBundles(targetDir, opts = {}) {
|
|
89
|
+
const roles = opts.roles ?? AGENT_ROLES;
|
|
90
|
+
for (const role of roles) {
|
|
91
|
+
const bundle = getAgent(role, opts);
|
|
92
|
+
const dir = join(targetDir, ".rafi", "compiled", role);
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
writeFileSync(join(dir, "system.md"), bundle.system, "utf8");
|
|
95
|
+
const meta = { skills: bundle.skills, model: bundle.model, effort: bundle.effort };
|
|
96
|
+
writeFileSync(join(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Write lean Claude subagent files to `<targetDir>/.claude/agents/<role>.md`.
|
|
101
|
+
* Each file has YAML front-matter (name, description) followed by the role's
|
|
102
|
+
* composed system text.
|
|
103
|
+
*/
|
|
104
|
+
export function emitClaudeAgents(targetDir, opts = {}) {
|
|
105
|
+
const roles = opts.roles ?? AGENT_ROLES;
|
|
106
|
+
const agentsDir = join(targetDir, ".claude", "agents");
|
|
107
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
108
|
+
for (const role of roles) {
|
|
109
|
+
const bundle = getAgent(role, opts);
|
|
110
|
+
const frontMatter = `---\nname: ${bundle.manifest.name}\ndescription: ${bundle.manifest.description}\n---\n\n`;
|
|
111
|
+
writeFileSync(join(agentsDir, `${role}.md`), frontMatter + bundle.system, "utf8");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type RulePack } from "rafi-spec";
|
|
2
|
+
/** Absolute path to the bundled `content/` directory. */
|
|
3
|
+
export declare const CONTENT_DIR: string;
|
|
4
|
+
/** Parse a rule-pack markdown string into its validated front-matter + body. */
|
|
5
|
+
export declare function parseRulePack(raw: string): RulePack;
|
|
6
|
+
/** Default stack strings + flags (content/defaults.yaml). */
|
|
7
|
+
export interface Defaults {
|
|
8
|
+
stack: Record<string, string>;
|
|
9
|
+
flags: Record<string, boolean>;
|
|
10
|
+
}
|
|
11
|
+
/** Read content/defaults.yaml. */
|
|
12
|
+
export declare function loadDefaults(): Defaults;
|
|
13
|
+
/** The flattened-rules-doc preamble (everything before the first section). */
|
|
14
|
+
export declare function loadPreamble(): string;
|
|
15
|
+
/** One row of content/rules/packs.index.yaml. */
|
|
16
|
+
export interface PackIndexEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
category: string;
|
|
19
|
+
path: string;
|
|
20
|
+
condition: string;
|
|
21
|
+
template: boolean;
|
|
22
|
+
supersededByForeman?: boolean;
|
|
23
|
+
order: number;
|
|
24
|
+
}
|
|
25
|
+
/** Read the pack registry, sorted by its `order` field. */
|
|
26
|
+
export declare function loadPacksIndex(): PackIndexEntry[];
|
|
27
|
+
/** A loaded pack: its parsed content plus where it sits in the registry. */
|
|
28
|
+
export interface LoadedPack extends RulePack {
|
|
29
|
+
/** Path relative to content/rules (e.g. `base/core.md`). */
|
|
30
|
+
path: string;
|
|
31
|
+
order: number;
|
|
32
|
+
}
|
|
33
|
+
/** Load a single pack file by its path relative to content/rules. */
|
|
34
|
+
export declare function loadPack(relPath: string): RulePack;
|
|
35
|
+
/** Load every pack listed in the index, in index order. */
|
|
36
|
+
export declare function loadAllPacks(): LoadedPack[];
|
|
37
|
+
/** Absolute path to the bundled `content/docs/` directory. */
|
|
38
|
+
export declare const DOCS_DIR: string;
|
|
39
|
+
/** One entry from content/docs/docs.index.yaml. */
|
|
40
|
+
export interface DocIndexEntry {
|
|
41
|
+
/** Path relative to content/docs/ (and will be placed under targetDir/docs/). */
|
|
42
|
+
path: string;
|
|
43
|
+
/** Copy gate: always | ai | frontend. */
|
|
44
|
+
gate: "always" | "ai" | "frontend";
|
|
45
|
+
}
|
|
46
|
+
/** Read content/docs/docs.index.yaml. */
|
|
47
|
+
export declare function loadDocsIndex(): DocIndexEntry[];
|
|
48
|
+
/** Names of the pack files physically present under content/rules (for drift checks). */
|
|
49
|
+
export declare function packFilesOnDisk(): string[];
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the bundled authoring content (rule packs, their index, and the default
|
|
3
|
+
* stack/flags) that the composition step consumes.
|
|
4
|
+
*
|
|
5
|
+
* `parseRulePack` is a pure string→RulePack transform (front-matter split + schema
|
|
6
|
+
* validation) kept separate from the filesystem helpers so the parse rules are
|
|
7
|
+
* unit-testable. `CONTENT_DIR` resolves the same whether this module runs from
|
|
8
|
+
* `src/` (tsx/tests) or `dist/` (published) because `content/` ships alongside both.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import { assertRulePack } from "rafi-spec";
|
|
15
|
+
/** Absolute path to the bundled `content/` directory. */
|
|
16
|
+
export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "content");
|
|
17
|
+
const RULES_DIR = join(CONTENT_DIR, "rules");
|
|
18
|
+
const FRONT_MATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
19
|
+
/** Parse a rule-pack markdown string into its validated front-matter + body. */
|
|
20
|
+
export function parseRulePack(raw) {
|
|
21
|
+
const m = raw.match(FRONT_MATTER);
|
|
22
|
+
if (!m)
|
|
23
|
+
throw new Error("rule pack is missing front-matter");
|
|
24
|
+
const fm = parseYaml(m[1]);
|
|
25
|
+
assertRulePack(fm); // throws "Invalid rule pack: …" on schema failure
|
|
26
|
+
return { ...fm, body: m[2] };
|
|
27
|
+
}
|
|
28
|
+
/** Read content/defaults.yaml. */
|
|
29
|
+
export function loadDefaults() {
|
|
30
|
+
return parseYaml(readFileSync(join(CONTENT_DIR, "defaults.yaml"), "utf8"));
|
|
31
|
+
}
|
|
32
|
+
/** The flattened-rules-doc preamble (everything before the first section). */
|
|
33
|
+
export function loadPreamble() {
|
|
34
|
+
return readFileSync(join(CONTENT_DIR, "preamble.md"), "utf8");
|
|
35
|
+
}
|
|
36
|
+
/** Read the pack registry, sorted by its `order` field. */
|
|
37
|
+
export function loadPacksIndex() {
|
|
38
|
+
const parsed = parseYaml(readFileSync(join(RULES_DIR, "packs.index.yaml"), "utf8"));
|
|
39
|
+
return [...parsed.packs].sort((a, b) => a.order - b.order);
|
|
40
|
+
}
|
|
41
|
+
/** Load a single pack file by its path relative to content/rules. */
|
|
42
|
+
export function loadPack(relPath) {
|
|
43
|
+
return parseRulePack(readFileSync(join(RULES_DIR, relPath), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
/** Load every pack listed in the index, in index order. */
|
|
46
|
+
export function loadAllPacks() {
|
|
47
|
+
return loadPacksIndex().map((entry) => ({
|
|
48
|
+
...loadPack(entry.path),
|
|
49
|
+
path: entry.path,
|
|
50
|
+
order: entry.order,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** Absolute path to the bundled `content/docs/` directory. */
|
|
54
|
+
export const DOCS_DIR = join(CONTENT_DIR, "docs");
|
|
55
|
+
/** Read content/docs/docs.index.yaml. */
|
|
56
|
+
export function loadDocsIndex() {
|
|
57
|
+
const parsed = parseYaml(readFileSync(join(DOCS_DIR, "docs.index.yaml"), "utf8"));
|
|
58
|
+
return parsed.docs;
|
|
59
|
+
}
|
|
60
|
+
/** Names of the pack files physically present under content/rules (for drift checks). */
|
|
61
|
+
export function packFilesOnDisk() {
|
|
62
|
+
const out = [];
|
|
63
|
+
const walk = (dir, prefix) => {
|
|
64
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
if (ent.isDirectory())
|
|
66
|
+
walk(join(dir, ent.name), `${prefix}${ent.name}/`);
|
|
67
|
+
else if (ent.name.endsWith(".md"))
|
|
68
|
+
out.push(`${prefix}${ent.name}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walk(RULES_DIR, "");
|
|
72
|
+
return out.sort();
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* special-agents — the Rafi library. Re-exports the neutral schema, the templating
|
|
3
|
+
* engine, and the content loaders. The higher-level composition API (getAgent,
|
|
4
|
+
* getSkill, compile) is layered on these in the rest of Phase 3.
|
|
5
|
+
*/
|
|
6
|
+
export * from "rafi-spec";
|
|
7
|
+
export { render, type TemplateContext } from "./template.js";
|
|
8
|
+
export { CONTENT_DIR, DOCS_DIR, parseRulePack, loadDefaults, loadDocsIndex, loadPreamble, loadPacksIndex, loadPack, loadAllPacks, packFilesOnDisk, type Defaults, type PackIndexEntry, type LoadedPack, type DocIndexEntry, } from "./content.js";
|
|
9
|
+
export { composeRulesMarkdown, composeAgentSystem, getAgent, buildConditionsHeader, emitAgentsMd, emitClaudeMd, emitCompiledBundles, emitClaudeAgents, renderPackBody, type CompileOptions, type AgentComposeOptions, type ComposedAgent, type EmitOptions, } from "./compile.js";
|
|
10
|
+
export { resolvePackRefs, resolveAgentPacks, type ConditionFlags, type ResolveContext, type ResolvableManifest, } from "./resolve.js";
|
|
11
|
+
export { SKILLS_DIR, parseSkillManifest, loadSkill, loadAllSkills, skillNames, } from "./skills.js";
|
|
12
|
+
export { AGENTS_DIR, AGENT_ROLES, parseAgentManifest, loadAgent, loadAllAgents, } from "./agents.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* special-agents — the Rafi library. Re-exports the neutral schema, the templating
|
|
3
|
+
* engine, and the content loaders. The higher-level composition API (getAgent,
|
|
4
|
+
* getSkill, compile) is layered on these in the rest of Phase 3.
|
|
5
|
+
*/
|
|
6
|
+
export * from "rafi-spec";
|
|
7
|
+
export { render } from "./template.js";
|
|
8
|
+
export { CONTENT_DIR, DOCS_DIR, parseRulePack, loadDefaults, loadDocsIndex, loadPreamble, loadPacksIndex, loadPack, loadAllPacks, packFilesOnDisk, } from "./content.js";
|
|
9
|
+
export { composeRulesMarkdown, composeAgentSystem, getAgent, buildConditionsHeader, emitAgentsMd, emitClaudeMd, emitCompiledBundles, emitClaudeAgents, renderPackBody, } from "./compile.js";
|
|
10
|
+
export { resolvePackRefs, resolveAgentPacks, } from "./resolve.js";
|
|
11
|
+
export { SKILLS_DIR, parseSkillManifest, loadSkill, loadAllSkills, skillNames, } from "./skills.js";
|
|
12
|
+
export { AGENTS_DIR, AGENT_ROLES, parseAgentManifest, loadAgent, loadAllAgents, } from "./agents.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack resolution — turns a role manifest's pack references into an ordered, deduped
|
|
3
|
+
* list of concrete index entries.
|
|
4
|
+
*
|
|
5
|
+
* Reference forms (all `<category>/<name>` or `<category>/*`, matching the manifest
|
|
6
|
+
* and index layout):
|
|
7
|
+
* - `base/core` → the single pack with that category + name
|
|
8
|
+
* - `base/*` → every pack in that category, in index order
|
|
9
|
+
*
|
|
10
|
+
* Resolution is decoupled from `ProjectConfig`: the caller maps project flags onto
|
|
11
|
+
* the generic {@link ConditionFlags} (ai/frontend/cloud/backend) that gate the
|
|
12
|
+
* manifest's `conditionalPacks`. Order follows appearance — listed packs first (globs
|
|
13
|
+
* expanded in index order), then conditional groups in a fixed order — deduped by
|
|
14
|
+
* first occurrence so authoring intent is preserved deterministically.
|
|
15
|
+
*/
|
|
16
|
+
import type { ConditionalPacks } from "rafi-spec";
|
|
17
|
+
import type { PackIndexEntry } from "./content.js";
|
|
18
|
+
/** Flags that gate `conditionalPacks`. The caller maps project flags onto these. */
|
|
19
|
+
export interface ConditionFlags {
|
|
20
|
+
ai?: boolean;
|
|
21
|
+
frontend?: boolean;
|
|
22
|
+
cloud?: boolean;
|
|
23
|
+
backend?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolveContext {
|
|
26
|
+
/** Which conditional pack groups to include. */
|
|
27
|
+
conditions: ConditionFlags;
|
|
28
|
+
/** When true, packs marked `supersededByForeman` are dropped (foreman owns them). */
|
|
29
|
+
foremanActive?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Expand and flatten a list of pack refs into ordered, deduped index entries.
|
|
33
|
+
* Globs expand in index order; duplicates keep their first occurrence.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePackRefs(refs: string[], index: PackIndexEntry[]): PackIndexEntry[];
|
|
36
|
+
/** A manifest's pack-bearing fields (the subset the resolver needs). */
|
|
37
|
+
export interface ResolvableManifest {
|
|
38
|
+
packs: string[];
|
|
39
|
+
conditionalPacks?: ConditionalPacks;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a role's full pack set: listed `packs` first, then each enabled conditional
|
|
43
|
+
* group (in {@link CONDITION_ORDER}), deduped by first occurrence. With
|
|
44
|
+
* `foremanActive`, packs marked `supersededByForeman` are removed.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveAgentPacks(manifest: ResolvableManifest, ctx: ResolveContext, index: PackIndexEntry[]): PackIndexEntry[];
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Fixed order in which conditional groups are appended after the listed packs. */
|
|
2
|
+
const CONDITION_ORDER = ["ai", "frontend", "cloud", "backend"];
|
|
3
|
+
/** Resolve one ref (`cat/name` or `cat/*`) to its index entries. Throws if unresolved. */
|
|
4
|
+
function resolveOneRef(ref, index) {
|
|
5
|
+
const slash = ref.indexOf("/");
|
|
6
|
+
if (slash === -1)
|
|
7
|
+
throw new Error(`malformed pack ref (expected category/name): ${ref}`);
|
|
8
|
+
const category = ref.slice(0, slash);
|
|
9
|
+
const rest = ref.slice(slash + 1);
|
|
10
|
+
if (rest === "*") {
|
|
11
|
+
const matches = index.filter((e) => e.category === category);
|
|
12
|
+
if (matches.length === 0)
|
|
13
|
+
throw new Error(`no packs match glob: ${ref}`);
|
|
14
|
+
return matches;
|
|
15
|
+
}
|
|
16
|
+
const match = index.find((e) => e.category === category && e.name === rest);
|
|
17
|
+
if (!match)
|
|
18
|
+
throw new Error(`unknown pack ref: ${ref}`);
|
|
19
|
+
return [match];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Expand and flatten a list of pack refs into ordered, deduped index entries.
|
|
23
|
+
* Globs expand in index order; duplicates keep their first occurrence.
|
|
24
|
+
*/
|
|
25
|
+
export function resolvePackRefs(refs, index) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const ref of refs) {
|
|
29
|
+
for (const entry of resolveOneRef(ref, index)) {
|
|
30
|
+
if (seen.has(entry.name))
|
|
31
|
+
continue;
|
|
32
|
+
seen.add(entry.name);
|
|
33
|
+
out.push(entry);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a role's full pack set: listed `packs` first, then each enabled conditional
|
|
40
|
+
* group (in {@link CONDITION_ORDER}), deduped by first occurrence. With
|
|
41
|
+
* `foremanActive`, packs marked `supersededByForeman` are removed.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveAgentPacks(manifest, ctx, index) {
|
|
44
|
+
const refs = [...manifest.packs];
|
|
45
|
+
const conditional = manifest.conditionalPacks ?? {};
|
|
46
|
+
for (const key of CONDITION_ORDER) {
|
|
47
|
+
if (ctx.conditions[key])
|
|
48
|
+
refs.push(...(conditional[key] ?? []));
|
|
49
|
+
}
|
|
50
|
+
let resolved = resolvePackRefs(refs, index);
|
|
51
|
+
if (ctx.foremanActive)
|
|
52
|
+
resolved = resolved.filter((e) => !e.supersededByForeman);
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SkillManifest } from "rafi-spec";
|
|
2
|
+
/** Absolute path to the bundled `content/skills/` directory. */
|
|
3
|
+
export declare const SKILLS_DIR: string;
|
|
4
|
+
/** Parse a SKILL.md string into its validated front-matter + body. */
|
|
5
|
+
export declare function parseSkillManifest(raw: string): SkillManifest;
|
|
6
|
+
/** Directory names of every bundled skill, sorted. */
|
|
7
|
+
export declare function skillNames(): string[];
|
|
8
|
+
/** Load a single skill by its directory name. */
|
|
9
|
+
export declare function loadSkill(name: string): SkillManifest;
|
|
10
|
+
/** Load every bundled skill, in directory-name order. */
|
|
11
|
+
export declare function loadAllSkills(): SkillManifest[];
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill loader. Reads `content/skills/<name>/SKILL.md` units — the standard
|
|
3
|
+
* Anthropic format plus the optional Rafi `pins` / `codexPriority` fields. A pure
|
|
4
|
+
* `parseSkillManifest` (string → SkillManifest) is split from the directory walk so
|
|
5
|
+
* the parse rules are unit-tested without disk.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { assertSkillManifest } from "rafi-spec";
|
|
11
|
+
import { CONTENT_DIR } from "./content.js";
|
|
12
|
+
/** Absolute path to the bundled `content/skills/` directory. */
|
|
13
|
+
export const SKILLS_DIR = join(CONTENT_DIR, "skills");
|
|
14
|
+
const FRONT_MATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
15
|
+
/** Parse a SKILL.md string into its validated front-matter + body. */
|
|
16
|
+
export function parseSkillManifest(raw) {
|
|
17
|
+
const m = raw.match(FRONT_MATTER);
|
|
18
|
+
if (!m)
|
|
19
|
+
throw new Error("skill is missing front-matter");
|
|
20
|
+
const fm = parseYaml(m[1]);
|
|
21
|
+
assertSkillManifest(fm); // throws "Invalid skill manifest: …"
|
|
22
|
+
return { ...fm, body: m[2] };
|
|
23
|
+
}
|
|
24
|
+
/** Directory names of every bundled skill, sorted. */
|
|
25
|
+
export function skillNames() {
|
|
26
|
+
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
27
|
+
.filter((e) => e.isDirectory() && existsSync(join(SKILLS_DIR, e.name, "SKILL.md")))
|
|
28
|
+
.map((e) => e.name)
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
/** Load a single skill by its directory name. */
|
|
32
|
+
export function loadSkill(name) {
|
|
33
|
+
const path = join(SKILLS_DIR, name, "SKILL.md");
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
throw new Error(`unknown skill: ${name}`);
|
|
36
|
+
const skill = parseSkillManifest(readFileSync(path, "utf8"));
|
|
37
|
+
if (skill.name !== name) {
|
|
38
|
+
throw new Error(`skill ${name}: manifest name "${skill.name}" does not match its directory`);
|
|
39
|
+
}
|
|
40
|
+
return skill;
|
|
41
|
+
}
|
|
42
|
+
/** Load every bundled skill, in directory-name order. */
|
|
43
|
+
export function loadAllSkills() {
|
|
44
|
+
return skillNames().map(loadSkill);
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Rafi templating engine — deliberately tiny (§3 of PLAN.md).
|
|
3
|
+
*
|
|
4
|
+
* Two directives only:
|
|
5
|
+
* - `{{key}}` → substituted from {@link TemplateContext.vars}
|
|
6
|
+
* - `{{#if flag}}…{{/if}}` → body kept when the flag is true, else the whole
|
|
7
|
+
* block (markers + body) is removed
|
|
8
|
+
*
|
|
9
|
+
* No nesting, no expressions, no helpers. An unknown var or flag is a hard error
|
|
10
|
+
* rather than a silent blank, so a typo can never quietly drop guidance from a
|
|
11
|
+
* composed agent. Conditional blocks are resolved first, so vars that live only
|
|
12
|
+
* inside a dropped block are never required to exist.
|
|
13
|
+
*/
|
|
14
|
+
/** Values available to the engine for one render. */
|
|
15
|
+
export interface TemplateContext {
|
|
16
|
+
/** `{{key}}` substitutions (e.g. the resolved `stack.*` strings). */
|
|
17
|
+
vars: Record<string, string>;
|
|
18
|
+
/** Booleans gating `{{#if flag}}…{{/if}}` blocks (e.g. `hasFrontend`). */
|
|
19
|
+
flags: Record<string, boolean>;
|
|
20
|
+
}
|
|
21
|
+
/** Render a template string against the given context. Throws on unknown var/flag. */
|
|
22
|
+
export declare function render(template: string, ctx: TemplateContext): string;
|