@printpractical/cruze 0.0.2 → 0.0.4
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 +11 -11
- package/dist/adapters/inbound/cli/argument_files.js +29 -0
- package/dist/adapters/inbound/cli/cli_context.js +1 -0
- package/dist/adapters/inbound/cli/command_options.js +20 -0
- package/dist/adapters/inbound/cli/commands/journal.js +3 -2
- package/dist/adapters/inbound/cli/commands/progress.js +7 -3
- package/dist/adapters/inbound/cli/commands/review.js +4 -3
- package/dist/adapters/inbound/cli/commands/status.js +3 -0
- package/dist/adapters/inbound/cli/commands.js +32 -20
- package/dist/adapters/inbound/cli/parse_command.js +8 -2
- package/dist/adapters/inbound/cli/run_cli.js +9 -5
- package/dist/adapters/outbound/node_project_files.js +4 -2
- 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/journal_events.js +12 -4
- package/dist/app/use_cases/project_status.js +8 -2
- package/dist/app/use_cases/record_progress.js +17 -0
- 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/dist/domain/status/work_status.js +1 -3
- package/package.json +1 -1
- package/skills/about/SKILL.md +1 -1
- package/skills/envision/SKILL.md +1 -1
- package/skills/envision/feature.md +1 -0
- package/skills/formats/reference/config.md +1 -1
- package/skills/formats/reference/durable-docs.md +1 -1
- package/skills/land/SKILL.md +1 -0
- package/skills/realign/notes/0.0.3.md +11 -0
- package/skills/realign/notes/0.0.4.md +27 -0
- package/skills/realign/notes.md +2 -0
- package/skills/rethink/levels.md +1 -1
- package/skills/roles/SKILL.md +1 -1
- package/src/adapters/inbound/cli/argument_files.ts +29 -0
- package/src/adapters/inbound/cli/cli_context.ts +1 -0
- package/src/adapters/inbound/cli/command_options.ts +20 -0
- package/src/adapters/inbound/cli/commands/journal.ts +3 -2
- package/src/adapters/inbound/cli/commands/progress.ts +7 -3
- package/src/adapters/inbound/cli/commands/review.ts +4 -3
- package/src/adapters/inbound/cli/commands/status.ts +3 -0
- package/src/adapters/inbound/cli/commands.ts +32 -20
- package/src/adapters/inbound/cli/parse_command.ts +8 -2
- package/src/adapters/inbound/cli/run_cli.ts +9 -5
- package/src/adapters/outbound/node_project_files.ts +4 -2
- 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/journal_events.ts +12 -4
- package/src/app/use_cases/project_status.ts +8 -2
- package/src/app/use_cases/record_progress.ts +17 -0
- 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/src/domain/status/work_status.ts +1 -3
- 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
|
@@ -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 ) => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { evaluateApproval, } from "../approvals/evaluate.js";
|
|
2
|
-
import { findIds } from "../ids.js";
|
|
3
2
|
import { PATHS } from "../project/layout.js";
|
|
4
3
|
import { readDelta } from "../project/deltas.js";
|
|
5
4
|
import { readScope } from "../project/plan_parts.js";
|
|
@@ -89,13 +88,12 @@ export function buildGateReasons(view , branch )
|
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
/** The elements a change touches: its scope
|
|
91
|
+
/** The elements a change touches: its scope and its delta, not the elements it only cites, such as a test seam. */
|
|
93
92
|
export function touchedIds(view , change ) {
|
|
94
93
|
const feature = featureOf(view.items, change);
|
|
95
94
|
const scope = readScope(change.doc);
|
|
96
95
|
const ids = new Set([...(scope?.delivers ?? []), ...(scope?.builds ?? []), ...(scope?.removes ?? [])]);
|
|
97
96
|
const deltaDoc = feature?.doc ?? change.doc;
|
|
98
97
|
for (const id of readDelta(deltaDoc).ids) ids.add(id);
|
|
99
|
-
for (const id of findIds(change.doc.lines.join("\n"))) ids.add(id);
|
|
100
98
|
return new Set([...ids].filter((id) => !id.startsWith("RULE-")));
|
|
101
99
|
}
|
package/package.json
CHANGED
package/skills/about/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Cruze is a spec-driven development framework. It keeps one living model of the s
|
|
|
9
9
|
|
|
10
10
|
This installation is an early preview. It has:
|
|
11
11
|
|
|
12
|
-
- The `cruze` CLI: `cruze init` sets up a repository, `cruze install` updates the skills after an upgrade, and `cruze help` lists the commands that validate, approve, check, trace and land work.
|
|
12
|
+
- The `cruze` CLI: `cruze init` sets up a repository, `cruze install` updates the skills after an upgrade, and `cruze help` lists the commands that validate, approve, check, trace and land work. Each prints its result on stdout, as JSON when stdout isn't a terminal, or as text with `--text`. A command refuses an option it doesn't take, so check `cruze help` when one is refused.
|
|
13
13
|
- `cruze-formats`, the format of every Cruze document.
|
|
14
14
|
- Knowledge skills holding the standards to design and code to: `cruze-hexagonal-design`, `cruze-behavioural-testing`, `cruze-grilling`, `cruze-domain-language`, `cruze-dependency-approval` and `cruze-research`.
|
|
15
15
|
- The workflow: `explore` (optional), `envision` for the what, `architect` for the how, `roadmap` for the order of work, then `plan`, `build`, `verify` and `land` for each change. `triage` handles bugs, `rethink` steps back from anywhere, and `next` says what to do now.
|
package/skills/envision/SKILL.md
CHANGED
|
@@ -21,7 +21,7 @@ Envision decides the what: the problem, who has it, and the behaviour that solve
|
|
|
21
21
|
- Requirements and scenarios follow `.agents/skills/cruze-formats/reference/specs.md`. Every scenario uses concrete values: real names, real inputs, exact output. The scenarios become the tests, so their values are the expected results.
|
|
22
22
|
- Keep implementation out: no modules, types, libraries or file formats unless the user names one as a constraint. When the user raises one, record it as a constraint or a settled decision and move on.
|
|
23
23
|
- The vision stays within one page, excluding the feature map.
|
|
24
|
-
- Only the CLI writes the feature map. Use `cruze features add` and `cruze features drop`.
|
|
24
|
+
- Only the CLI writes the feature map. Use `cruze features add`, `cruze features update` and `cruze features drop`.
|
|
25
25
|
|
|
26
26
|
## Next step
|
|
27
27
|
|
|
@@ -13,6 +13,7 @@ The result is a feature folder whose `feature.md` holds the intent, the settled
|
|
|
13
13
|
- **Requirements:** each behaviour the feature adds, changes or removes, and the capability it belongs to. It goes in an existing capability when the behaviour fits one; otherwise it starts a new capability.
|
|
14
14
|
- **Scenarios:** for each requirement, the main case, each edge and failure case a user will meet, and what they observe in each. Invent concrete edge cases to test the boundaries.
|
|
15
15
|
6. **Grill in rounds** until every open decision is settled. Record decisions under `## Settled decisions` and terms in the glossary as they settle.
|
|
16
|
+
- When a decision changes something the feature's summary on the feature map promises, such as a policy it names, correct the summary with `cruze features update <slug> --summary <text>`.
|
|
16
17
|
7. **Write the spec delta.** Use `ADDED` for new requirements, `MODIFIED` for a requirement whose text or scenarios change (writing its full new body with every scenario it keeps), `REMOVED` with a `Reason` and `Migration` for retired behaviour, and `ADDED CAPABILITY` for a new capability.
|
|
17
18
|
- Name every ID with glossary terms. An ID is never reused.
|
|
18
19
|
- Done when every requirement has a scenario for its main case and one for each failure a user can meet.
|
|
@@ -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
|
|
|
@@ -16,7 +16,7 @@ What the product is for. At most one page, excluding the feature map.
|
|
|
16
16
|
| `## Feature map` | Managed. `### Implemented` is a table of Feature, Release and Summary. `### Future` is a table of Feature, Summary and Goals |
|
|
17
17
|
|
|
18
18
|
- Feature-map entries are named by slug, such as `jump-hosts`. A future entry gets a folder ID only when its feature starts.
|
|
19
|
-
- Only the CLI writes the feature map. `cruze features add` puts an entry on the future list, `cruze features drop` removes one, and `land` moves a finished feature to implemented, tagged with the release in progress.
|
|
19
|
+
- Only the CLI writes the feature map. `cruze features add` puts an entry on the future list, `cruze features update` corrects an entry's summary on either list, `cruze features drop` removes one from the future list, and `land` moves a finished feature to implemented, tagged with the release in progress.
|
|
20
20
|
|
|
21
21
|
## docs/glossary.md
|
|
22
22
|
|
package/skills/land/SKILL.md
CHANGED
|
@@ -20,6 +20,7 @@ Land makes finished work part of the living model. `cruze land` merges the chang
|
|
|
20
20
|
- When they don't hold, stop and offer `rethink`.
|
|
21
21
|
4. **Run `cruze land`** and quote its report: what it merged, what is now built, and whether the work finished and was archived.
|
|
22
22
|
- A `merge-conflict` means someone changed an element this feature merged earlier. Stop and run `rethink`, which reconciles it.
|
|
23
|
+
- When the work finished, read the feature's row under `### Implemented` in `docs/vision.md` against its settled decisions and spec delta. When the summary no longer describes what landed, correct it with `cruze features update <slug> --summary <text>`.
|
|
23
24
|
- A `merge-invalid` means the merged docs, or the feature and changes being landed, wouldn't validate, such as a decision citing an ID the change removes. Nothing was written. Stop and run `rethink`.
|
|
24
25
|
5. **Update the changelog.** Under `## [Unreleased]` in `CHANGELOG.md`, add one line per user-visible behaviour the change delivers, in the user's words, under `### Added`, `### Changed` or `### Removed`. Name the behaviour, not the code, including any rule a user can now run into that no scenario covers yet.
|
|
25
26
|
6. **Update the README** when the change altered how someone installs, runs or uses the product. Run every command you add, to check it works.
|
|
@@ -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"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Realign notes: 0.0.4
|
|
2
|
+
|
|
3
|
+
## New work only
|
|
4
|
+
|
|
5
|
+
### argument-files-anywhere: Files named on the command line may be outside the project
|
|
6
|
+
- Applies to: new work only
|
|
7
|
+
- Rule: `.agents/skills/cruze-roles/SKILL.md`, "Running a role"
|
|
8
|
+
|
|
9
|
+
### cli-output-format: The CLI prints one format on stdout, JSON for agents and text with --text
|
|
10
|
+
- Applies to: new work only
|
|
11
|
+
- Rule: `.agents/skills/cruze-about/SKILL.md`
|
|
12
|
+
|
|
13
|
+
### overlap-scope: Overlap counts only in-flight changes, and only the elements their scope and delta name
|
|
14
|
+
- Applies to: new work only
|
|
15
|
+
- Rule: `.agents/skills/cruze-plan/SKILL.md`, "Steps", step 7
|
|
16
|
+
|
|
17
|
+
### refused-options: The CLI refuses an option its command doesn't take
|
|
18
|
+
- Applies to: new work only
|
|
19
|
+
- Rule: `.agents/skills/cruze-about/SKILL.md`
|
|
20
|
+
|
|
21
|
+
### feature-map-summary: A feature's summary on the feature map follows its decisions
|
|
22
|
+
- Applies to: new work only
|
|
23
|
+
- Rule: `.agents/skills/cruze-land/SKILL.md`, "Landing a change", step 4
|
|
24
|
+
|
|
25
|
+
### journal-list-item: Journal list narrows to one feature or change with --item
|
|
26
|
+
- Applies to: new work only
|
|
27
|
+
- Rule: `.agents/skills/cruze-about/SKILL.md`
|
package/skills/realign/notes.md
CHANGED
package/skills/rethink/levels.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
## Vision
|
|
32
32
|
|
|
33
|
-
1. Edit `docs/vision.md` with the user: goals, non-goals or constraints. The feature map changes only through `cruze features add` and `cruze features drop`. When the problem, users or goals changed, update the overview under the title of `README.md` to match.
|
|
33
|
+
1. Edit `docs/vision.md` with the user: goals, non-goals or constraints. The feature map changes only through `cruze features add`, `cruze features update` and `cruze features drop`. When the problem, users or goals changed, update the overview under the title of `README.md` to match.
|
|
34
34
|
2. Update `docs/roadmap.md` to match, following `.agents/skills/cruze-roadmap/SKILL.md`.
|
|
35
35
|
3. For each feature in progress that served a dropped goal, the user decides: keep it, or stop it with `cruze abandon <ref> --reason "<why>"`. Abandoning archives the folder and puts the feature back on the future list; changes that already landed stay.
|
|
36
36
|
4. Re-approve with `cruze approve vision`, then `cruze approve roadmap`.
|
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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { UsageError } from "./cli_context.ts";
|
|
4
|
+
|
|
5
|
+
// Files a person or agent names on the command line, such as a scratch file of review blockers or
|
|
6
|
+
// the path for an exported bundle. They may be anywhere, unlike project files, and relative paths
|
|
7
|
+
// resolve against the working directory.
|
|
8
|
+
|
|
9
|
+
export async function readArgumentFile(path: string): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
return await readFile(resolve(path), "utf8");
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new UsageError(`cannot read ${path}: ${reason(error)}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function writeArgumentFile(path: string, text: string): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(dirname(resolve(path)), { recursive: true });
|
|
20
|
+
await writeFile(resolve(path), text, "utf8");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new UsageError(`cannot write ${path}: ${reason(error)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function reason(error: unknown): string {
|
|
27
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { UsageError } from "./cli_context.ts";
|
|
2
|
+
import type { Command } from "./commands.ts";
|
|
3
|
+
|
|
4
|
+
/** Options every command takes. */
|
|
5
|
+
const GLOBAL_OPTIONS = ["json", "text", "help", "version"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Refuses an option the command doesn't take, rather than ignoring it, so a caller never gets
|
|
9
|
+
* wider output than it asked for. An unknown subcommand is left to the handler to report.
|
|
10
|
+
*/
|
|
11
|
+
export function refuseUnknownOptions(name: string, args: string[], command: Command, given: string[]): void {
|
|
12
|
+
const sub = Array.isArray(command.options) ? undefined : args[0];
|
|
13
|
+
const taken = Array.isArray(command.options) ? command.options : sub === undefined ? undefined : (command.options as Record<string, readonly string[]>)[sub];
|
|
14
|
+
if (taken === undefined) return;
|
|
15
|
+
const refused = given.filter((option) => !GLOBAL_OPTIONS.includes(option) && !taken.includes(option));
|
|
16
|
+
if (refused.length === 0) return;
|
|
17
|
+
const which = sub === undefined ? `cruze ${name}` : `cruze ${name} ${sub}`;
|
|
18
|
+
const takes = taken.length === 0 ? "it takes no options" : `it takes ${taken.map((o) => `--${o}`).join(", ")}`;
|
|
19
|
+
throw new UsageError(`${which} doesn't take ${refused.map((o) => `--${o}`).join(", ")}; ${takes}`);
|
|
20
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { exportFeedback, listEvents, recordEvent } from "../../../../app/use_cases/journal_events.ts";
|
|
2
|
+
import { writeArgumentFile } from "../argument_files.ts";
|
|
2
3
|
import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
|
|
3
4
|
|
|
4
5
|
export async function runJournal(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
|
|
@@ -14,7 +15,7 @@ export async function runJournal(context: CliContext, args: string[], options: O
|
|
|
14
15
|
return { json: entry, human: `Recorded ${entry.event}.` };
|
|
15
16
|
}
|
|
16
17
|
if (args[0] === "list") {
|
|
17
|
-
const entries = await listEvents(context, options.event);
|
|
18
|
+
const entries = await listEvents(context, options.event, options.item);
|
|
18
19
|
return { json: entries, human: entries.map((e) => `${e.at} ${e.event} ${JSON.stringify({ ...e, at: undefined, event: undefined, by: undefined })}`).join("\n") || "No entries." };
|
|
19
20
|
}
|
|
20
21
|
throw new UsageError(`cruze journal takes add or list, not "${args[0]}"`);
|
|
@@ -24,7 +25,7 @@ export async function runFeedback(context: CliContext, args: string[], options:
|
|
|
24
25
|
if (args[0] !== "export") throw new UsageError("usage: cruze feedback export [--out <file>] [--no-redact]");
|
|
25
26
|
const bundle = await exportFeedback(context, options.redact);
|
|
26
27
|
if (options.out !== undefined) {
|
|
27
|
-
await
|
|
28
|
+
await writeArgumentFile(options.out, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
28
29
|
return { json: { out: options.out, entries: bundle.entries.length }, human: `Wrote ${bundle.entries.length} entries to ${options.out}.` };
|
|
29
30
|
}
|
|
30
31
|
return { json: bundle, human: `${bundle.entries.length} journal entries${bundle.redacted ? ", redacted" : ""}.` };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addFutureFeature, completeTask, dropFutureFeature, pruneRoadmap, recordDeviation, reopenTask } from "../../../../app/use_cases/record_progress.ts";
|
|
1
|
+
import { addFutureFeature, completeTask, dropFutureFeature, updateFeatureSummary, pruneRoadmap, recordDeviation, reopenTask } from "../../../../app/use_cases/record_progress.ts";
|
|
2
2
|
import { abandonWork } from "../../../../app/use_cases/abandon_work.ts";
|
|
3
3
|
import { UsageError, requireArgs, requireOption, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
|
|
4
4
|
|
|
@@ -23,17 +23,21 @@ export async function runTask(context: CliContext, args: string[], options: Opti
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function runFeatures(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
|
|
26
|
-
requireArgs(args, ["add|drop", "slug"]);
|
|
26
|
+
requireArgs(args, ["add|update|drop", "slug"]);
|
|
27
27
|
if (args[0] === "add") {
|
|
28
28
|
const goals = (options.goals ?? "").split(",").map((g) => g.trim()).filter((g) => g !== "");
|
|
29
29
|
const report = await addFutureFeature(context, args[1] ?? "", requireOption(options.summary, "--summary"), goals);
|
|
30
30
|
return { json: report, human: `Added ${report.feature} to the future list.` };
|
|
31
31
|
}
|
|
32
|
+
if (args[0] === "update") {
|
|
33
|
+
const report = await updateFeatureSummary(context, args[1] ?? "", requireOption(options.summary, "--summary"));
|
|
34
|
+
return { json: report, human: `Updated the summary of ${report.feature} on the ${report.list === "Future" ? "future" : "implemented"} list.` };
|
|
35
|
+
}
|
|
32
36
|
if (args[0] === "drop") {
|
|
33
37
|
const report = await dropFutureFeature(context, args[1] ?? "", requireOption(options.reason, "--reason"));
|
|
34
38
|
return { json: report, human: `Dropped ${report.feature} from the future list.` };
|
|
35
39
|
}
|
|
36
|
-
throw new UsageError(`cruze features takes add or drop, not "${args[0]}"`);
|
|
40
|
+
throw new UsageError(`cruze features takes add, update or drop, not "${args[0]}"`);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export async function runRoadmap(context: CliContext, args: string[]): Promise<CommandResult> {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { runReview } from "../../../../app/use_cases/run_review.ts";
|
|
2
|
+
import { readArgumentFile } from "../argument_files.ts";
|
|
2
3
|
import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
|
|
3
4
|
|
|
4
5
|
export async function runReviewCommand(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
|
|
5
6
|
requireArgs(args, ["role"]);
|
|
6
7
|
const round = options.round === undefined ? undefined : Number(options.round);
|
|
7
8
|
if (round !== undefined && round !== 1 && round !== 2) throw new UsageError("--round is 1 or 2");
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
if (options.blockers !== undefined && round !== 2) throw new UsageError("--blockers goes with --round 2");
|
|
10
|
+
const blockers = options.blockers === undefined ? undefined : await readArgumentFile(options.blockers);
|
|
10
11
|
const report = await runReview(context, {
|
|
11
12
|
role: args[0] ?? "",
|
|
12
13
|
...(options.item === undefined ? {} : { item: options.item }),
|
|
@@ -14,5 +15,5 @@ export async function runReviewCommand(context: CliContext, args: string[], opti
|
|
|
14
15
|
...(blockers === undefined ? {} : { blockers }),
|
|
15
16
|
...(options.base === undefined ? {} : { base: options.base }),
|
|
16
17
|
});
|
|
17
|
-
return { json: report, human: report.report };
|
|
18
|
+
return { json: report, human: report.status === "reported" ? report.report : `${report.instruction}\n\n${report.prompt}` };
|
|
18
19
|
}
|
|
@@ -3,6 +3,9 @@ import type { ApprovalStatus } from "../../../../domain/approvals/evaluate.ts";
|
|
|
3
3
|
import { UsageError, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
|
|
4
4
|
|
|
5
5
|
export async function runStatus(context: CliContext, _args: string[], options: Options): Promise<CommandResult> {
|
|
6
|
+
if (options.override !== undefined && options.gate === undefined) throw new UsageError("--override goes with --gate build");
|
|
7
|
+
if (options.change !== undefined && !options.overlap) throw new UsageError("--change goes with --overlap");
|
|
8
|
+
if (options.gate !== undefined && options.overlap) throw new UsageError("--gate and --overlap are alternatives");
|
|
6
9
|
if (options.gate !== undefined) {
|
|
7
10
|
if (options.gate !== "build") throw new UsageError(`unknown gate "${options.gate}"; the only gate is build`);
|
|
8
11
|
const report = await checkBuildGate(context, options.override);
|
|
@@ -9,25 +9,36 @@ import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progres
|
|
|
9
9
|
import { runReviewCommand } from "./commands/review.ts";
|
|
10
10
|
import { runNext, runStatus } from "./commands/status.ts";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
12
|
+
/** A command, and the options it takes: one list, or one per subcommand for a command that has them. */
|
|
13
|
+
export interface Command {
|
|
14
|
+
handler: Handler;
|
|
15
|
+
usage: string;
|
|
16
|
+
options: readonly string[] | Readonly<Record<string, readonly string[]>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const COMMANDS: Record<string, Command> = {
|
|
20
|
+
init: { handler: runInit, options: ["name", "yes", "agent", "package"], usage: "init [--name <name>] [--yes] [--agent claude|other] [--package <npm spec>] set up this repository and install the skills" },
|
|
21
|
+
install: { handler: runInstall, options: ["agent"], usage: "install [--agent <agent>] install or update the Cruze skills" },
|
|
22
|
+
validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
|
|
23
|
+
status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
24
|
+
next: { handler: runNext, options: [], usage: "next the step to run next on this branch, and why" },
|
|
25
|
+
approve: { handler: runApprove, options: ["replan", "rebase"], usage: "approve <vision|architecture|roadmap|ref|path> [--replan <ID>...] [--rebase <ID>...] record your approval of a document" },
|
|
26
|
+
new: {
|
|
27
|
+
handler: runNew,
|
|
28
|
+
options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title"] },
|
|
29
|
+
usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr|note> <slug> --title <title> [--feature <ref>] [--roadmap <slug>]",
|
|
30
|
+
},
|
|
31
|
+
task: { handler: runTask, options: { done: ["change", "commit"], deviation: ["change"], reopen: ["change"] }, usage: "task <done|deviation|reopen> <T#> [text] [--change <ref>] [--commit <sha>]" },
|
|
32
|
+
features: { handler: runFeatures, options: { add: ["summary", "goals"], update: ["summary"], drop: ["reason"] }, usage: "features <add|update|drop> <slug> [--summary <text>] [--goals <ids>] [--reason <text>]" },
|
|
33
|
+
roadmap: { handler: runRoadmap, options: { prune: [] }, usage: "roadmap prune clear landed items from the roadmap status when a release closes" },
|
|
34
|
+
journal: { handler: runJournal, options: { add: ["set", "item"], list: ["event", "item"] }, usage: "journal <add <event> --set key=value... [--item <ref>]|list [--event <event>] [--item <ref>]>" },
|
|
35
|
+
review: { handler: runReviewCommand, options: ["item", "round", "blockers", "base"], usage: "review <role> [--item <ref>] [--round 1|2 --blockers <file>] [--base <commit>] run a role in a fresh agent context" },
|
|
36
|
+
feedback: { handler: runFeedback, options: { export: ["out", "no-redact"] }, usage: "feedback export [--out <file>] [--no-redact]" },
|
|
37
|
+
trace: { handler: runTrace, options: ["all", "change"], usage: "trace [--all] [--change <ref>] link scenarios to the tests that prove them" },
|
|
38
|
+
check: { handler: runCheck, options: ["file", "ci"], usage: "check [<file>...] [--ci] enforce layer rules and file budgets" },
|
|
39
|
+
abandon: { handler: runAbandon, options: ["reason"], usage: "abandon <ref> --reason <text> archive a feature or standalone change that stops for good" },
|
|
40
|
+
land: { handler: runLand, options: ["override"], usage: "land [<ref>] [--override <reason>] merge a verified change into the living docs" },
|
|
41
|
+
realign: { handler: runRealign, options: { status: ["full"], done: ["change"] }, usage: "realign <status [--full]|done [--change <ref>]> what the installed Cruze expects of existing code, and recording that the code meets it" },
|
|
31
42
|
};
|
|
32
43
|
|
|
33
44
|
export const USAGE = `Usage: cruze <command> [options]
|
|
@@ -35,4 +46,5 @@ export const USAGE = `Usage: cruze <command> [options]
|
|
|
35
46
|
Commands:
|
|
36
47
|
${Object.values(COMMANDS).map((c) => ` ${c.usage}`).join("\n")}
|
|
37
48
|
|
|
38
|
-
Every command prints
|
|
49
|
+
Every command prints its result on stdout: JSON when stdout isn't a terminal or with --json,
|
|
50
|
+
and text on a terminal or with --text. Errors and notes go to stderr.`;
|
|
@@ -5,6 +5,8 @@ export interface ParsedCommand {
|
|
|
5
5
|
/** The command and, for grouped commands, its subcommand, such as `new feature`. */
|
|
6
6
|
words: string[];
|
|
7
7
|
options: Options;
|
|
8
|
+
/** The options given on the command line, by long name, so a command can refuse ones it doesn't take. */
|
|
9
|
+
given: string[];
|
|
8
10
|
help: boolean;
|
|
9
11
|
version: boolean;
|
|
10
12
|
}
|
|
@@ -15,11 +17,13 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
15
17
|
parsed = parseArgs({
|
|
16
18
|
args: argv,
|
|
17
19
|
allowPositionals: true,
|
|
20
|
+
tokens: true,
|
|
18
21
|
options: {
|
|
19
22
|
name: { type: "string" },
|
|
20
23
|
yes: { type: "boolean", short: "y", default: false },
|
|
21
24
|
agent: { type: "string", multiple: true, default: [] },
|
|
22
25
|
json: { type: "boolean", default: false },
|
|
26
|
+
text: { type: "boolean", default: false },
|
|
23
27
|
version: { type: "boolean", short: "v", default: false },
|
|
24
28
|
help: { type: "boolean", short: "h", default: false },
|
|
25
29
|
title: { type: "string" },
|
|
@@ -53,13 +57,14 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
53
57
|
} catch (error) {
|
|
54
58
|
throw new UsageError(error instanceof Error ? error.message : String(error));
|
|
55
59
|
}
|
|
56
|
-
const { values, positionals } = parsed;
|
|
60
|
+
const { values, positionals, tokens } = parsed;
|
|
57
61
|
const optional = <K extends string>(key: K, value: string | undefined): Partial<Record<K, string>> =>
|
|
58
62
|
value === undefined ? {} : ({ [key]: value } as Record<K, string>);
|
|
59
63
|
const options: Options = {
|
|
60
64
|
yes: values.yes,
|
|
61
65
|
agent: values.agent,
|
|
62
66
|
json: values.json,
|
|
67
|
+
text: values.text,
|
|
63
68
|
set: values.set,
|
|
64
69
|
redact: !values["no-redact"],
|
|
65
70
|
all: values.all,
|
|
@@ -88,5 +93,6 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
88
93
|
...optional("base", values.base),
|
|
89
94
|
...optional("blockers", values.blockers),
|
|
90
95
|
};
|
|
91
|
-
|
|
96
|
+
const given = [...new Set(tokens.flatMap((token) => (token.kind === "option" ? [token.name] : [])))];
|
|
97
|
+
return { words: positionals, options, given, help: values.help, version: values.version };
|
|
92
98
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { versionNotice } from "../../../app/use_cases/record_version.ts";
|
|
2
2
|
import { CruzeError } from "../../../domain/cruze_error.ts";
|
|
3
3
|
import { type CliContext, UsageError } from "./cli_context.ts";
|
|
4
|
+
import { refuseUnknownOptions } from "./command_options.ts";
|
|
4
5
|
import { COMMANDS, USAGE } from "./commands.ts";
|
|
5
6
|
import { parseCommand } from "./parse_command.ts";
|
|
6
7
|
|
|
@@ -16,15 +17,17 @@ const NO_VERSION_NOTICE = new Set(["init", "install", "validate"]);
|
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Runs one CLI invocation and returns the exit code: 0 on success, 1 when a check fails or
|
|
19
|
-
* an expected error occurs, 2 for usage errors.
|
|
20
|
-
*
|
|
20
|
+
* an expected error occurs, 2 for usage errors. The result goes to stdout in one format: JSON for an agent,
|
|
21
|
+
* when stdout isn't a terminal or with --json, and text for a person, on a terminal or with --text.
|
|
22
|
+
* stderr carries errors, and a note when the project's skills don't match this CLI, even when the command fails.
|
|
21
23
|
*/
|
|
22
24
|
export async function runCli(argv: string[], context: CliContext, terminal: Terminal): Promise<number> {
|
|
23
|
-
|
|
25
|
+
// Read from argv too, so a usage error is reported in the format asked for.
|
|
26
|
+
const json = argv.includes("--json") || (!terminal.stdoutIsTerminal && !argv.includes("--text"));
|
|
24
27
|
let notice: string | undefined;
|
|
25
28
|
try {
|
|
26
29
|
const parsed = parseCommand(argv);
|
|
27
|
-
json
|
|
30
|
+
if (parsed.options.json && parsed.options.text) throw new UsageError("--json and --text are alternatives");
|
|
28
31
|
if (parsed.version) {
|
|
29
32
|
terminal.stdout(context.bundle.version);
|
|
30
33
|
return 0;
|
|
@@ -36,10 +39,11 @@ export async function runCli(argv: string[], context: CliContext, terminal: Term
|
|
|
36
39
|
}
|
|
37
40
|
const command = COMMANDS[name];
|
|
38
41
|
if (command === undefined) throw new UsageError(`Unknown command: ${name}`);
|
|
42
|
+
refuseUnknownOptions(name, args, command, parsed.given);
|
|
39
43
|
if (!NO_VERSION_NOTICE.has(name)) notice = await versionNotice(context.files, context.bundle.version);
|
|
40
44
|
const result = await command.handler(context, args, parsed.options);
|
|
41
|
-
terminal.stderr(result.human);
|
|
42
45
|
if (json) terminal.stdout(JSON.stringify(result.json, null, 2));
|
|
46
|
+
else if (result.human !== "") terminal.stdout(result.human);
|
|
43
47
|
return result.failed === true ? 1 : 0;
|
|
44
48
|
} catch (error) {
|
|
45
49
|
if (error instanceof UsageError) {
|