@printpractical/cruze 0.0.1 → 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 +41 -17
- package/dist/adapters/inbound/cli/cli_context.js +1 -0
- package/dist/adapters/inbound/cli/commands/code.js +2 -1
- package/dist/adapters/inbound/cli/commands/install.js +4 -0
- package/dist/adapters/inbound/cli/commands/realign.js +28 -0
- package/dist/adapters/inbound/cli/commands/review.js +1 -1
- package/dist/adapters/inbound/cli/commands.js +3 -1
- package/dist/adapters/inbound/cli/parse_command.js +2 -0
- package/dist/adapters/inbound/cli/run_cli.js +9 -0
- 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/project_context.js +4 -2
- package/dist/app/use_cases/check_code.js +8 -3
- package/dist/app/use_cases/init_project.js +31 -3
- package/dist/app/use_cases/install_skills.js +8 -1
- package/dist/app/use_cases/journal_events.js +4 -3
- package/dist/app/use_cases/land_change.js +27 -6
- package/dist/app/use_cases/realign_done.js +45 -0
- package/dist/app/use_cases/realign_status.js +106 -0
- package/dist/app/use_cases/record_version.js +56 -0
- package/dist/app/use_cases/run_review.js +26 -12
- package/dist/app/use_cases/validate_project.js +10 -4
- package/dist/domain/check/check_source.js +23 -2
- package/dist/domain/config.js +18 -8
- package/dist/domain/journal.js +29 -5
- package/dist/domain/project/layout.js +1 -0
- package/dist/domain/scaffold.js +15 -1
- package/dist/domain/status/next_step.js +19 -2
- package/dist/domain/validation/plan_rules.js +18 -1
- package/dist/domain/validation/project_rules.js +17 -1
- package/dist/domain/validation/validate_project.js +2 -1
- package/dist/domain/validation/work_item_rules.js +7 -0
- package/dist/domain/versions/ci_pin.js +40 -0
- package/dist/domain/versions/cruze_version.js +51 -0
- package/dist/domain/versions/realign_notes.js +72 -0
- package/dist/domain/versions/rule_versions.js +22 -0
- package/dist/domain/versions/scheduled_exceptions.js +41 -0
- package/package.json +1 -1
- package/skills/architect/feature.md +4 -2
- package/skills/behavioural-testing/SKILL.md +6 -0
- package/skills/build/SKILL.md +1 -1
- package/skills/envision/project.md +4 -2
- package/skills/formats/reference/config.md +7 -3
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/hexagonal-design/SKILL.md +7 -1
- package/skills/hexagonal-design/languages/rust.md +7 -2
- package/skills/land/SKILL.md +1 -1
- package/skills/next/SKILL.md +1 -0
- package/skills/plan/tasks.md +7 -3
- package/skills/plan/test-plan.md +1 -1
- package/skills/realign/SKILL.md +35 -0
- package/skills/realign/notes/0.0.2.md +69 -0
- package/skills/realign/notes/0.0.3.md +11 -0
- package/skills/realign/notes.md +30 -0
- package/skills/research/SKILL.md +2 -1
- package/skills/rethink/SKILL.md +3 -1
- package/skills/rethink/levels.md +1 -1
- package/skills/roles/SKILL.md +5 -2
- package/skills/roles/auditor.md +35 -0
- package/skills/roles/code-reviewer.md +4 -3
- package/skills/roles/design-reviewer.md +9 -6
- package/skills/roles/researcher.md +1 -0
- package/src/adapters/inbound/cli/cli_context.ts +1 -0
- package/src/adapters/inbound/cli/commands/code.ts +2 -1
- package/src/adapters/inbound/cli/commands/install.ts +4 -0
- package/src/adapters/inbound/cli/commands/realign.ts +28 -0
- package/src/adapters/inbound/cli/commands/review.ts +1 -1
- package/src/adapters/inbound/cli/commands.ts +3 -1
- package/src/adapters/inbound/cli/parse_command.ts +2 -0
- package/src/adapters/inbound/cli/run_cli.ts +9 -0
- 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/project_context.ts +4 -2
- package/src/app/use_cases/check_code.ts +8 -3
- package/src/app/use_cases/init_project.ts +31 -3
- package/src/app/use_cases/install_skills.ts +8 -1
- package/src/app/use_cases/journal_events.ts +4 -3
- package/src/app/use_cases/land_change.ts +27 -6
- package/src/app/use_cases/realign_done.ts +45 -0
- package/src/app/use_cases/realign_status.ts +106 -0
- package/src/app/use_cases/record_version.ts +56 -0
- package/src/app/use_cases/run_review.ts +26 -12
- package/src/app/use_cases/validate_project.ts +10 -4
- package/src/domain/check/check_source.ts +23 -2
- package/src/domain/config.ts +18 -8
- package/src/domain/journal.ts +29 -5
- package/src/domain/project/layout.ts +1 -0
- package/src/domain/scaffold.ts +15 -1
- package/src/domain/status/next_step.ts +19 -2
- package/src/domain/validation/plan_rules.ts +18 -1
- package/src/domain/validation/project_rules.ts +17 -1
- package/src/domain/validation/validate_project.ts +2 -1
- package/src/domain/validation/work_item_rules.ts +7 -0
- package/src/domain/versions/ci_pin.ts +40 -0
- package/src/domain/versions/cruze_version.ts +51 -0
- package/src/domain/versions/realign_notes.ts +72 -0
- package/src/domain/versions/rule_versions.ts +22 -0
- package/src/domain/versions/scheduled_exceptions.ts +41 -0
- package/templates/init/config.yaml +5 -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,35 +61,56 @@ 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 |
|
|
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
|
|
|
82
|
-
|
|
82
|
+
The CLI writes JSON to stdout when it isn't attached to a terminal, and a human summary to stderr. Pass `--json` to get JSON on a terminal too.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
cruze install
|
|
86
|
-
```
|
|
84
|
+
## Upgrading
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
A repository runs three copies of Cruze: the CLI on your path, the skills in `.agents/skills/`, and the CLI that CI runs. `.cruze/config.yaml` records the version the skills came from, as `cruze: "0.0.2"`. When the CLI you run differs from it, every `cruze` command says so in a note on stderr, and `cruze validate` reports it as a `cruze-version` warning.
|
|
89
87
|
|
|
90
|
-
`
|
|
88
|
+
It also records `standards:`, the version whose expectations the code meets. A release can expect more of existing code, such as where tests live. Its new rules report on your code right away, but they fail CI only once you realign to that release, so upgrading never turns CI red.
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
1. **Upgrade the CLI** the same way you installed it, naming the new version, then check it with `cruze --version`:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm install -g https://github.com/PrintPractical/cruze/releases/download/v<version>/printpractical-cruze-<version>.tgz
|
|
94
|
+
npm install -g @printpractical/cruze@<version>
|
|
95
|
+
npm install -g --install-links github:PrintPractical/cruze#v<version>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
2. **Update each repository.** In its root, run:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
cruze install
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`install` does three things:
|
|
105
|
+
- It replaces the Cruze skills (those named `cruze-*`) and leaves your own skills alone. Pass `--agent claude` to link them for Claude Code in a repository without `CLAUDE.md` or `.claude/`.
|
|
106
|
+
- It records the new version in `.cruze/config.yaml`.
|
|
107
|
+
- It moves `.github/workflows/ci.yml` to the new version when CI runs the npm package, a release tarball or a release tag. It names any other source, such as a branch, for you to update by hand.
|
|
108
|
+
|
|
109
|
+
3. **Commit** the skills, the config and the CI workflow together, with a message such as `chore: upgrade Cruze to <version>`, so CI runs the version the skills were written for.
|
|
110
|
+
|
|
111
|
+
4. **Realign the code** when it suits you, with the `realign` skill. `cruze realign status` lists what each release since `standards:` expects of existing code: what its CLI rules find, and the questions a fresh-context auditor answers. You decide each finding: fix it in a prefactor change, waive it for good, or defer it. `cruze realign done` then moves `standards:` up. Findings still waiting for their prefactor are listed under `check.exceptions` until it lands, so CI stays green. Add `--full` to audit against every release, such as for a project that adopted Cruze partway. `cruze next` suggests realigning when nothing else is due.
|
|
112
|
+
|
|
113
|
+
An older CLI refuses to install over a repository that already moved to a newer version, so a stale CLI on another machine can't roll it back. To go back on purpose, set `cruze:` in `.cruze/config.yaml` to the older version first, then run that version's `cruze install`.
|
|
93
114
|
|
|
94
115
|
## Commands
|
|
95
116
|
|
|
@@ -109,8 +130,10 @@ Skills call these at fixed points, and CI runs `check` and `trace` on every push
|
|
|
109
130
|
| `cruze roadmap prune` | Clears landed items from the roadmap's status when a release closes |
|
|
110
131
|
| `cruze trace` | Fails when a delivered scenario has no test carrying its ID; `--all` checks every built scenario |
|
|
111
132
|
| `cruze check` | Enforces the layer rules and the file budgets; `--ci` also fails on budget warnings |
|
|
112
|
-
| `cruze review <role>` | Runs a
|
|
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 |
|
|
113
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
|
+
| `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
|
+
| `cruze realign done` | Moves `standards:` up once every finding is fixed, waived or scheduled in a change named with `--change` |
|
|
114
137
|
| `cruze abandon <ref>` | Archives a feature or standalone change that stops for good, with its reason, and puts a feature back on the future list |
|
|
115
138
|
| `cruze journal add <event>` | Records a rethink, review round, disposition, verification, override or bug; `cruze feedback export` bundles the journal for improving Cruze itself |
|
|
116
139
|
|
|
@@ -132,8 +155,9 @@ Stepping back is editing: change an upstream document and every approval that ci
|
|
|
132
155
|
| `cruze-land` | Merges the change into the living docs and updates the changelog, README and `AGENTS.md`; also lands bug fixes |
|
|
133
156
|
| `cruze-triage` | Reproduces a bug, finds its root cause against the living docs, and fixes it test-first or routes it to architect or rethink |
|
|
134
157
|
| `cruze-rethink` | Steps back to the level a discovery touches, keeps completed work, re-approves what went stale and records why |
|
|
158
|
+
| `cruze-realign` | Brings existing code up to what a new Cruze expects: audits it against each release's notes, decides every finding with you, and schedules the fixes as prefactor changes |
|
|
135
159
|
| `cruze-next` | Says which step to run next and why |
|
|
136
|
-
| `cruze-roles` | The fresh-context design reviewer, code reviewer, verifier and
|
|
160
|
+
| `cruze-roles` | The fresh-context design reviewer, code reviewer, verifier, researcher and auditor, and how a review runs to a disposition for every finding |
|
|
137
161
|
| `cruze-formats` | The format of every Cruze document, with templates |
|
|
138
162
|
| `cruze-hexagonal-design` | Ports and adapters with domain-driven design: who owns each rule, dependency direction, many small modules, contracts, runtime ownership, and notes for Rust and C++ |
|
|
139
163
|
| `cruze-behavioural-testing` | Which tests to write and at which seam, fakes instead of mocks, and protecting approved scenarios' tests |
|
|
@@ -5,7 +5,7 @@ import { landChange } from "../../../../app/use_cases/land_change.js";
|
|
|
5
5
|
export async function runCheck(context , args , options ) {
|
|
6
6
|
const files = [...options.file, ...args];
|
|
7
7
|
const report = await checkCode(context, { ci: options.ci, ...(files.length === 0 ? {} : { files }) });
|
|
8
|
-
const lines = report.findings.map((f) => `${f.severity} ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message} [${f.rule}]`);
|
|
8
|
+
const lines = report.findings.map((f) => `${f.severity} ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message} [${f.rule}]${f.pending === true ? " (pending realign)" : ""}`);
|
|
9
9
|
lines.push(`Checked ${report.checked} file(s): ${report.passed ? "passed" : "failed"}.`);
|
|
10
10
|
return { json: report, human: lines.join("\n"), failed: !report.passed };
|
|
11
11
|
}
|
|
@@ -25,6 +25,7 @@ export async function runLand(context , args , options
|
|
|
25
25
|
if (report.built.length > 0) lines.push(`Now built: ${report.built.join(", ")}`);
|
|
26
26
|
if (report.removed.length > 0) lines.push(`Removed: ${report.removed.join(", ")}`);
|
|
27
27
|
if (report.restamped.length > 0) lines.push(`Re-stamped approvals: ${report.restamped.join(", ")}`);
|
|
28
|
+
if (report.unexcepted.length > 0) lines.push(`CI checks these again, now that the change realign scheduled them in has landed: ${report.unexcepted.join(", ")}`);
|
|
28
29
|
if (report.archivedTo !== undefined) lines.push(`Finished; archived to ${report.archivedTo}.`);
|
|
29
30
|
return { json: report, human: lines.join("\n") };
|
|
30
31
|
}
|
|
@@ -11,5 +11,9 @@ export function describeSkillInstall(report ) {
|
|
|
11
11
|
const lines = [`Installed ${count} Cruze skill${count === 1 ? "" : "s"} into .agents/skills/: ${report.installed.join(", ")}`];
|
|
12
12
|
if (report.removed.length > 0) lines.push(`Removed skills no longer shipped: ${report.removed.join(", ")}`);
|
|
13
13
|
for (const link of report.linked) lines.push(`Linked them for ${link.agent} in ${link.dir}/`);
|
|
14
|
+
const { from, to, ci } = report.recorded;
|
|
15
|
+
if (from !== to) lines.push(`Recorded Cruze ${to} in .cruze/config.yaml${from === undefined ? "" : `, up from ${from}`}`);
|
|
16
|
+
if (ci.repinned.length > 0) lines.push(`CI runs ${ci.repinned.join(", ")}`);
|
|
17
|
+
for (const spec of ci.unrecognised) lines.push(`CI runs ${spec}, which Cruze can't move to ${to}; update .github/workflows/ci.yml yourself`);
|
|
14
18
|
return lines;
|
|
15
19
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { realignDone } from "../../../../app/use_cases/realign_done.js";
|
|
2
|
+
import { realignStatus } from "../../../../app/use_cases/realign_status.js";
|
|
3
|
+
import { UsageError, requireArgs, } from "../cli_context.js";
|
|
4
|
+
|
|
5
|
+
export async function runRealign(context , args , options ) {
|
|
6
|
+
requireArgs(args, ["status|done"]);
|
|
7
|
+
if (args[0] === "status") {
|
|
8
|
+
const report = await realignStatus(context, { full: options.full });
|
|
9
|
+
const lines = [`Code meets Cruze ${report.standards}; the installed skills are ${report.target}${report.full ? " (full audit)" : ""}.`];
|
|
10
|
+
for (const note of report.notes) {
|
|
11
|
+
const open = note.findings.filter((f) => !f.waived);
|
|
12
|
+
const how = note.detect !== undefined ? `${open.length} open finding(s) from ${note.detect}` : `for the auditor: ${note.ask ?? ""}`;
|
|
13
|
+
lines.push(`${note.version} ${note.id}: ${note.title} (${how})`);
|
|
14
|
+
for (const f of open) lines.push(` ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message}`);
|
|
15
|
+
}
|
|
16
|
+
if (report.newWorkOnly.length > 0) lines.push(`New work only, nothing to move: ${report.newWorkOnly.join(", ")}`);
|
|
17
|
+
if (report.notes.length === 0) lines.push("Nothing to realign.");
|
|
18
|
+
return { json: report, human: lines.join("\n") };
|
|
19
|
+
}
|
|
20
|
+
if (args[0] === "done") {
|
|
21
|
+
const report = await realignDone(context, options.change === undefined ? {} : { change: options.change });
|
|
22
|
+
const lines = [report.from === report.to ? `The code already meets Cruze ${report.to}.` : `The code now meets Cruze ${report.to}, up from ${report.from}.`];
|
|
23
|
+
if (report.scheduled !== undefined) lines.push(`${report.scheduled} fixes the rest.`);
|
|
24
|
+
if (report.excepted.length > 0) lines.push(`Listed under check.exceptions until it lands: ${report.excepted.join(", ")}`);
|
|
25
|
+
return { json: report, human: lines.join("\n") };
|
|
26
|
+
}
|
|
27
|
+
throw new UsageError(`cruze realign takes status or done, not "${args[0]}"`);
|
|
28
|
+
}
|
|
@@ -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
|
}
|
|
@@ -3,13 +3,14 @@ import { runCheck, runLand, runTrace } from "./commands/code.js";
|
|
|
3
3
|
import { runApprove, runNew, runValidate } from "./commands/documents.js";
|
|
4
4
|
import { runInit } from "./commands/init.js";
|
|
5
5
|
import { runInstall } from "./commands/install.js";
|
|
6
|
+
import { runRealign } from "./commands/realign.js";
|
|
6
7
|
import { runFeedback, runJournal } from "./commands/journal.js";
|
|
7
8
|
import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.js";
|
|
8
9
|
import { runReviewCommand } from "./commands/review.js";
|
|
9
10
|
import { runNext, runStatus } from "./commands/status.js";
|
|
10
11
|
|
|
11
12
|
export const COMMANDS = {
|
|
12
|
-
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" },
|
|
13
14
|
install: { handler: runInstall, usage: "install [--agent <agent>] install or update the Cruze skills" },
|
|
14
15
|
validate: { handler: runValidate, usage: "validate check every document against the formats" },
|
|
15
16
|
status: { handler: runStatus, usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
@@ -26,6 +27,7 @@ export const COMMANDS = {
|
|
|
26
27
|
check: { handler: runCheck, usage: "check [<file>...] [--ci] enforce layer rules and file budgets" },
|
|
27
28
|
abandon: { handler: runAbandon, usage: "abandon <ref> --reason <text> archive a feature or standalone change that stops for good" },
|
|
28
29
|
land: { handler: runLand, usage: "land [<ref>] [--override <reason>] merge a verified change into the living docs" },
|
|
30
|
+
realign: { handler: runRealign, usage: "realign <status [--full]|done [--change <ref>]> what the installed Cruze expects of existing code, and recording that the code meets it" },
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const USAGE = `Usage: cruze <command> [options]
|
|
@@ -41,6 +41,7 @@ export function parseCommand(argv ) {
|
|
|
41
41
|
gate: { type: "string" },
|
|
42
42
|
override: { type: "string" },
|
|
43
43
|
overlap: { type: "boolean", default: false },
|
|
44
|
+
full: { type: "boolean", default: false },
|
|
44
45
|
round: { type: "string" },
|
|
45
46
|
package: { type: "string" },
|
|
46
47
|
replan: { type: "string", multiple: true, default: [] },
|
|
@@ -65,6 +66,7 @@ export function parseCommand(argv ) {
|
|
|
65
66
|
file: values.file,
|
|
66
67
|
ci: values.ci,
|
|
67
68
|
overlap: values.overlap,
|
|
69
|
+
full: values.full,
|
|
68
70
|
replan: values.replan,
|
|
69
71
|
rebase: values.rebase,
|
|
70
72
|
...optional("name", values.name),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { versionNotice } from "../../../app/use_cases/record_version.js";
|
|
1
2
|
import { CruzeError } from "../../../domain/cruze_error.js";
|
|
2
3
|
import { UsageError } from "./cli_context.js";
|
|
3
4
|
import { COMMANDS, USAGE } from "./commands.js";
|
|
@@ -10,12 +11,17 @@ import { parseCommand } from "./parse_command.js";
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
/** Commands that set the version themselves, or report the skew as a problem of their own. */
|
|
15
|
+
const NO_VERSION_NOTICE = new Set(["init", "install", "validate"]);
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Runs one CLI invocation and returns the exit code: 0 on success, 1 when a check fails or
|
|
15
19
|
* an expected error occurs, 2 for usage errors. Agents get JSON on stdout; people get a summary on stderr.
|
|
20
|
+
* When the project's skills don't match this CLI, a note on stderr says so, even when the command fails.
|
|
16
21
|
*/
|
|
17
22
|
export async function runCli(argv , context , terminal ) {
|
|
18
23
|
let json = !terminal.stdoutIsTerminal || argv.includes("--json");
|
|
24
|
+
let notice ;
|
|
19
25
|
try {
|
|
20
26
|
const parsed = parseCommand(argv);
|
|
21
27
|
json ||= parsed.options.json;
|
|
@@ -30,6 +36,7 @@ export async function runCli(argv , context , terminal
|
|
|
30
36
|
}
|
|
31
37
|
const command = COMMANDS[name];
|
|
32
38
|
if (command === undefined) throw new UsageError(`Unknown command: ${name}`);
|
|
39
|
+
if (!NO_VERSION_NOTICE.has(name)) notice = await versionNotice(context.files, context.bundle.version);
|
|
33
40
|
const result = await command.handler(context, args, parsed.options);
|
|
34
41
|
terminal.stderr(result.human);
|
|
35
42
|
if (json) terminal.stdout(JSON.stringify(result.json, null, 2));
|
|
@@ -46,5 +53,7 @@ export async function runCli(argv , context , terminal
|
|
|
46
53
|
return 1;
|
|
47
54
|
}
|
|
48
55
|
throw error;
|
|
56
|
+
} finally {
|
|
57
|
+
if (notice !== undefined) terminal.stderr(`cruze: note: ${notice}`);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
@@ -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,5 @@
|
|
|
1
1
|
import { buildProjectView, } from "../domain/project/project_view.js";
|
|
2
|
-
import { SNAPSHOT_ROOTS } from "../domain/project/layout.js";
|
|
2
|
+
import { PATHS, SNAPSHOT_ROOTS } from "../domain/project/layout.js";
|
|
3
3
|
import { journalPath, serializeEntry, } from "../domain/journal.js";
|
|
4
4
|
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ import { journalPath, serializeEntry, } from "../domain/journa
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
/** Reads every document under docs/ and .cruze
|
|
17
|
+
/** Reads every document under docs/ and .cruze/, and the README, into a view. */
|
|
18
18
|
export async function loadView(files ) {
|
|
19
19
|
const snapshot = new Map ();
|
|
20
20
|
for (const root of SNAPSHOT_ROOTS) {
|
|
@@ -23,6 +23,8 @@ export async function loadView(files ) {
|
|
|
23
23
|
if (text !== undefined) snapshot.set(path, text);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const readme = await files.readText(PATHS.readme);
|
|
27
|
+
if (readme !== undefined) snapshot.set(PATHS.readme, readme);
|
|
26
28
|
return buildProjectView(snapshot);
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -2,6 +2,7 @@ import { factsOf } from "../../domain/elements.js";
|
|
|
2
2
|
import { checkSource, } from "../../domain/check/check_source.js";
|
|
3
3
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
4
4
|
import { matchesAny } from "../../domain/glob.js";
|
|
5
|
+
import { FIRST_RELEASE, isPending } from "../../domain/versions/rule_versions.js";
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
import { resolveChange } from "../../domain/project/artifact_ref.js";
|
|
@@ -18,7 +19,8 @@ import { loadView, } from "../project_context.js";
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Enforces layer rules and file budgets. Layer violations always fail; budget and
|
|
21
|
-
* module-map warnings fail only with `ci`.
|
|
22
|
+
* module-map warnings fail only with `ci`. A rule newer than the project's `standards:`
|
|
23
|
+
* is pending: it reports, but fails nothing until the project realigns.
|
|
22
24
|
*/
|
|
23
25
|
export async function checkCode(deps , options ) {
|
|
24
26
|
const view = await loadView(deps.files);
|
|
@@ -32,8 +34,11 @@ export async function checkCode(deps , options
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
const only = options.files?.filter((file) => sources.has(file));
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
+
const standards = config.standards ?? FIRST_RELEASE;
|
|
38
|
+
const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
|
|
39
|
+
isPending("check", f.rule, standards) ? { ...f, pending: true } : f,
|
|
40
|
+
);
|
|
41
|
+
const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
|
|
37
42
|
return { passed: failing.length === 0, checked: only?.length ?? sources.size, findings };
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -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
|
}
|
|
@@ -4,6 +4,7 @@ import { AGENT_SKILL_HOMES, SHARED_SKILLS_DIR, agentSkillHome, knownAgents } fro
|
|
|
4
4
|
import { SKILL_FILE, installedSkillName, isInstalledSkillName, skillProblems } from "../../domain/skill.js";
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
import { recordVersion, refuseDowngrade, } from "./record_version.js";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
@@ -15,12 +16,16 @@ import { SKILL_FILE, installedSkillName, isInstalledSkillName, skillProblems } f
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Replaces the project's Cruze skills with the bundled set. Skills are written to
|
|
22
25
|
* the shared Agent Skills directory, and linked for every agent the project uses.
|
|
23
26
|
* Skills without the Cruze prefix belong to the user and are never touched.
|
|
27
|
+
* The project then records this version, and its CI runs it too. An older CLI
|
|
28
|
+
* never replaces the skills of a project that moved to a newer one.
|
|
24
29
|
*/
|
|
25
30
|
export async function installSkills(
|
|
26
31
|
deps ,
|
|
@@ -29,6 +34,7 @@ export async function installSkills(
|
|
|
29
34
|
const { files, bundle } = deps;
|
|
30
35
|
const skills = await bundle.skills();
|
|
31
36
|
assertValid(skills);
|
|
37
|
+
await refuseDowngrade(files, bundle.version);
|
|
32
38
|
|
|
33
39
|
const names = skills.map((skill) => installedSkillName(skill.folder));
|
|
34
40
|
const removed = await removeStaleSkills(files, SHARED_SKILLS_DIR, names);
|
|
@@ -48,7 +54,8 @@ export async function installSkills(
|
|
|
48
54
|
linked.push({ agent, dir: home.skillsDir });
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
const recorded = await recordVersion(files, bundle.version);
|
|
58
|
+
return { version: bundle.version, installed: names, removed, linked, recorded };
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
function assertValid(skills ) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CruzeError } from "../../domain/cruze_error.js";
|
|
2
|
-
import { recordedEventProblems, } from "../../domain/journal.js";
|
|
2
|
+
import { recordedEventProblems, recordedFields, } from "../../domain/journal.js";
|
|
3
3
|
import { resolveArtifact } from "../../domain/project/artifact_ref.js";
|
|
4
4
|
import { appendJournal, loadView, } from "../project_context.js";
|
|
5
5
|
|
|
@@ -8,13 +8,14 @@ export async function recordEvent(deps , event , fields
|
|
|
8
8
|
const problems = recordedEventProblems(event, fields);
|
|
9
9
|
if (problems.length > 0) throw new CruzeError("invalid-event", problems.join("; "));
|
|
10
10
|
let folder = ".cruze";
|
|
11
|
+
let recorded = recordedFields(fields);
|
|
11
12
|
if (item !== undefined) {
|
|
12
13
|
const resolved = resolveArtifact(await loadView(deps.files), item).item;
|
|
13
14
|
if (resolved === undefined) throw new CruzeError("not-found", `"${item}" is not a feature or change`);
|
|
14
15
|
folder = resolved.folder;
|
|
15
|
-
|
|
16
|
+
recorded = { ...recorded, item: resolved.ref };
|
|
16
17
|
}
|
|
17
|
-
return appendJournal(deps, folder, event, { ...
|
|
18
|
+
return appendJournal(deps, folder, event, { ...recorded, cruze: deps.bundle.version });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function listEvents(deps , event ) {
|
|
@@ -15,6 +15,7 @@ import { changesOf, featureOf, } from "../../domain/project/work_i
|
|
|
15
15
|
import { isVerified } from "../../domain/status/verification.js";
|
|
16
16
|
import { activeChange } from "../../domain/status/work_status.js";
|
|
17
17
|
import { validateProject } from "../../domain/validation/validate_project.js";
|
|
18
|
+
import { withoutScheduledExceptions } from "../../domain/versions/scheduled_exceptions.js";
|
|
18
19
|
import { appendJournal, loadView, withFiles, } from "../project_context.js";
|
|
19
20
|
|
|
20
21
|
const AGENTS_FILE = "AGENTS.md";
|
|
@@ -26,6 +27,8 @@ const AGENTS_FILE = "AGENTS.md";
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
|
|
@@ -54,7 +57,9 @@ export async function landChange(deps , ref , options
|
|
|
54
57
|
const landed = `${deps.clock.today()}${commit === null ? "" : ` (${commit})`}`;
|
|
55
58
|
const books = applyBookkeeping(view, change, source, landed, plan.files);
|
|
56
59
|
const merged = withFiles(view, plan.files);
|
|
57
|
-
|
|
60
|
+
const recorded = { ...(merged.approvals.get(source.folder) ?? { version: 1, approvals: [], merged: {} }), merged: plan.merged };
|
|
61
|
+
const checked = withFiles(merged, new Map([[approvalsPath(source.folder), serializeApprovals(recorded)]]));
|
|
62
|
+
requireStillValid(view, checked, [source.path, ...changesOf(view.items, source).map((c) => c.path)]);
|
|
58
63
|
|
|
59
64
|
const approvals = new Map ();
|
|
60
65
|
const by = (await deps.repository.userName()) ?? "unknown";
|
|
@@ -81,8 +86,9 @@ export async function landChange(deps , ref , options
|
|
|
81
86
|
const rendered = renderAgentsFile(agents, merged);
|
|
82
87
|
if (rendered !== agents) await deps.files.writeText(AGENTS_FILE, rendered);
|
|
83
88
|
}
|
|
89
|
+
const unexcepted = await clearScheduledExceptions(deps, [change.ref, source.ref]);
|
|
84
90
|
|
|
85
|
-
const report = { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, restamped: restamp, finished: books.finished };
|
|
91
|
+
const report = { change: change.ref, merged: plan.mergedIds, built: plan.built, removed: plan.removed, restamped: restamp, finished: books.finished, unexcepted };
|
|
86
92
|
if (books.archive !== undefined) report.archivedTo = books.archive.to;
|
|
87
93
|
return report;
|
|
88
94
|
}
|
|
@@ -102,12 +108,27 @@ function requireReady(view , change , source )
|
|
|
102
108
|
if (reasons.length > 0) throw new CruzeError("not-ready", `${change.ref} cannot land yet:\n ${reasons.join("\n ")}`);
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
/** The merge may not introduce errors into the living docs. */
|
|
106
|
-
function requireStillValid(before , after ) {
|
|
111
|
+
/** The merge may not introduce errors into the living docs, or into the work it lands, such as a citation of an ID it removes. */
|
|
112
|
+
function requireStillValid(before , after , work ) {
|
|
107
113
|
const key = (p ) => `${p.path}|${p.message}`;
|
|
108
114
|
const existing = new Set(validateProject(before).map(key));
|
|
109
|
-
const
|
|
115
|
+
const watched = (path ) => path.startsWith("docs/") || work.includes(path);
|
|
116
|
+
const introduced = validateProject(after).filter((p) => p.severity === "error" && watched(p.path) && !existing.has(key(p)));
|
|
110
117
|
if (introduced.length > 0) {
|
|
111
|
-
throw new CruzeError("merge-invalid", `landing would
|
|
118
|
+
throw new CruzeError("merge-invalid", `landing would leave errors behind; nothing was written:\n ${introduced.map((p) => `${p.path}:${p.line ?? ""} ${p.message}`).join("\n ")}`);
|
|
112
119
|
}
|
|
113
120
|
}
|
|
121
|
+
|
|
122
|
+
/** Removes the exceptions `realign` scheduled in the landed work, so CI checks those files again. */
|
|
123
|
+
async function clearScheduledExceptions(deps , refs ) {
|
|
124
|
+
let text = await deps.files.readText(PATHS.config);
|
|
125
|
+
if (text === undefined) return [];
|
|
126
|
+
const removed = [];
|
|
127
|
+
for (const ref of [...new Set(refs)]) {
|
|
128
|
+
const result = withoutScheduledExceptions(text, ref);
|
|
129
|
+
text = result.text;
|
|
130
|
+
removed.push(...result.removed);
|
|
131
|
+
}
|
|
132
|
+
if (removed.length > 0) await deps.files.writeText(PATHS.config, text);
|
|
133
|
+
return removed;
|
|
134
|
+
}
|