@printpractical/cruze 0.0.2 → 0.0.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/README.md +9 -9
- package/dist/adapters/inbound/cli/commands/review.js +1 -1
- package/dist/adapters/inbound/cli/commands.js +1 -1
- package/dist/adapters/outbound/node_project_files.js +2 -1
- package/dist/adapters/outbound/process_agent_runner.js +8 -3
- package/dist/app/ports/agent_runner.js +5 -6
- package/dist/app/use_cases/init_project.js +31 -3
- package/dist/app/use_cases/run_review.js +25 -11
- package/dist/domain/config.js +8 -8
- package/dist/domain/scaffold.js +15 -1
- package/package.json +1 -1
- package/skills/formats/reference/config.md +1 -1
- package/skills/realign/notes/0.0.3.md +11 -0
- package/skills/realign/notes.md +1 -0
- package/skills/roles/SKILL.md +1 -1
- package/src/adapters/inbound/cli/commands/review.ts +1 -1
- package/src/adapters/inbound/cli/commands.ts +1 -1
- package/src/adapters/outbound/node_project_files.ts +2 -1
- package/src/adapters/outbound/process_agent_runner.ts +8 -3
- package/src/app/ports/agent_runner.ts +5 -6
- package/src/app/use_cases/init_project.ts +31 -3
- package/src/app/use_cases/run_review.ts +25 -11
- package/src/domain/config.ts +8 -8
- package/src/domain/scaffold.ts +15 -1
- package/templates/init/config.yaml +1 -6
- package/templates/init/review/claude.yaml +7 -0
- package/templates/init/review/other.yaml +7 -0
- package/templates/init/CLAUDE.md +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It works with any agent that supports the [Agent Skills](https://agentskills.io)
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
Version 0.0.
|
|
13
|
+
Version 0.0.3 is the third preview. It sets projects up for agents other than Claude Code, and runs reviews in the agent's own helpers when no review agent is configured. Early development. The CLI is complete for the V1 lifecycle. The document formats ship as the `cruze-formats` skill, the standards an agent designs and codes to ship as knowledge skills, and the whole workflow, from explore to land, ships as workflow skills. Claude Code is the agent tested so far.
|
|
14
14
|
|
|
15
15
|
## Why
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ The skills run `cruze` commands, so the agent needs `cruze` on its path. Install
|
|
|
35
35
|
From a GitHub Release, without npm. Each release attaches the packed package:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install -g https://github.com/PrintPractical/cruze/releases/download/v0.0.
|
|
38
|
+
npm install -g https://github.com/PrintPractical/cruze/releases/download/v0.0.3/printpractical-cruze-0.0.3.tgz
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
From npm, once a version is published there:
|
|
@@ -47,7 +47,7 @@ npm install -g @printpractical/cruze
|
|
|
47
47
|
From the git repository, at a tag or `#main`. This needs git and access to the repository. npm 11 needs `--install-links` for a global git install, and the first `cruze` command builds the CLI, which takes a moment:
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
npm install -g --install-links github:PrintPractical/cruze#v0.0.
|
|
50
|
+
npm install -g --install-links github:PrintPractical/cruze#v0.0.3
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### 2. Set up a repository
|
|
@@ -61,21 +61,21 @@ cruze init
|
|
|
61
61
|
When you didn't install from npm, tell `init` where Cruze came from, so the CI it writes runs the same source instead of the npm package:
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
cruze init --package https://github.com/PrintPractical/cruze/releases/download/v0.0.
|
|
64
|
+
cruze init --package https://github.com/PrintPractical/cruze/releases/download/v0.0.3/printpractical-cruze-0.0.3.tgz
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`init` asks for the project name,
|
|
67
|
+
`init` asks for the project name, and which coding agent you use: `claude` for Claude Code, or `other` for any agent that reads `AGENTS.md` and `.agents/skills/`, such as Codex or OpenCode. It doesn't ask when you pass `--agent claude` or `--agent other`, or when the repository already has `CLAUDE.md` or `.claude/`. With `--yes`, or without a terminal, it sets up Claude Code. Then it creates:
|
|
68
68
|
|
|
69
69
|
| Path | Purpose |
|
|
70
70
|
| --- | --- |
|
|
71
71
|
| `README.md` | Titled with the project name |
|
|
72
72
|
| `CHANGELOG.md` | In [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format |
|
|
73
73
|
| `AGENTS.md` | Agent instructions; Cruze keeps its managed sections current |
|
|
74
|
-
| `CLAUDE.md` |
|
|
75
|
-
| `.cruze/config.yaml` | Project configuration, including the Cruze version whose skills the project uses |
|
|
74
|
+
| `CLAUDE.md` | For Claude Code only: a link to `AGENTS.md` |
|
|
75
|
+
| `.cruze/config.yaml` | Project configuration, including the Cruze version whose skills the project uses, and for Claude Code the command `cruze review` runs |
|
|
76
76
|
| `.github/workflows/ci.yml` | CI that runs `cruze validate`, `check` and `trace`; the walking-skeleton change adds the language's build and test steps |
|
|
77
77
|
| `.gitattributes` | Merges Cruze's journals line by line, so parallel branches don't conflict on them |
|
|
78
|
-
| `.agents/skills/cruze-*` | The Cruze skills, linked into `.claude/skills/` for Claude Code |
|
|
78
|
+
| `.agents/skills/cruze-*` | The Cruze skills, also linked into `.claude/skills/` for Claude Code |
|
|
79
79
|
|
|
80
80
|
Existing files are never overwritten, so it is safe to run in an existing repository.
|
|
81
81
|
|
|
@@ -130,7 +130,7 @@ Skills call these at fixed points, and CI runs `check` and `trace` on every push
|
|
|
130
130
|
| `cruze roadmap prune` | Clears landed items from the roadmap's status when a release closes |
|
|
131
131
|
| `cruze trace` | Fails when a delivered scenario has no test carrying its ID; `--all` checks every built scenario |
|
|
132
132
|
| `cruze check` | Enforces the layer rules and the file budgets; `--ci` also fails on budget warnings |
|
|
133
|
-
| `cruze review <role>` | Runs a role (design reviewer, code reviewer, verifier, researcher or auditor) in a fresh agent context, through the command in `.cruze/config.yaml
|
|
133
|
+
| `cruze review <role>` | Runs a role (design reviewer, code reviewer, verifier, researcher or auditor) in a fresh agent context, through the command in `.cruze/config.yaml`. When no command is set, or its program isn't installed, it prints the role's prompt for the agent to run in a helper of its own |
|
|
134
134
|
| `cruze land` | Merges a verified change into the living docs, re-stamps the approvals the merge would make stale, fills the Commands and Layout sections of `AGENTS.md`, and archives finished work |
|
|
135
135
|
| `cruze realign status` | Lists what the installed Cruze expects of existing code since its `standards:` version, with what the CLI rules find now; `--full` covers every release |
|
|
136
136
|
| `cruze realign done` | Moves `standards:` up once every finding is fixed, waived or scheduled in a change named with `--change` |
|
|
@@ -14,5 +14,5 @@ export async function runReviewCommand(context , args , opti
|
|
|
14
14
|
...(blockers === undefined ? {} : { blockers }),
|
|
15
15
|
...(options.base === undefined ? {} : { base: options.base }),
|
|
16
16
|
});
|
|
17
|
-
return { json: report, human: report.report };
|
|
17
|
+
return { json: report, human: report.status === "reported" ? report.report : `${report.instruction}\n\n${report.prompt}` };
|
|
18
18
|
}
|
|
@@ -10,7 +10,7 @@ import { runReviewCommand } from "./commands/review.js";
|
|
|
10
10
|
import { runNext, runStatus } from "./commands/status.js";
|
|
11
11
|
|
|
12
12
|
export const COMMANDS = {
|
|
13
|
-
init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent
|
|
13
|
+
init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent claude|other] [--package <npm spec>] set up this repository and install the skills" },
|
|
14
14
|
install: { handler: runInstall, usage: "install [--agent <agent>] install or update the Cruze skills" },
|
|
15
15
|
validate: { handler: runValidate, usage: "validate check every document against the formats" },
|
|
16
16
|
status: { handler: runStatus, usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
@@ -78,7 +78,8 @@ export class NodeProjectFiles {
|
|
|
78
78
|
this.assertInsideRoot(resolve(dirname(linkPath), target));
|
|
79
79
|
await mkdir(dirname(linkPath), { recursive: true });
|
|
80
80
|
await rm(linkPath, { recursive: true, force: true });
|
|
81
|
-
|
|
81
|
+
// No link type: on Windows, Node picks a file or directory link from what the target is.
|
|
82
|
+
await symlink(target, linkPath);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** Resolves a project-relative path, refusing anything outside the root. */
|
|
@@ -11,15 +11,20 @@ export class ProcessAgentRunner {
|
|
|
11
11
|
|
|
12
12
|
run(command , prompt ) {
|
|
13
13
|
const [program, ...args] = command;
|
|
14
|
-
if (program === undefined) return Promise.resolve({
|
|
14
|
+
if (program === undefined) return Promise.resolve({ kind: "missing" });
|
|
15
15
|
return new Promise((resolve) => {
|
|
16
16
|
const child = spawn(program, args, { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
17
17
|
let output = "";
|
|
18
18
|
let error = "";
|
|
19
19
|
child.stdout.on("data", (chunk ) => (output += chunk.toString("utf8")));
|
|
20
20
|
child.stderr.on("data", (chunk ) => (error += chunk.toString("utf8")));
|
|
21
|
-
|
|
22
|
-
child.on("
|
|
21
|
+
// Spawning reports ENOENT when the program isn't on the PATH.
|
|
22
|
+
child.on("error", (failure ) =>
|
|
23
|
+
resolve(failure.code === "ENOENT" ? { kind: "missing" } : { kind: "finished", exitCode: 127, output, error: `${error}${failure.message}` }),
|
|
24
|
+
);
|
|
25
|
+
child.on("close", (code) => resolve({ kind: "finished", exitCode: code ?? 1, output, error }));
|
|
26
|
+
// A program that never started closes stdin; ignore the write error, since the spawn error reports it.
|
|
27
|
+
child.stdin.on("error", () => {});
|
|
23
28
|
child.stdin.end(prompt);
|
|
24
29
|
});
|
|
25
30
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
|
+
import { agentSkillHome } from "../../domain/agent_skill_homes.js";
|
|
1
3
|
import { parseProjectName } from "../../domain/project_name.js";
|
|
2
|
-
import { INIT_SCAFFOLD,
|
|
4
|
+
import { CLAUDE_INSTRUCTIONS, INIT_AGENTS, INIT_SCAFFOLD, renderTemplate, reviewSettingTemplate } from "../../domain/scaffold.js";
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
|
|
@@ -10,6 +12,7 @@ import { installSkills, } from "./install_skills.js";
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
|
|
@@ -20,6 +23,7 @@ import { installSkills, } from "./install_skills.js";
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
|
|
@@ -36,11 +40,13 @@ export async function initProject(
|
|
|
36
40
|
) {
|
|
37
41
|
const { files, bundle, prompter } = deps;
|
|
38
42
|
const project = parseProjectName(request.name ?? (await prompter.ask("Project name", request.defaultName)));
|
|
43
|
+
const agent = await chooseAgent(files, prompter, request.agents);
|
|
39
44
|
const values = {
|
|
40
45
|
project_name: project,
|
|
41
46
|
project_name_quoted: JSON.stringify(project),
|
|
42
47
|
cruze_version: bundle.version,
|
|
43
48
|
cruze_package: request.package ?? `@printpractical/cruze@${bundle.version}`,
|
|
49
|
+
review_setting: (await bundle.template(reviewSettingTemplate(agent))).trimEnd(),
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
const created = [];
|
|
@@ -53,7 +59,29 @@ export async function initProject(
|
|
|
53
59
|
await files.writeText(entry.path, renderTemplate(await bundle.template(entry.template), values));
|
|
54
60
|
created.push(entry.path);
|
|
55
61
|
}
|
|
62
|
+
if (agent === "claude") {
|
|
63
|
+
if (await files.exists(CLAUDE_INSTRUCTIONS.path)) skipped.push(CLAUDE_INSTRUCTIONS.path);
|
|
64
|
+
else {
|
|
65
|
+
await files.link(CLAUDE_INSTRUCTIONS.path, CLAUDE_INSTRUCTIONS.target);
|
|
66
|
+
created.push(CLAUDE_INSTRUCTIONS.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
|
|
57
|
-
const
|
|
58
|
-
|
|
70
|
+
const linkFor = new Set(request.agents.filter((a) => a !== "other"));
|
|
71
|
+
if (agent === "claude") linkFor.add("claude");
|
|
72
|
+
const skills = await installSkills({ files, bundle }, { agents: [...linkFor] });
|
|
73
|
+
return { project, agent, created, skipped, skills };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The agent named with `--agent`, or shown by the project's files, or else the answer to asking. */
|
|
77
|
+
async function chooseAgent(files , prompter , requested ) {
|
|
78
|
+
if (requested.includes("claude")) return "claude";
|
|
79
|
+
if (requested.includes("other")) return "other";
|
|
80
|
+
for (const marker of agentSkillHome("claude")?.markers ?? []) {
|
|
81
|
+
if (await files.exists(marker)) return "claude";
|
|
82
|
+
}
|
|
83
|
+
const answer = (await prompter.ask(`Coding agent (${INIT_AGENTS.join(" or ")})`, "claude")).trim().toLowerCase();
|
|
84
|
+
const agent = INIT_AGENTS.find((a) => a === answer);
|
|
85
|
+
if (agent === undefined) throw new CruzeError("unknown-agent", `Unknown agent: ${answer}. Answer ${INIT_AGENTS.join(" or ")}, or pass --agent.`);
|
|
86
|
+
return agent;
|
|
59
87
|
}
|
|
@@ -18,16 +18,19 @@ export const ROLES = ["design-reviewer", "code-reviewer", "verifier", "researche
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The role's report, or, when the configured agent isn't installed, the prompt for the calling
|
|
23
|
+
* agent to run in a helper of its own.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Runs a Cruze role in a fresh agent context: the role's prompt, what it reviews, and nothing
|
|
30
|
-
* from the conversation that produced the work. Returns the role's report as written
|
|
32
|
+
* from the conversation that produced the work. Returns the role's report as written, or the
|
|
33
|
+
* prompt to run in a helper when the configured agent isn't installed.
|
|
31
34
|
*/
|
|
32
35
|
export async function runReview(deps , request ) {
|
|
33
36
|
if (!(ROLES ).includes(request.role)) throw new CruzeError("unknown-role", `the roles are ${ROLES.join(", ")}`);
|
|
@@ -55,12 +58,23 @@ export async function runReview(deps , req
|
|
|
55
58
|
context.map((line) => `- ${line}`).join("\n"),
|
|
56
59
|
].join("\n\n");
|
|
57
60
|
|
|
58
|
-
const
|
|
61
|
+
const subject = { role, ...(request.item === undefined ? {} : { item: request.item }) };
|
|
62
|
+
const command = view.config?.config?.review.command;
|
|
63
|
+
if (command === undefined) return { ...subject, status: "run-in-helper", instruction: helperInstruction(undefined), prompt };
|
|
59
64
|
const run = await deps.runner.run(command, prompt);
|
|
65
|
+
if (run.kind === "missing") return { ...subject, command, status: "run-in-helper", instruction: helperInstruction(command[0]), prompt };
|
|
60
66
|
if (run.exitCode !== 0) throw new CruzeError("review-failed", `${command.join(" ")} exited with ${run.exitCode}: ${run.error.trim() || run.output.trim()}`);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
return { ...subject, command, status: "reported", report: run.output.trim() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Why cruze review can't start the fresh context itself, and how the calling agent does instead. */
|
|
71
|
+
function helperInstruction(program ) {
|
|
72
|
+
return [
|
|
73
|
+
program === undefined ? "No review agent is configured under review.command." : `${program} is not installed, so cruze review can't start a fresh context.`,
|
|
74
|
+
"Start a helper agent with no conversation history, give it this prompt and nothing else, and take its report as the role's report.",
|
|
75
|
+
"If you can't start a helper, tell the user, and have them run the prompt in a new session.",
|
|
76
|
+
"To have cruze review start an agent itself, set review.command in .cruze/config.yaml.",
|
|
77
|
+
].join(" ");
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
async function bundledRole(deps , role ) {
|
package/dist/domain/config.js
CHANGED
|
@@ -19,12 +19,12 @@ import { parse as parseYaml } from "yaml";
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
/** Claude Code in print mode, allowed to read the project, its history and run `cruze`, but not to edit. */
|
|
27
|
-
export const DEFAULT_REVIEW_COMMAND = ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"];
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
|
|
@@ -95,8 +95,8 @@ export function parseConfig(text ) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
const review = asRecord(root["review"]) ?? {};
|
|
98
|
-
const reviewCommand = review["command"] === undefined ?
|
|
99
|
-
if (reviewCommand
|
|
98
|
+
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
99
|
+
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
100
100
|
|
|
101
101
|
const config = {
|
|
102
102
|
version: 1,
|
|
@@ -109,7 +109,7 @@ export function parseConfig(text ) {
|
|
|
109
109
|
check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
|
|
110
110
|
layers,
|
|
111
111
|
commands,
|
|
112
|
-
review:
|
|
112
|
+
review: reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand },
|
|
113
113
|
};
|
|
114
114
|
return { config, problems };
|
|
115
115
|
}
|
package/dist/domain/scaffold.js
CHANGED
|
@@ -11,12 +11,26 @@ export const INIT_SCAFFOLD = [
|
|
|
11
11
|
{ path: "README.md", template: "init/README.md" },
|
|
12
12
|
{ path: "CHANGELOG.md", template: "init/CHANGELOG.md" },
|
|
13
13
|
{ path: "AGENTS.md", template: "init/AGENTS.md" },
|
|
14
|
-
{ path: "CLAUDE.md", template: "init/CLAUDE.md" },
|
|
15
14
|
{ path: ".cruze/config.yaml", template: "init/config.yaml" },
|
|
16
15
|
{ path: ".github/workflows/ci.yml", template: "init/ci.yml" },
|
|
17
16
|
{ path: ".gitattributes", template: "init/gitattributes" },
|
|
18
17
|
];
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The agents `cruze init` sets a project up for: Claude Code, which gets `CLAUDE.md`, linked skills
|
|
21
|
+
* and a review command, or any other agent, which reads `AGENTS.md` and `.agents/skills/`.
|
|
22
|
+
*/
|
|
23
|
+
export const INIT_AGENTS = ["claude", "other"] ;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/** Claude Code reads `CLAUDE.md`, so init links it to `AGENTS.md` rather than keeping a second copy. */
|
|
27
|
+
export const CLAUDE_INSTRUCTIONS = { path: "CLAUDE.md", target: "AGENTS.md" };
|
|
28
|
+
|
|
29
|
+
/** The template for the config's `review:` setting, which only Claude Code gets filled in. */
|
|
30
|
+
export function reviewSettingTemplate(agent ) {
|
|
31
|
+
return `init/review/${agent}.yaml`;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/** Replaces every `{{key}}` with its value; an unknown key is a template bug. */
|
|
21
35
|
export function renderTemplate(template , values ) {
|
|
22
36
|
return template.replace(/\{\{([a-z_]+)\}\}/g, (placeholder, key ) => {
|
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ layers:
|
|
|
51
51
|
| `check.exceptions` | Files allowed past a budget, or Rust test files allowed under `src/` (`test-placement`), each with a `path` and a `reason`. `realign` adds entries whose reason ends `scheduled in <ref>`, and landing that change removes them. |
|
|
52
52
|
| `layers` | Each layer has a `name`, the `paths` it covers, and `may_import`, the layers it may depend on. |
|
|
53
53
|
| `commands` | The project's commands by name, such as `test` or `run`. The walking-skeleton change fills them in, and `cruze land` copies them into the Commands section of `AGENTS.md`. |
|
|
54
|
-
| `review.command` | The program and arguments `cruze review` runs to start an agent in a fresh context, with the prompt on standard input.
|
|
54
|
+
| `review.command` | The program and arguments `cruze review` runs to start an agent in a fresh context, with the prompt on standard input. Optional. `cruze init` sets it for Claude Code to run in print mode, allowed to read the project and run `git diff`, `git log`, `git show` and `cruze`. The verifier runs the system, so add the project's build and run commands. When it is unset, or its program isn't installed, `cruze review` hands the role's prompt back to the calling agent to run in a helper. |
|
|
55
55
|
|
|
56
56
|
Rules for layers:
|
|
57
57
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Realign notes: 0.0.3
|
|
2
|
+
|
|
3
|
+
## New work only
|
|
4
|
+
|
|
5
|
+
### review-helper-fallback: A review runs in a helper when no review agent is configured or installed
|
|
6
|
+
- Applies to: new work only
|
|
7
|
+
- Rule: `.agents/skills/cruze-roles/SKILL.md`, "Running a role", step 3
|
|
8
|
+
|
|
9
|
+
### optional-review-command: review.command is optional, and set only for Claude Code
|
|
10
|
+
- Applies to: new work only
|
|
11
|
+
- Rule: `.agents/skills/cruze-formats/reference/config.md`, "Configuration format"
|
package/skills/realign/notes.md
CHANGED
package/skills/roles/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ A role is a prompt with fixed inputs and a report as its only output. It runs in
|
|
|
19
19
|
|
|
20
20
|
1. Read the role file and gather exactly the inputs it lists. Done when you have every input path, plus any command output the role asks for.
|
|
21
21
|
2. Start a fresh context: a helper agent with no conversation history, where the agent can start one. Give it the role file's full text and the inputs the role lists, including draft documents it names, and nothing else. Your own opinions, the conversation and your reasons for the design stay out.
|
|
22
|
-
3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier`, `researcher` or `auditor`. Add `--base <commit>` for the code under review. It starts the agent configured under `review:` in `.cruze/config.yaml` in a fresh context and prints the role's report.
|
|
22
|
+
3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier`, `researcher` or `auditor`. Add `--base <commit>` for the code under review. It starts the agent configured under `review:` in `.cruze/config.yaml` in a fresh context and prints the role's report. When no agent is configured, or it isn't installed, it reports `status: run-in-helper` with the role's full prompt instead: start a helper, such as a subagent, and give it exactly that prompt. If no fresh context can run, the reviewers, the verifier and the auditor must still run fresh: tell the user, and have them run the role in a new session. The researcher may run in your own context, since its evidence is checkable; say that you did.
|
|
23
23
|
4. Take the report as it comes back. Check its evidence against the files before you act on any finding.
|
|
24
24
|
|
|
25
25
|
## Running a review
|
|
@@ -14,5 +14,5 @@ export async function runReviewCommand(context: CliContext, args: string[], opti
|
|
|
14
14
|
...(blockers === undefined ? {} : { blockers }),
|
|
15
15
|
...(options.base === undefined ? {} : { base: options.base }),
|
|
16
16
|
});
|
|
17
|
-
return { json: report, human: report.report };
|
|
17
|
+
return { json: report, human: report.status === "reported" ? report.report : `${report.instruction}\n\n${report.prompt}` };
|
|
18
18
|
}
|
|
@@ -10,7 +10,7 @@ import { runReviewCommand } from "./commands/review.ts";
|
|
|
10
10
|
import { runNext, runStatus } from "./commands/status.ts";
|
|
11
11
|
|
|
12
12
|
export const COMMANDS: Record<string, { handler: Handler; usage: string }> = {
|
|
13
|
-
init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent
|
|
13
|
+
init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent claude|other] [--package <npm spec>] set up this repository and install the skills" },
|
|
14
14
|
install: { handler: runInstall, usage: "install [--agent <agent>] install or update the Cruze skills" },
|
|
15
15
|
validate: { handler: runValidate, usage: "validate check every document against the formats" },
|
|
16
16
|
status: { handler: runStatus, usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
@@ -78,7 +78,8 @@ export class NodeProjectFiles implements ProjectFiles {
|
|
|
78
78
|
this.assertInsideRoot(resolve(dirname(linkPath), target));
|
|
79
79
|
await mkdir(dirname(linkPath), { recursive: true });
|
|
80
80
|
await rm(linkPath, { recursive: true, force: true });
|
|
81
|
-
|
|
81
|
+
// No link type: on Windows, Node picks a file or directory link from what the target is.
|
|
82
|
+
await symlink(target, linkPath);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** Resolves a project-relative path, refusing anything outside the root. */
|
|
@@ -11,15 +11,20 @@ export class ProcessAgentRunner implements AgentRunner {
|
|
|
11
11
|
|
|
12
12
|
run(command: string[], prompt: string): Promise<AgentRun> {
|
|
13
13
|
const [program, ...args] = command;
|
|
14
|
-
if (program === undefined) return Promise.resolve({
|
|
14
|
+
if (program === undefined) return Promise.resolve({ kind: "missing" });
|
|
15
15
|
return new Promise((resolve) => {
|
|
16
16
|
const child = spawn(program, args, { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
17
17
|
let output = "";
|
|
18
18
|
let error = "";
|
|
19
19
|
child.stdout.on("data", (chunk: Buffer) => (output += chunk.toString("utf8")));
|
|
20
20
|
child.stderr.on("data", (chunk: Buffer) => (error += chunk.toString("utf8")));
|
|
21
|
-
|
|
22
|
-
child.on("
|
|
21
|
+
// Spawning reports ENOENT when the program isn't on the PATH.
|
|
22
|
+
child.on("error", (failure: NodeJS.ErrnoException) =>
|
|
23
|
+
resolve(failure.code === "ENOENT" ? { kind: "missing" } : { kind: "finished", exitCode: 127, output, error: `${error}${failure.message}` }),
|
|
24
|
+
);
|
|
25
|
+
child.on("close", (code) => resolve({ kind: "finished", exitCode: code ?? 1, output, error }));
|
|
26
|
+
// A program that never started closes stdin; ignore the write error, since the spawn error reports it.
|
|
27
|
+
child.stdin.on("error", () => {});
|
|
23
28
|
child.stdin.end(prompt);
|
|
24
29
|
});
|
|
25
30
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/** Starts a coding agent in a fresh context, non-interactively, and collects what it reports. */
|
|
2
2
|
export interface AgentRunner {
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Runs `command` in the project root with `prompt` on standard input. Reports `missing`,
|
|
5
|
+
* without running anything, when the command's program isn't installed.
|
|
6
|
+
*/
|
|
4
7
|
run(command: string[], prompt: string): Promise<AgentRun>;
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
export
|
|
8
|
-
exitCode: number;
|
|
9
|
-
output: string;
|
|
10
|
-
error: string;
|
|
11
|
-
}
|
|
10
|
+
export type AgentRun = { kind: "finished"; exitCode: number; output: string; error: string } | { kind: "missing" };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { CruzeError } from "../../domain/cruze_error.ts";
|
|
2
|
+
import { agentSkillHome } from "../../domain/agent_skill_homes.ts";
|
|
1
3
|
import { parseProjectName } from "../../domain/project_name.ts";
|
|
2
|
-
import { INIT_SCAFFOLD, renderTemplate } from "../../domain/scaffold.ts";
|
|
4
|
+
import { CLAUDE_INSTRUCTIONS, INIT_AGENTS, INIT_SCAFFOLD, type InitAgent, renderTemplate, reviewSettingTemplate } from "../../domain/scaffold.ts";
|
|
3
5
|
import type { Bundle } from "../ports/bundle.ts";
|
|
4
6
|
import type { ProjectFiles } from "../ports/project_files.ts";
|
|
5
7
|
import type { Prompter } from "../ports/prompter.ts";
|
|
@@ -10,6 +12,7 @@ export interface InitProjectRequest {
|
|
|
10
12
|
name?: string;
|
|
11
13
|
/** Offered as the default answer when asking for the name. */
|
|
12
14
|
defaultName: string;
|
|
15
|
+
/** From `--agent`: `claude`, `other`, or agents to link skills for. Asked for when it names neither. */
|
|
13
16
|
agents: string[];
|
|
14
17
|
/**
|
|
15
18
|
* How CI runs this version of Cruze through npx, such as `github:PrintPractical/cruze#v0.0.1`
|
|
@@ -20,6 +23,7 @@ export interface InitProjectRequest {
|
|
|
20
23
|
|
|
21
24
|
export interface InitProjectReport {
|
|
22
25
|
project: string;
|
|
26
|
+
agent: InitAgent;
|
|
23
27
|
created: string[];
|
|
24
28
|
/** Files that already existed and were left untouched. */
|
|
25
29
|
skipped: string[];
|
|
@@ -36,11 +40,13 @@ export async function initProject(
|
|
|
36
40
|
): Promise<InitProjectReport> {
|
|
37
41
|
const { files, bundle, prompter } = deps;
|
|
38
42
|
const project = parseProjectName(request.name ?? (await prompter.ask("Project name", request.defaultName)));
|
|
43
|
+
const agent = await chooseAgent(files, prompter, request.agents);
|
|
39
44
|
const values = {
|
|
40
45
|
project_name: project,
|
|
41
46
|
project_name_quoted: JSON.stringify(project),
|
|
42
47
|
cruze_version: bundle.version,
|
|
43
48
|
cruze_package: request.package ?? `@printpractical/cruze@${bundle.version}`,
|
|
49
|
+
review_setting: (await bundle.template(reviewSettingTemplate(agent))).trimEnd(),
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
const created: string[] = [];
|
|
@@ -53,7 +59,29 @@ export async function initProject(
|
|
|
53
59
|
await files.writeText(entry.path, renderTemplate(await bundle.template(entry.template), values));
|
|
54
60
|
created.push(entry.path);
|
|
55
61
|
}
|
|
62
|
+
if (agent === "claude") {
|
|
63
|
+
if (await files.exists(CLAUDE_INSTRUCTIONS.path)) skipped.push(CLAUDE_INSTRUCTIONS.path);
|
|
64
|
+
else {
|
|
65
|
+
await files.link(CLAUDE_INSTRUCTIONS.path, CLAUDE_INSTRUCTIONS.target);
|
|
66
|
+
created.push(CLAUDE_INSTRUCTIONS.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
|
|
57
|
-
const
|
|
58
|
-
|
|
70
|
+
const linkFor = new Set(request.agents.filter((a) => a !== "other"));
|
|
71
|
+
if (agent === "claude") linkFor.add("claude");
|
|
72
|
+
const skills = await installSkills({ files, bundle }, { agents: [...linkFor] });
|
|
73
|
+
return { project, agent, created, skipped, skills };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The agent named with `--agent`, or shown by the project's files, or else the answer to asking. */
|
|
77
|
+
async function chooseAgent(files: ProjectFiles, prompter: Prompter, requested: string[]): Promise<InitAgent> {
|
|
78
|
+
if (requested.includes("claude")) return "claude";
|
|
79
|
+
if (requested.includes("other")) return "other";
|
|
80
|
+
for (const marker of agentSkillHome("claude")?.markers ?? []) {
|
|
81
|
+
if (await files.exists(marker)) return "claude";
|
|
82
|
+
}
|
|
83
|
+
const answer = (await prompter.ask(`Coding agent (${INIT_AGENTS.join(" or ")})`, "claude")).trim().toLowerCase();
|
|
84
|
+
const agent = INIT_AGENTS.find((a) => a === answer);
|
|
85
|
+
if (agent === undefined) throw new CruzeError("unknown-agent", `Unknown agent: ${answer}. Answer ${INIT_AGENTS.join(" or ")}, or pass --agent.`);
|
|
86
|
+
return agent;
|
|
59
87
|
}
|
|
@@ -18,16 +18,19 @@ export interface ReviewRequest {
|
|
|
18
18
|
base?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
21
|
+
/**
|
|
22
|
+
* The role's report, or, when the configured agent isn't installed, the prompt for the calling
|
|
23
|
+
* agent to run in a helper of its own.
|
|
24
|
+
*/
|
|
25
|
+
export type ReviewReport = { role: Role; item?: string; command?: string[] } & (
|
|
26
|
+
| { status: "reported"; report: string }
|
|
27
|
+
| { status: "run-in-helper"; instruction: string; prompt: string }
|
|
28
|
+
);
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Runs a Cruze role in a fresh agent context: the role's prompt, what it reviews, and nothing
|
|
30
|
-
* from the conversation that produced the work. Returns the role's report as written
|
|
32
|
+
* from the conversation that produced the work. Returns the role's report as written, or the
|
|
33
|
+
* prompt to run in a helper when the configured agent isn't installed.
|
|
31
34
|
*/
|
|
32
35
|
export async function runReview(deps: ProjectDeps & { runner: AgentRunner }, request: ReviewRequest): Promise<ReviewReport> {
|
|
33
36
|
if (!(ROLES as readonly string[]).includes(request.role)) throw new CruzeError("unknown-role", `the roles are ${ROLES.join(", ")}`);
|
|
@@ -55,12 +58,23 @@ export async function runReview(deps: ProjectDeps & { runner: AgentRunner }, req
|
|
|
55
58
|
context.map((line) => `- ${line}`).join("\n"),
|
|
56
59
|
].join("\n\n");
|
|
57
60
|
|
|
58
|
-
const
|
|
61
|
+
const subject = { role, ...(request.item === undefined ? {} : { item: request.item }) };
|
|
62
|
+
const command = view.config?.config?.review.command;
|
|
63
|
+
if (command === undefined) return { ...subject, status: "run-in-helper", instruction: helperInstruction(undefined), prompt };
|
|
59
64
|
const run = await deps.runner.run(command, prompt);
|
|
65
|
+
if (run.kind === "missing") return { ...subject, command, status: "run-in-helper", instruction: helperInstruction(command[0]), prompt };
|
|
60
66
|
if (run.exitCode !== 0) throw new CruzeError("review-failed", `${command.join(" ")} exited with ${run.exitCode}: ${run.error.trim() || run.output.trim()}`);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
return { ...subject, command, status: "reported", report: run.output.trim() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Why cruze review can't start the fresh context itself, and how the calling agent does instead. */
|
|
71
|
+
function helperInstruction(program: string | undefined): string {
|
|
72
|
+
return [
|
|
73
|
+
program === undefined ? "No review agent is configured under review.command." : `${program} is not installed, so cruze review can't start a fresh context.`,
|
|
74
|
+
"Start a helper agent with no conversation history, give it this prompt and nothing else, and take its report as the role's report.",
|
|
75
|
+
"If you can't start a helper, tell the user, and have them run the prompt in a new session.",
|
|
76
|
+
"To have cruze review start an agent itself, set review.command in .cruze/config.yaml.",
|
|
77
|
+
].join(" ");
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
async function bundledRole(deps: ProjectDeps, role: Role): Promise<string> {
|
package/src/domain/config.ts
CHANGED
|
@@ -19,13 +19,13 @@ export interface CruzeConfig {
|
|
|
19
19
|
layers: Layer[];
|
|
20
20
|
/** Named project commands, such as `test: cargo test`, shown in AGENTS.md. */
|
|
21
21
|
commands: Record<string, string>;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How `cruze review` starts an agent in a fresh context: the program and its arguments.
|
|
24
|
+
* Without one, the calling agent runs the role's prompt in a helper of its own.
|
|
25
|
+
*/
|
|
26
|
+
review: { command?: string[] };
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
/** Claude Code in print mode, allowed to read the project, its history and run `cruze`, but not to edit. */
|
|
27
|
-
export const DEFAULT_REVIEW_COMMAND = ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"];
|
|
28
|
-
|
|
29
29
|
export interface Layer {
|
|
30
30
|
name: string;
|
|
31
31
|
paths: string[];
|
|
@@ -95,8 +95,8 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
const review = asRecord(root["review"]) ?? {};
|
|
98
|
-
const reviewCommand = review["command"] === undefined ?
|
|
99
|
-
if (reviewCommand
|
|
98
|
+
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
99
|
+
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
100
100
|
|
|
101
101
|
const config: CruzeConfig = {
|
|
102
102
|
version: 1,
|
|
@@ -109,7 +109,7 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
109
109
|
check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
|
|
110
110
|
layers,
|
|
111
111
|
commands,
|
|
112
|
-
review:
|
|
112
|
+
review: reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand },
|
|
113
113
|
};
|
|
114
114
|
return { config, problems };
|
|
115
115
|
}
|
package/src/domain/scaffold.ts
CHANGED
|
@@ -11,12 +11,26 @@ export const INIT_SCAFFOLD: readonly ScaffoldEntry[] = [
|
|
|
11
11
|
{ path: "README.md", template: "init/README.md" },
|
|
12
12
|
{ path: "CHANGELOG.md", template: "init/CHANGELOG.md" },
|
|
13
13
|
{ path: "AGENTS.md", template: "init/AGENTS.md" },
|
|
14
|
-
{ path: "CLAUDE.md", template: "init/CLAUDE.md" },
|
|
15
14
|
{ path: ".cruze/config.yaml", template: "init/config.yaml" },
|
|
16
15
|
{ path: ".github/workflows/ci.yml", template: "init/ci.yml" },
|
|
17
16
|
{ path: ".gitattributes", template: "init/gitattributes" },
|
|
18
17
|
];
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The agents `cruze init` sets a project up for: Claude Code, which gets `CLAUDE.md`, linked skills
|
|
21
|
+
* and a review command, or any other agent, which reads `AGENTS.md` and `.agents/skills/`.
|
|
22
|
+
*/
|
|
23
|
+
export const INIT_AGENTS = ["claude", "other"] as const;
|
|
24
|
+
export type InitAgent = (typeof INIT_AGENTS)[number];
|
|
25
|
+
|
|
26
|
+
/** Claude Code reads `CLAUDE.md`, so init links it to `AGENTS.md` rather than keeping a second copy. */
|
|
27
|
+
export const CLAUDE_INSTRUCTIONS = { path: "CLAUDE.md", target: "AGENTS.md" };
|
|
28
|
+
|
|
29
|
+
/** The template for the config's `review:` setting, which only Claude Code gets filled in. */
|
|
30
|
+
export function reviewSettingTemplate(agent: InitAgent): string {
|
|
31
|
+
return `init/review/${agent}.yaml`;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/** Replaces every `{{key}}` with its value; an unknown key is a template bug. */
|
|
21
35
|
export function renderTemplate(template: string, values: Record<string, string>): string {
|
|
22
36
|
return template.replace(/\{\{([a-z_]+)\}\}/g, (placeholder, key: string) => {
|
|
@@ -29,9 +29,4 @@ layers: []
|
|
|
29
29
|
# walking-skeleton change fills them in; `cruze land` copies them into AGENTS.md.
|
|
30
30
|
commands: {}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
# prompt on standard input. The default lets Claude Code read the project and run
|
|
34
|
-
# git and cruze. The verifier runs the system, so add the project's build and run
|
|
35
|
-
# commands, such as "Bash(cargo:*)". For Codex, use ["codex", "exec", "-"].
|
|
36
|
-
review:
|
|
37
|
-
command: ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"]
|
|
32
|
+
{{review_setting}}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# How `cruze review` starts an agent in a fresh context to run a role, with the
|
|
2
|
+
# prompt on standard input. This lets Claude Code read the project and run git
|
|
3
|
+
# and cruze. The verifier runs the system, so add the project's build and run
|
|
4
|
+
# commands, such as "Bash(cargo:*)". Without review.command, your agent runs each
|
|
5
|
+
# role in a helper of its own.
|
|
6
|
+
review:
|
|
7
|
+
command: ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*)"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# How `cruze review` starts an agent in a fresh context to run a role, with the
|
|
2
|
+
# prompt on standard input. Without it, your agent runs each role in a helper
|
|
3
|
+
# of its own. To have cruze review start one instead, set the command, such as
|
|
4
|
+
# ["codex", "exec", "-"] for Codex or ["opencode", "run", "--agent", "plan"] for
|
|
5
|
+
# OpenCode.
|
|
6
|
+
# review:
|
|
7
|
+
# command: ["codex", "exec", "-"]
|
package/templates/init/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@AGENTS.md
|