@serranolabs.io/munchkins 0.1.0 → 0.1.3
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/agents/_shared/presets.ts +10 -1
- package/agents/bugfix/bugfix-agent.ts +3 -5
- package/agents/director/director-agent.test.ts +149 -0
- package/agents/director/director-agent.ts +45 -0
- package/agents/director/prompts/plan.md +81 -0
- package/agents/director/prompts/spec.md +65 -0
- package/agents/director/prompts/triage.md +49 -0
- package/agents/director/scripts/dispatch.sh +63 -0
- package/agents/director/scripts/inflight-survey.sh +85 -0
- package/agents/director/scripts/repo-survey.sh +63 -0
- package/agents/feat-small/feat-small-agent.ts +3 -1
- package/agents/refactor/refactor-agent.ts +3 -1
- package/package.json +12 -3
- package/skills/director/SKILL.md +54 -0
- package/{agents/bugfix/prompts/bug-fix.md → skills/munchkins-bug-fix/SKILL.md} +5 -0
- package/{agents/feat-small/prompts/feat-small.md → skills/munchkins-feat-small/SKILL.md} +5 -0
- package/skills/{launch-munchkin → munchkins-launch-munchkin}/SKILL.md +1 -1
- package/skills/munchkins-new-munchkin/SKILL.md +646 -0
- package/{agents/refactor/prompts/refactor.md → skills/munchkins-refactor/SKILL.md} +5 -0
- package/src/cmux-launcher.test.ts +158 -0
- package/src/cmux-launcher.ts +70 -0
- package/src/index.ts +29 -4
- package/src/skills-install.test.ts +186 -0
- package/src/skills-install.ts +190 -19
- package/skills/new-munchkin/SKILL.md +0 -343
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { Prompt } from "@serranolabs.io/munchkins-core";
|
|
3
|
+
import { type OptionSchema, Prompt } from "@serranolabs.io/munchkins-core";
|
|
4
4
|
|
|
5
5
|
export function getAgentPromptsDir(importUrl: string): string {
|
|
6
6
|
return join(dirname(fileURLToPath(importUrl)), "prompts");
|
|
@@ -15,12 +15,21 @@ export const SUMMARY_WRITER_PATH = join(SHARED_PROMPTS, "summary-writer.md");
|
|
|
15
15
|
export const TEST_WRITER_PATH = join(SHARED_PROMPTS, "test-writer.md");
|
|
16
16
|
|
|
17
17
|
export const DEFAULT_CHECKS: readonly string[] = [
|
|
18
|
+
"bun run lint:fix",
|
|
18
19
|
"bun run lint",
|
|
19
20
|
"bun run typecheck",
|
|
20
21
|
"bun run scenario",
|
|
21
22
|
"bun test --pass-with-no-tests",
|
|
22
23
|
];
|
|
23
24
|
|
|
25
|
+
// Shared --branch-prefix declaration for the default agents. Director dispatch
|
|
26
|
+
// passes this through to child runs to scope their branches under `director/*`.
|
|
27
|
+
export const BRANCH_PREFIX_OPTION: OptionSchema = {
|
|
28
|
+
type: "string",
|
|
29
|
+
required: false,
|
|
30
|
+
description: "Branch namespace prefix; defaults to 'agent'",
|
|
31
|
+
};
|
|
32
|
+
|
|
24
33
|
export function defaultFixer(): Prompt {
|
|
25
34
|
return new Prompt(DETERMINISTIC_FIXER_PATH);
|
|
26
35
|
}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { AgentBuilder, gitWorktreeSandbox, Prompt, registry } from "@serranolabs.io/munchkins-core";
|
|
3
2
|
import {
|
|
3
|
+
BRANCH_PREFIX_OPTION,
|
|
4
4
|
DEFAULT_CHECKS,
|
|
5
5
|
defaultFixer,
|
|
6
6
|
defaultSummaryWriter,
|
|
7
7
|
GUIDELINES_PATH,
|
|
8
|
-
getAgentPromptsDir,
|
|
9
8
|
REFACTORER_PATH,
|
|
10
9
|
} from "../_shared/presets.js";
|
|
11
10
|
|
|
12
|
-
const PROMPTS = getAgentPromptsDir(import.meta.url);
|
|
13
|
-
|
|
14
11
|
const builder = new AgentBuilder(
|
|
15
12
|
"bug-fix",
|
|
16
13
|
"Fix a bug described in a markdown user-message file.",
|
|
17
14
|
gitWorktreeSandbox(),
|
|
18
15
|
)
|
|
16
|
+
.option("branchPrefix", BRANCH_PREFIX_OPTION)
|
|
19
17
|
.add(
|
|
20
18
|
new Prompt(GUIDELINES_PATH)
|
|
21
|
-
.
|
|
19
|
+
.withSkill("munchkins:bug-fix")
|
|
22
20
|
.withUserMessageFromOption("userMessage", {
|
|
23
21
|
required: true,
|
|
24
22
|
description: "Path to a markdown file describing the bug",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { registry } from "@serranolabs.io/munchkins-core";
|
|
7
|
+
import { $ } from "bun";
|
|
8
|
+
import "./director-agent.js";
|
|
9
|
+
|
|
10
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const REPO_ROOT = resolve(HERE, "../../../..");
|
|
12
|
+
const SCRIPTS = join(HERE, "scripts");
|
|
13
|
+
|
|
14
|
+
const TEST_GIT_IDENTITY = {
|
|
15
|
+
GIT_AUTHOR_NAME: "t",
|
|
16
|
+
GIT_AUTHOR_EMAIL: "t@t",
|
|
17
|
+
GIT_COMMITTER_NAME: "t",
|
|
18
|
+
GIT_COMMITTER_EMAIL: "t@t",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface Repo {
|
|
22
|
+
path: string;
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function createBareRepo(): Promise<Repo> {
|
|
27
|
+
const path = mkdtempSync(join(tmpdir(), "munchkins-director-test-"));
|
|
28
|
+
const env = { ...process.env, ...TEST_GIT_IDENTITY };
|
|
29
|
+
await $`git init -b main`.cwd(path).env(env).quiet();
|
|
30
|
+
await $`git config user.email t@t`.cwd(path).env(env).quiet();
|
|
31
|
+
await $`git config user.name t`.cwd(path).env(env).quiet();
|
|
32
|
+
await Bun.write(join(path, "seed.ts"), "export const seed = 1;\n");
|
|
33
|
+
await $`git add -A`.cwd(path).env(env).quiet();
|
|
34
|
+
await $`git commit -m seed`.cwd(path).env(env).quiet();
|
|
35
|
+
return {
|
|
36
|
+
path,
|
|
37
|
+
cleanup: () => {
|
|
38
|
+
try {
|
|
39
|
+
rmSync(path, { recursive: true, force: true });
|
|
40
|
+
} catch {
|
|
41
|
+
// best-effort
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("director registration", () => {
|
|
48
|
+
test("director is registered under name 'director' after side-effect import", () => {
|
|
49
|
+
expect(registry.get("director")).toBeDefined();
|
|
50
|
+
expect(registry.get("director")?.name).toBe("director");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("cron config is every 10 minutes, thinking verbosity, userMessage 'tick'", () => {
|
|
54
|
+
const builder = registry.get("director");
|
|
55
|
+
expect(builder?.getCron()).toEqual({
|
|
56
|
+
spec: "*/10 * * * *",
|
|
57
|
+
userMessage: "tick",
|
|
58
|
+
verbosity: "thinking",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("step count is 7: 3 deterministic + 3 agent + 1 trailing post-checks", () => {
|
|
63
|
+
// The director's main pipeline is 6 steps (steps 1–6); the trailing
|
|
64
|
+
// .addDeterministic(DEFAULT_CHECKS, defaultFixer) is the 7th, kept for
|
|
65
|
+
// parity with other munchkins so the integration phase has a gate to run.
|
|
66
|
+
expect(registry.get("director")?.getStepCount()).toBe(7);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("director opts out of the framework's --dry-run short-circuit", () => {
|
|
70
|
+
expect(registry.get("director")?.getHandlesDryRun()).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("director scripts", () => {
|
|
75
|
+
let repo: Repo;
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
repo = await createBareRepo();
|
|
79
|
+
// The scripts read $WORKTREE and $REPO_ROOT from the env; for these
|
|
80
|
+
// tests both point at the same temp repo (one-checkout fixture).
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
repo.cleanup();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("repo-survey.sh exits non-zero with the expected error when PURPOSE.md is absent", async () => {
|
|
88
|
+
const env = {
|
|
89
|
+
...process.env,
|
|
90
|
+
...TEST_GIT_IDENTITY,
|
|
91
|
+
WORKTREE: repo.path,
|
|
92
|
+
REPO_ROOT: repo.path,
|
|
93
|
+
PATH: process.env.PATH ?? "",
|
|
94
|
+
};
|
|
95
|
+
// Seed the .director/current sentinel; the script checks PURPOSE.md
|
|
96
|
+
// before reading it, so the failure path doesn't depend on this — but
|
|
97
|
+
// having it present rules out "missing sentinel" as the failure mode.
|
|
98
|
+
await $`mkdir -p ${repo.path}/.director`.quiet();
|
|
99
|
+
await Bun.write(join(repo.path, ".director", "current"), "fake-run");
|
|
100
|
+
const r = await $`bash ${join(SCRIPTS, "repo-survey.sh")}`.env(env).nothrow().quiet();
|
|
101
|
+
expect(r.exitCode).not.toBe(0);
|
|
102
|
+
const combined = r.stdout.toString() + r.stderr.toString();
|
|
103
|
+
expect(combined).toContain("PURPOSE.md not found at repo root");
|
|
104
|
+
expect(combined).toContain("docs/pages/agents/director.md");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("inflight-survey.sh in a repo with no director/* branches writes inflight.json with empty branches and worktrees", async () => {
|
|
108
|
+
// Stub `gh` to fail so the PR inventory falls back to '[]'.
|
|
109
|
+
const stubDir = mkdtempSync(join(tmpdir(), "munchkins-director-stub-"));
|
|
110
|
+
try {
|
|
111
|
+
await Bun.write(join(stubDir, "gh"), "#!/usr/bin/env bash\nexit 1\n");
|
|
112
|
+
await $`chmod +x ${join(stubDir, "gh")}`.quiet();
|
|
113
|
+
|
|
114
|
+
const env = {
|
|
115
|
+
...process.env,
|
|
116
|
+
...TEST_GIT_IDENTITY,
|
|
117
|
+
WORKTREE: repo.path,
|
|
118
|
+
REPO_ROOT: repo.path,
|
|
119
|
+
PATH: `${stubDir}:${process.env.PATH ?? ""}`,
|
|
120
|
+
};
|
|
121
|
+
const r = await $`bash ${join(SCRIPTS, "inflight-survey.sh")}`.env(env).nothrow().quiet();
|
|
122
|
+
expect(r.exitCode).toBe(0);
|
|
123
|
+
|
|
124
|
+
const runId = (await Bun.file(join(repo.path, ".director", "current")).text()).trim();
|
|
125
|
+
expect(runId).toMatch(/^\d{8}T\d{6}-/);
|
|
126
|
+
|
|
127
|
+
const inflightPath = join(repo.path, ".director", runId, "inflight.json");
|
|
128
|
+
const inflight = JSON.parse(await Bun.file(inflightPath).text());
|
|
129
|
+
expect(inflight.branches).toEqual([]);
|
|
130
|
+
expect(inflight.worktrees).toEqual([]);
|
|
131
|
+
// gh stub fails, so prs falls back to '[]' (empty array).
|
|
132
|
+
expect(inflight.prs).toEqual([]);
|
|
133
|
+
} finally {
|
|
134
|
+
rmSync(stubDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Smoke check that the director skill symlink resolves to the package source.
|
|
140
|
+
// Important because every agent step loads the skill via withSkill("director")
|
|
141
|
+
// and a stale symlink would surface as a runtime "skill not found".
|
|
142
|
+
describe("director skill availability", () => {
|
|
143
|
+
test(".claude/skills/director points at packages/munchkins/skills/director", async () => {
|
|
144
|
+
const skillPath = join(REPO_ROOT, ".claude/skills/director/SKILL.md");
|
|
145
|
+
const content = await Bun.file(skillPath).text();
|
|
146
|
+
expect(content).toContain("name: director");
|
|
147
|
+
expect(content).toContain("Vertical-slice rule");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { AgentBuilder, gitWorktreeSandbox, Prompt, registry } from "@serranolabs.io/munchkins-core";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_CHECKS,
|
|
6
|
+
defaultFixer,
|
|
7
|
+
defaultSummaryWriter,
|
|
8
|
+
GUIDELINES_PATH,
|
|
9
|
+
} from "../_shared/presets.js";
|
|
10
|
+
|
|
11
|
+
const AGENT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PROMPTS = join(AGENT_DIR, "prompts");
|
|
13
|
+
const SCRIPTS = join(AGENT_DIR, "scripts");
|
|
14
|
+
|
|
15
|
+
const builder = new AgentBuilder(
|
|
16
|
+
"director",
|
|
17
|
+
"Cron-driven orchestrator that triages, plans, and dispatches work via other munchkins.",
|
|
18
|
+
gitWorktreeSandbox(),
|
|
19
|
+
)
|
|
20
|
+
// userMessage is the cron tick payload; the daemon also passes it via env.
|
|
21
|
+
// No prompt step reads it directly — the agent's source of truth is
|
|
22
|
+
// PURPOSE.md plus the in-flight survey, not a per-tick user message.
|
|
23
|
+
.option("userMessage", {
|
|
24
|
+
type: "string",
|
|
25
|
+
required: false,
|
|
26
|
+
description:
|
|
27
|
+
"Per-tick payload (default 'tick'); unused by the pipeline but required by the CLI surface.",
|
|
28
|
+
default: "tick",
|
|
29
|
+
})
|
|
30
|
+
.addDeterministic([`bash ${join(SCRIPTS, "inflight-survey.sh")}`])
|
|
31
|
+
.addDeterministic([`bash ${join(SCRIPTS, "repo-survey.sh")}`])
|
|
32
|
+
.add(new Prompt(GUIDELINES_PATH).withSkill("director").withSystem(join(PROMPTS, "triage.md")))
|
|
33
|
+
.add(new Prompt(GUIDELINES_PATH).withSkill("director").withSystem(join(PROMPTS, "spec.md")))
|
|
34
|
+
.add(new Prompt(GUIDELINES_PATH).withSkill("director").withSystem(join(PROMPTS, "plan.md")))
|
|
35
|
+
.addDeterministic([`bash ${join(SCRIPTS, "dispatch.sh")}`])
|
|
36
|
+
.addDeterministic([...DEFAULT_CHECKS], {
|
|
37
|
+
loop: { maxIterations: 3, fixer: defaultFixer() },
|
|
38
|
+
})
|
|
39
|
+
.summaryWriter(defaultSummaryWriter())
|
|
40
|
+
.handlesDryRun()
|
|
41
|
+
.cron("*/10 * * * *", { userMessage: "tick", verbosity: "thinking" });
|
|
42
|
+
|
|
43
|
+
registry.register(builder);
|
|
44
|
+
|
|
45
|
+
export { builder };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Director step 5 — Plan
|
|
2
|
+
|
|
3
|
+
You are the **Plan** step of the director pipeline. Pipeline position: step 5 of 6.
|
|
4
|
+
|
|
5
|
+
## Early-exit on upstream idle
|
|
6
|
+
|
|
7
|
+
Read `.director/<run>/spec.md` first (discover `<run>` via `.director/current`). If the file's first non-empty line contains `"idle": true`, write the following to `.director/<run>/plan.md` and exit immediately:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
{"idle": true, "reason": "upstream idle"}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Do not run any other step.
|
|
14
|
+
|
|
15
|
+
## Input artifacts
|
|
16
|
+
|
|
17
|
+
- `.director/<run>/spec.md` — the thinnest viable slice from the Spec step.
|
|
18
|
+
|
|
19
|
+
## What to do
|
|
20
|
+
|
|
21
|
+
Two passes, in the same conversation:
|
|
22
|
+
|
|
23
|
+
### Pass A — Design tree
|
|
24
|
+
|
|
25
|
+
Read the spec. Enumerate every concrete decision point or ambiguity a downstream implementer would have to resolve. Examples:
|
|
26
|
+
|
|
27
|
+
- "Which file owns the new helper?"
|
|
28
|
+
- "Sync or async API shape?"
|
|
29
|
+
- "New test file or extend an existing one?"
|
|
30
|
+
- "Does this need a feature flag?"
|
|
31
|
+
|
|
32
|
+
Be exhaustive. If a decision is genuinely trivial, name it and mark it `trivial`. If it's load-bearing, name it and leave it open.
|
|
33
|
+
|
|
34
|
+
### Pass B — Architect resolution
|
|
35
|
+
|
|
36
|
+
For each non-trivial item in Pass A, make an opinionated call. Cite the reason in one sentence. Apply these tiebreakers in order:
|
|
37
|
+
|
|
38
|
+
1. **Match the repo's existing patterns.** Read the relevant code; copy the shape that's already there.
|
|
39
|
+
2. **Smaller change wins.** Prefer extension over new modules; prefer composition over abstraction.
|
|
40
|
+
3. **Reversibility.** When two options have similar surface, pick the one easier to undo.
|
|
41
|
+
|
|
42
|
+
If after one pass an ambiguity remains genuinely unresolvable (the spec is too vague, both options are equally load-bearing, the architect doesn't have enough context), **idle the tick**. Don't ship a half-resolved plan.
|
|
43
|
+
|
|
44
|
+
## Output
|
|
45
|
+
|
|
46
|
+
Write exactly one file: `.director/<run>/plan.md`.
|
|
47
|
+
|
|
48
|
+
Non-idle format — a user-message-ready markdown brief that the downstream munchkin can consume directly:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
# <slice title from spec.md>
|
|
52
|
+
|
|
53
|
+
## Goal
|
|
54
|
+
<one paragraph>
|
|
55
|
+
|
|
56
|
+
## Files to modify
|
|
57
|
+
- `<path>` — <what changes>
|
|
58
|
+
- ...
|
|
59
|
+
|
|
60
|
+
## Implementation plan
|
|
61
|
+
<numbered steps; each step names files and the resolved design decision behind it>
|
|
62
|
+
|
|
63
|
+
## Acceptance criteria
|
|
64
|
+
- <runnable checks>
|
|
65
|
+
- ...
|
|
66
|
+
|
|
67
|
+
## Out of scope
|
|
68
|
+
- <explicit non-changes>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Idle format:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{"idle": true, "reason": "<short — e.g. 'architect could not resolve sync-vs-async ambiguity'>"}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Rules
|
|
78
|
+
|
|
79
|
+
- Touch no other files. Your only side effect is writing `plan.md`.
|
|
80
|
+
- The non-idle plan must be self-contained — the downstream munchkin will receive it as `--user-message` and won't have context for anything you leave implicit.
|
|
81
|
+
- Idle is valid. Better to idle than to ship a half-resolved plan.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Director step 4 — Spec
|
|
2
|
+
|
|
3
|
+
You are the **Spec** step of the director pipeline. Pipeline position: step 4 of 6.
|
|
4
|
+
|
|
5
|
+
## Early-exit on upstream idle
|
|
6
|
+
|
|
7
|
+
Read `.director/<run>/triage.json` first (discover `<run>` via `.director/current`). If it contains `"idle": true`, write the following to `.director/<run>/spec.md` and exit immediately:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
{"idle": true, "reason": "upstream idle"}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Do not run any other step.
|
|
14
|
+
|
|
15
|
+
## Input artifacts
|
|
16
|
+
|
|
17
|
+
- `.director/<run>/triage.json` — non-idle: `{ work_type, justification, independence_argument, goal }`.
|
|
18
|
+
- `PURPOSE.md` — for tone-matching the slice against the repo's success criteria.
|
|
19
|
+
|
|
20
|
+
## What to do
|
|
21
|
+
|
|
22
|
+
Two passes, in the same conversation:
|
|
23
|
+
|
|
24
|
+
### Pass A — Ambitious draft
|
|
25
|
+
|
|
26
|
+
Produce the most ambitious version of the slice that is still consistent with the `work_type`. Name the files, the new public surfaces, the user-visible behavior, the acceptance criteria. Don't trim yet — this pass exists so the next pass has something concrete to cut.
|
|
27
|
+
|
|
28
|
+
### Pass B — Less-is-more cut
|
|
29
|
+
|
|
30
|
+
Re-read Pass A. Cut everything that isn't load-bearing for the slice's stated `goal`. Apply ruthlessly:
|
|
31
|
+
|
|
32
|
+
- Remove "nice to have" sub-features.
|
|
33
|
+
- Collapse multiple acceptance criteria into one if they test the same thing.
|
|
34
|
+
- Strip any "we should also" expansions — they belong in a future tick.
|
|
35
|
+
- Prefer extending existing files over creating new ones.
|
|
36
|
+
|
|
37
|
+
The result is the **thinnest viable slice**.
|
|
38
|
+
|
|
39
|
+
## Output
|
|
40
|
+
|
|
41
|
+
Write exactly one file: `.director/<run>/spec.md`. It must contain only the post-cut spec — *not* Pass A. Structure:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
# <short slice title>
|
|
45
|
+
|
|
46
|
+
## Goal
|
|
47
|
+
<one paragraph from triage.json, refined>
|
|
48
|
+
|
|
49
|
+
## Files in scope
|
|
50
|
+
- `<path>` — <one-line reason>
|
|
51
|
+
- ...
|
|
52
|
+
|
|
53
|
+
## Acceptance criteria
|
|
54
|
+
- <observable, runnable check>
|
|
55
|
+
- ...
|
|
56
|
+
|
|
57
|
+
## Out of scope
|
|
58
|
+
- <explicit cuts from Pass A>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
|
|
63
|
+
- Touch no other files. Your only side effect is writing `spec.md`.
|
|
64
|
+
- The spec must be small enough that the downstream munchkin (`feat-small` / `bug-fix` / `refactor`) can ship it in one run without expanding scope.
|
|
65
|
+
- If during Pass B you realize the slice is actually two slices, keep one and put the other in "Out of scope".
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Director step 3 — Triage
|
|
2
|
+
|
|
3
|
+
You are the **Triage** step of the director pipeline. Pipeline position: step 3 of 6.
|
|
4
|
+
|
|
5
|
+
## Input artifacts (in the worktree)
|
|
6
|
+
|
|
7
|
+
- `PURPOSE.md` — the repo's north star. Source of truth for what counts as "advancing the project".
|
|
8
|
+
- `.director/<run>/inflight.json` — array of in-flight `director/*` work (PRs, branches, worktrees) with file scope and goal. May be `[]`.
|
|
9
|
+
- `.director/<run>/survey.md` — repo state: recent `git log`, open PRs (all), lint/typecheck status.
|
|
10
|
+
|
|
11
|
+
Discover the current run directory by reading `.director/current` from the worktree root.
|
|
12
|
+
|
|
13
|
+
## What to do
|
|
14
|
+
|
|
15
|
+
1. Read all three input artifacts.
|
|
16
|
+
2. Brainstorm candidate slices that would advance an unmet `PURPOSE.md` success criterion. Be ambitious about *what* matters; be specific about file scope.
|
|
17
|
+
3. For each candidate, apply the **vertical-slice rule** (disjoint file scope / no upstream dep / no downstream coupling) against every entry in `inflight.json`.
|
|
18
|
+
4. Among candidates that pass the rule, apply the **"less is more" tiebreakers** (cheaper work type wins; fewer files / fewer concepts wins; don't refactor newly shipped features).
|
|
19
|
+
5. Pick exactly one candidate. If no candidate qualifies, idle.
|
|
20
|
+
|
|
21
|
+
## Output
|
|
22
|
+
|
|
23
|
+
Write exactly one file: `.director/<run>/triage.json`.
|
|
24
|
+
|
|
25
|
+
Schema (non-idle):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"work_type": "feature" | "bug-fix" | "refactor" | "performance",
|
|
30
|
+
"justification": "<2-4 sentences: why this slice now, citing which PURPOSE.md bullet it advances>",
|
|
31
|
+
"independence_argument": "<for each inflight entry, a sentence explaining why the three vertical-slice criteria pass>",
|
|
32
|
+
"goal": "<one paragraph describing the slice — what changes, in which files, observable outcome>"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Schema (idle):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"idle": true,
|
|
41
|
+
"reason": "<short — e.g. 'all candidates depend on PR #42' or 'PURPOSE.md success criteria all satisfied'>"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Rules
|
|
46
|
+
|
|
47
|
+
- Touch no other files. Do **not** modify code, commit, or open PRs. Your only side effect is writing `triage.json`.
|
|
48
|
+
- The `goal` field must be specific enough that the Spec step (next) can turn it into a thinnest-viable-slice without re-deriving intent.
|
|
49
|
+
- Idle is valid. Better to idle than to ship a slice that violates the vertical-slice rule.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Step 6 — Dispatch. Reads triage.json + plan.md, derives the target munchkin
|
|
3
|
+
# from work_type, then runs the child as a foreground subprocess. Early-exits
|
|
4
|
+
# on upstream idle and short-circuits to a print-only path when
|
|
5
|
+
# $__MUNCHKINS_OPT_dryRun is "true".
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
WORKDIR="${WORKTREE:-$PWD}"
|
|
10
|
+
REPO="${REPO_ROOT:-$WORKDIR}"
|
|
11
|
+
|
|
12
|
+
RUN_ID="$(cat "$WORKDIR/.director/current" 2>/dev/null || true)"
|
|
13
|
+
if [ -z "$RUN_ID" ]; then
|
|
14
|
+
echo "[director] dispatch: .director/current missing — pipeline did not initialize" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
RUN_DIR="$WORKDIR/.director/$RUN_ID"
|
|
19
|
+
TRIAGE="$RUN_DIR/triage.json"
|
|
20
|
+
PLAN="$RUN_DIR/plan.md"
|
|
21
|
+
|
|
22
|
+
# Upstream-idle guard: if any upstream artifact says idle, exit 0.
|
|
23
|
+
if [ -f "$TRIAGE" ] && grep -qE '"idle"[[:space:]]*:[[:space:]]*true' "$TRIAGE"; then
|
|
24
|
+
echo "[director] dispatch: triage idle — nothing to do"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
if [ -f "$PLAN" ] && head -n 5 "$PLAN" | grep -qE '"idle"[[:space:]]*:[[:space:]]*true'; then
|
|
28
|
+
echo "[director] dispatch: plan idle — nothing to do"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$TRIAGE" ] || [ ! -f "$PLAN" ]; then
|
|
33
|
+
echo "[director] dispatch: missing triage.json or plan.md in $RUN_DIR" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Extract work_type. Use a Bun one-liner so we don't ship a shell JSON parser.
|
|
38
|
+
work_type="$(bun -e '
|
|
39
|
+
const t = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf-8"));
|
|
40
|
+
process.stdout.write(String(t.work_type ?? ""));
|
|
41
|
+
' "$TRIAGE")"
|
|
42
|
+
|
|
43
|
+
case "$work_type" in
|
|
44
|
+
feature) target="feat-small" ;;
|
|
45
|
+
bug-fix) target="bug-fix" ;;
|
|
46
|
+
refactor) target="refactor" ;;
|
|
47
|
+
performance) target="refactor" ;; # Phase 1 mapping; replaced by `performance` in Phase 2.
|
|
48
|
+
*)
|
|
49
|
+
echo "[director] dispatch: unknown work_type \"$work_type\" in $TRIAGE" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
cmd=(bun run munchkins "$target" "--user-message=$PLAN" "--branch-prefix=director")
|
|
55
|
+
|
|
56
|
+
if [ "${__MUNCHKINS_OPT_dryRun:-}" = "true" ]; then
|
|
57
|
+
echo "[director] dispatch (dry-run): ${cmd[*]}"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo "[director] dispatch: ${cmd[*]}"
|
|
62
|
+
cd "$REPO"
|
|
63
|
+
exec "${cmd[@]}"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Step 1 — Inflight-survey. Inventories all director-spawned work currently
|
|
3
|
+
# in flight: open director/* PRs (when `gh` is available + authed), local
|
|
4
|
+
# director/* branches, and director-namespaced worktrees.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
WORKDIR="${WORKTREE:-$PWD}"
|
|
9
|
+
DIRECTOR_ROOT="$WORKDIR/.director"
|
|
10
|
+
|
|
11
|
+
# Derive a stable, sortable run id and remember it for downstream steps.
|
|
12
|
+
RUN_ID="$(date -u +%Y%m%dT%H%M%S)-${RANDOM}${RANDOM}"
|
|
13
|
+
RUN_DIR="$DIRECTOR_ROOT/$RUN_ID"
|
|
14
|
+
mkdir -p "$RUN_DIR"
|
|
15
|
+
printf '%s' "$RUN_ID" > "$DIRECTOR_ROOT/current"
|
|
16
|
+
|
|
17
|
+
OUT="$RUN_DIR/inflight.json"
|
|
18
|
+
|
|
19
|
+
# Branch inventory (always available).
|
|
20
|
+
branches=()
|
|
21
|
+
while IFS= read -r br; do
|
|
22
|
+
br="${br#"${br%%[![:space:]]*}"}"
|
|
23
|
+
[ -n "$br" ] && branches+=("$br")
|
|
24
|
+
done < <(git -C "$WORKDIR" branch --list 'director/*' --format='%(refname:short)' 2>/dev/null || true)
|
|
25
|
+
|
|
26
|
+
# Worktree inventory (filter porcelain output for director-namespaced branches).
|
|
27
|
+
worktrees=()
|
|
28
|
+
if git -C "$WORKDIR" worktree list --porcelain >/dev/null 2>&1; then
|
|
29
|
+
current_path=""
|
|
30
|
+
while IFS= read -r line; do
|
|
31
|
+
case "$line" in
|
|
32
|
+
"worktree "*) current_path="${line#worktree }" ;;
|
|
33
|
+
"branch refs/heads/director/"*)
|
|
34
|
+
wbranch="${line#branch refs/heads/}"
|
|
35
|
+
worktrees+=("$current_path|$wbranch")
|
|
36
|
+
;;
|
|
37
|
+
"")
|
|
38
|
+
current_path=""
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
done < <(git -C "$WORKDIR" worktree list --porcelain)
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Open PRs against director/* heads. Best-effort: skip silently if `gh` is
|
|
45
|
+
# missing or unauthenticated — the local inventory is still useful.
|
|
46
|
+
pr_json="[]"
|
|
47
|
+
if command -v gh >/dev/null 2>&1; then
|
|
48
|
+
if pr_json_raw="$(gh pr list --head 'director/*' --state open \
|
|
49
|
+
--json number,title,headRefName,files 2>/dev/null)"; then
|
|
50
|
+
pr_json="$pr_json_raw"
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Compose final JSON. Branches and worktrees are flat string arrays; PRs is
|
|
55
|
+
# whatever `gh` emitted (already JSON). Idle case is the empty triplet — the
|
|
56
|
+
# triage step treats it as "nothing in flight".
|
|
57
|
+
{
|
|
58
|
+
printf '{\n'
|
|
59
|
+
printf ' "branches": ['
|
|
60
|
+
if [ "${#branches[@]}" -gt 0 ]; then
|
|
61
|
+
sep=""
|
|
62
|
+
for b in "${branches[@]}"; do
|
|
63
|
+
esc="${b//\\/\\\\}"
|
|
64
|
+
esc="${esc//\"/\\\"}"
|
|
65
|
+
printf '%s"%s"' "$sep" "$esc"
|
|
66
|
+
sep=", "
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
printf '],\n'
|
|
70
|
+
printf ' "worktrees": ['
|
|
71
|
+
if [ "${#worktrees[@]}" -gt 0 ]; then
|
|
72
|
+
sep=""
|
|
73
|
+
for w in "${worktrees[@]}"; do
|
|
74
|
+
p="${w%%|*}"
|
|
75
|
+
bv="${w##*|}"
|
|
76
|
+
printf '%s{"path": "%s", "branch": "%s"}' "$sep" "${p//\"/\\\"}" "${bv//\"/\\\"}"
|
|
77
|
+
sep=", "
|
|
78
|
+
done
|
|
79
|
+
fi
|
|
80
|
+
printf '],\n'
|
|
81
|
+
printf ' "prs": %s\n' "$pr_json"
|
|
82
|
+
printf '}\n'
|
|
83
|
+
} > "$OUT"
|
|
84
|
+
|
|
85
|
+
echo "[director] inflight-survey wrote $OUT (run-id=$RUN_ID)"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Step 2 — Repo-survey. Captures `git log`, `gh pr list`, lint/typecheck status
|
|
3
|
+
# into a markdown brief the triage step reads alongside PURPOSE.md.
|
|
4
|
+
#
|
|
5
|
+
# Gates the rest of the pipeline on PURPOSE.md being present at the repo root.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
WORKDIR="${WORKTREE:-$PWD}"
|
|
10
|
+
REPO="${REPO_ROOT:-$WORKDIR}"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$REPO/PURPOSE.md" ]; then
|
|
13
|
+
echo "PURPOSE.md not found at repo root. The director requires a written north star. See docs/pages/agents/director.md." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
RUN_ID="$(cat "$WORKDIR/.director/current" 2>/dev/null || true)"
|
|
18
|
+
if [ -z "$RUN_ID" ]; then
|
|
19
|
+
echo "[director] repo-survey: .director/current missing — did inflight-survey run?" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
RUN_DIR="$WORKDIR/.director/$RUN_ID"
|
|
24
|
+
mkdir -p "$RUN_DIR"
|
|
25
|
+
OUT="$RUN_DIR/survey.md"
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
echo "# Director repo survey — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
29
|
+
echo
|
|
30
|
+
echo "## Recent commits (\`git log --oneline -30\`)"
|
|
31
|
+
echo
|
|
32
|
+
echo '```'
|
|
33
|
+
git -C "$REPO" log --oneline -30 2>&1 || true
|
|
34
|
+
echo '```'
|
|
35
|
+
echo
|
|
36
|
+
echo "## Open PRs (\`gh pr list\`)"
|
|
37
|
+
echo
|
|
38
|
+
if command -v gh >/dev/null 2>&1; then
|
|
39
|
+
echo '```'
|
|
40
|
+
gh pr list --limit 30 2>&1 || echo "(gh pr list failed — likely unauthenticated)"
|
|
41
|
+
echo '```'
|
|
42
|
+
else
|
|
43
|
+
echo "(\`gh\` not on PATH — open PRs unknown)"
|
|
44
|
+
fi
|
|
45
|
+
echo
|
|
46
|
+
echo "## Lint status (\`bun run lint\`)"
|
|
47
|
+
echo
|
|
48
|
+
if (cd "$REPO" && bun run lint >/dev/null 2>&1); then
|
|
49
|
+
echo "PASS"
|
|
50
|
+
else
|
|
51
|
+
echo "FAIL"
|
|
52
|
+
fi
|
|
53
|
+
echo
|
|
54
|
+
echo "## Typecheck status (\`bun run typecheck\`)"
|
|
55
|
+
echo
|
|
56
|
+
if (cd "$REPO" && bun run typecheck >/dev/null 2>&1); then
|
|
57
|
+
echo "PASS"
|
|
58
|
+
else
|
|
59
|
+
echo "FAIL"
|
|
60
|
+
fi
|
|
61
|
+
} > "$OUT"
|
|
62
|
+
|
|
63
|
+
echo "[director] repo-survey wrote $OUT"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { AgentBuilder, gitWorktreeSandbox, Prompt, registry } from "@serranolabs.io/munchkins-core";
|
|
3
3
|
import {
|
|
4
|
+
BRANCH_PREFIX_OPTION,
|
|
4
5
|
DEFAULT_CHECKS,
|
|
5
6
|
defaultFixer,
|
|
6
7
|
GUIDELINES_PATH,
|
|
@@ -16,9 +17,10 @@ const builder = new AgentBuilder(
|
|
|
16
17
|
"Implement a new feature described in a markdown user-message file.",
|
|
17
18
|
gitWorktreeSandbox(),
|
|
18
19
|
)
|
|
20
|
+
.option("branchPrefix", BRANCH_PREFIX_OPTION)
|
|
19
21
|
.add(
|
|
20
22
|
new Prompt(GUIDELINES_PATH)
|
|
21
|
-
.
|
|
23
|
+
.withSkill("munchkins:feat-small")
|
|
22
24
|
.withUserMessageFromOption("userMessage", {
|
|
23
25
|
required: true,
|
|
24
26
|
description: "Path to a markdown file (or inline text) describing the feature",
|