@printpractical/cruze 0.0.4 → 0.0.5
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 +8 -8
- package/dist/adapters/inbound/cli/cli_context.js +2 -0
- package/dist/adapters/inbound/cli/commands/documents.js +5 -1
- package/dist/adapters/inbound/cli/commands/status.js +4 -1
- package/dist/adapters/inbound/cli/commands.js +5 -1
- package/dist/adapters/inbound/cli/parse_command.js +4 -0
- package/dist/app/use_cases/approve_artifact.js +26 -1
- package/dist/domain/approvals/agent_approval.js +68 -0
- package/dist/domain/config.js +30 -2
- package/dist/domain/journal.js +5 -0
- package/dist/domain/status/verification.js +3 -2
- package/dist/domain/validation/plan_rules.js +2 -0
- package/dist/domain/validation/size_rules.js +36 -0
- package/dist/domain/validation/work_item_rules.js +2 -0
- package/dist/domain/versions/rule_versions.js +3 -0
- package/package.json +1 -1
- package/skills/architect/feature.md +7 -2
- package/skills/formats/SKILL.md +1 -1
- package/skills/formats/reference/config.md +10 -0
- package/skills/formats/reference/work-items.md +1 -1
- package/skills/plan/SKILL.md +3 -3
- package/skills/realign/notes/0.0.5.md +38 -0
- package/skills/realign/notes.md +1 -0
- package/skills/rethink/SKILL.md +5 -1
- package/skills/rethink/levels.md +3 -3
- package/skills/roles/SKILL.md +17 -7
- package/skills/roles/code-reviewer.md +1 -1
- package/skills/roles/design-reviewer.md +2 -2
- package/skills/verify/SKILL.md +1 -1
- package/src/adapters/inbound/cli/cli_context.ts +2 -0
- package/src/adapters/inbound/cli/commands/documents.ts +5 -1
- package/src/adapters/inbound/cli/commands/status.ts +4 -1
- package/src/adapters/inbound/cli/commands.ts +5 -1
- package/src/adapters/inbound/cli/parse_command.ts +4 -0
- package/src/app/use_cases/approve_artifact.ts +26 -1
- package/src/domain/approvals/agent_approval.ts +68 -0
- package/src/domain/config.ts +30 -2
- package/src/domain/journal.ts +5 -0
- package/src/domain/status/verification.ts +3 -2
- package/src/domain/validation/plan_rules.ts +2 -0
- package/src/domain/validation/size_rules.ts +36 -0
- package/src/domain/validation/work_item_rules.ts +2 -0
- package/src/domain/versions/rule_versions.ts +3 -0
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.5 is the fifth preview. It cuts the ceremony per change: reviews bring you only the findings that are yours to decide, plans are approved once their review closes, a rethink re-stamps the work it didn't change, and changes are sized to be worth their review and verify. 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
|
|
|
@@ -22,7 +22,7 @@ Spec-driven workflows tend to fail in the same few ways:
|
|
|
22
22
|
- Everything is "fully specified", yet the result fails when a person actually uses it.
|
|
23
23
|
- Review loops keep finding new things to change and never converge.
|
|
24
24
|
|
|
25
|
-
Cruze answers each of these with a specific mechanism. A living `docs/architecture.md` is read at every step and updated by every change. Approvals are bound to document content, so stepping back is just editing. Every task names the file it touches, and `cruze check` enforces layer rules. Scenarios are traced to behavioural tests, and the system is actually run. Reviews use a closed rubric with a hard limit of two rounds.
|
|
25
|
+
Cruze answers each of these with a specific mechanism. A living `docs/architecture.md` is read at every step and updated by every change. Approvals are bound to document content, so stepping back is just editing. Every task names the file it touches, and `cruze check` enforces layer rules. Scenarios are traced to behavioural tests, and the system is actually run. Reviews use a closed rubric with a hard limit of two rounds, and bring you only the findings that are yours to decide: changes to behaviour or to the designed contracts.
|
|
26
26
|
|
|
27
27
|
## Quick start
|
|
28
28
|
|
|
@@ -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.5/printpractical-cruze-0.0.5.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.5
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### 2. Set up a repository
|
|
@@ -61,7 +61,7 @@ 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.5/printpractical-cruze-0.0.5.tgz
|
|
65
65
|
```
|
|
66
66
|
|
|
67
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:
|
|
@@ -118,8 +118,8 @@ Skills call these at fixed points, and CI runs `check` and `trace` on every push
|
|
|
118
118
|
|
|
119
119
|
| Command | What it does |
|
|
120
120
|
| --- | --- |
|
|
121
|
-
| `cruze validate` | Checks every document against the formats: IDs, elements, deltas, scope rules, task lines, test plans, roadmap and config |
|
|
122
|
-
| `cruze approve <doc>` | Records an approval as a fingerprint: the document's design hash plus the hash of every upstream element it cites. `--replan` sets built elements a rethink changed back to planned; `--rebase` accepts living text a feature must now build on |
|
|
121
|
+
| `cruze validate` | Checks every document against the formats: IDs, elements, deltas, scope rules, task lines, test plans, roadmap and config, and warns when a change falls outside the size band in `changes:` |
|
|
122
|
+
| `cruze approve <doc>` | Records an approval as a fingerprint: the document's design hash plus the hash of every upstream element it cites. `--replan` sets built elements a rethink changed back to planned; `--rebase` accepts living text a feature must now build on. `--by-agent` approves a change for you once its plan review has closed, and `--restamp-unchanged` re-stamps work a rethink didn't change; both record that the agent gave the approval |
|
|
123
123
|
| `cruze next` | Names the step to run next on this branch, and why, from computed status |
|
|
124
124
|
| `cruze status` | Computes each document's state (approved, edited, upstream changed or unapproved) from content, never from stored state |
|
|
125
125
|
| `cruze status --gate build` | Passes only when this branch's change, its feature and the architecture are approved and current |
|
|
@@ -149,7 +149,7 @@ Stepping back is editing: change an upstream document and every approval that ci
|
|
|
149
149
|
| `cruze-envision` | The what: the project's vision, goals and feature map, or one feature's intent and requirements with scenarios |
|
|
150
150
|
| `cruze-architect` | The how: the architecture, a feature's architecture delta and its changes, or a tweak; ends with a design review and a walkthrough for your approval |
|
|
151
151
|
| `cruze-roadmap` | Orders the release into phases with blocking edges and goal coverage, and closes a release |
|
|
152
|
-
| `cruze-plan` | Turns a change into a test plan and ordered tasks with file targets, reviewed in a fresh context |
|
|
152
|
+
| `cruze-plan` | Turns a change into a test plan and ordered tasks with file targets, reviewed in a fresh context and approved once the review closes |
|
|
153
153
|
| `cruze-build` | Builds the change task by task, each test-first, checked and committed, and stops to rethink when the design is wrong |
|
|
154
154
|
| `cruze-verify` | Runs the checks, a fresh-context verifier that uses the real system, a two-lane code review and a manual test script, then asks you to accept |
|
|
155
155
|
| `cruze-land` | Merges the change into the living docs and updates the changelog, README and `AGENTS.md`; also lands bug fixes |
|
|
@@ -13,8 +13,12 @@ export async function runValidate(context ) {
|
|
|
13
13
|
|
|
14
14
|
export async function runApprove(context , args , options ) {
|
|
15
15
|
requireArgs(args, ["document"]);
|
|
16
|
-
|
|
16
|
+
if (options.byAgent && options.restampUnchanged) throw new UsageError("--by-agent and --restamp-unchanged are alternatives");
|
|
17
|
+
const agent = options.byAgent ? "plan-review" : options.restampUnchanged ? "rethink" : undefined;
|
|
18
|
+
const report = await approveArtifact(context, args[0] ?? "", { replan: options.replan, rebase: options.rebase, ...(agent === undefined ? {} : { agent }) });
|
|
17
19
|
const lines = [`Approved ${report.artifact} (${report.hash.slice(0, 19)}), pinned to ${report.upstream.length} upstream element(s).`];
|
|
20
|
+
if (report.basis === "plan-review") lines.push("Approved for the user: its plan review closed with no blocker open.");
|
|
21
|
+
if (report.restamped !== undefined) lines.push(`Re-stamped for the user: its own design is unchanged, and only ${report.restamped.join(", ")} changed.`);
|
|
18
22
|
if (report.stamped.length > 0) lines.push(`Marked ${report.stamped.length} element(s) planned.`);
|
|
19
23
|
if (report.bound !== undefined) lines.push(`Bound the change to branch ${report.bound}.`);
|
|
20
24
|
if (report.replanned.length > 0) lines.push(`Set back to planned: ${report.replanned.join(", ")}.`);
|
|
@@ -35,8 +35,11 @@ export async function runStatus(context , _args , options
|
|
|
35
35
|
return { json: report, human: lines.join("\n") };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const BASIS = { "plan-review": " (by the agent, after its plan review)", rethink: " (re-stamped by the agent after a rethink)" };
|
|
39
|
+
|
|
38
40
|
function describe(label , status ) {
|
|
39
|
-
const
|
|
41
|
+
const byAgent = status.state === "approved" ? BASIS[status.record?.basis ?? ""] ?? "" : "";
|
|
42
|
+
const detail = status.changed.length > 0 ? ` (${status.changed.join(", ")})` : status.unjournaled === true ? " (no journal entry)" : byAgent;
|
|
40
43
|
return `${label}: ${status.state}${detail}`;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -22,7 +22,11 @@ export const COMMANDS = {
|
|
|
22
22
|
validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
|
|
23
23
|
status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
24
24
|
next: { handler: runNext, options: [], usage: "next the step to run next on this branch, and why" },
|
|
25
|
-
approve: {
|
|
25
|
+
approve: {
|
|
26
|
+
handler: runApprove,
|
|
27
|
+
options: ["replan", "rebase", "by-agent", "restamp-unchanged"],
|
|
28
|
+
usage: "approve <vision|architecture|roadmap|ref|path> [--replan <ID>...] [--rebase <ID>...] [--by-agent|--restamp-unchanged] record an approval of a document",
|
|
29
|
+
},
|
|
26
30
|
new: {
|
|
27
31
|
handler: runNew,
|
|
28
32
|
options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title"] },
|
|
@@ -52,6 +52,8 @@ export function parseCommand(argv ) {
|
|
|
52
52
|
rebase: { type: "string", multiple: true, default: [] },
|
|
53
53
|
base: { type: "string" },
|
|
54
54
|
blockers: { type: "string" },
|
|
55
|
+
"by-agent": { type: "boolean", default: false },
|
|
56
|
+
"restamp-unchanged": { type: "boolean", default: false },
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
} catch (error) {
|
|
@@ -74,6 +76,8 @@ export function parseCommand(argv ) {
|
|
|
74
76
|
full: values.full,
|
|
75
77
|
replan: values.replan,
|
|
76
78
|
rebase: values.rebase,
|
|
79
|
+
byAgent: values["by-agent"],
|
|
80
|
+
restampUnchanged: values["restamp-unchanged"],
|
|
77
81
|
...optional("name", values.name),
|
|
78
82
|
...optional("title", values.title),
|
|
79
83
|
...optional("feature", values.feature),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requirePlanReviewClosed, requireUnchangedSinceApproval, } from "../../domain/approvals/agent_approval.js";
|
|
1
2
|
import { evaluateApproval } from "../../domain/approvals/evaluate.js";
|
|
2
3
|
import { fingerprint, isApprovable } from "../../domain/approvals/fingerprint.js";
|
|
3
4
|
import { latestApproval, serializeApprovals, withApproval, } from "../../domain/approvals/records.js";
|
|
@@ -29,6 +30,10 @@ import { appendJournal, loadView, withFiles, } from "../project
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
|
|
@@ -36,6 +41,8 @@ import { appendJournal, loadView, withFiles, } from "../project
|
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
|
|
44
|
+
|
|
45
|
+
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
/**
|
|
@@ -53,6 +60,9 @@ export async function approveArtifact(deps , ref , options
|
|
|
53
60
|
if (item !== undefined && item.kind !== "feature" && !isPlanned(item.doc)) {
|
|
54
61
|
throw new CruzeError("not-planned", `${path} has no test plan or tasks yet; plan it before approving`);
|
|
55
62
|
}
|
|
63
|
+
const before = evaluateApproval(view, path);
|
|
64
|
+
if (options.agent === "plan-review") requirePlanReviewClosed(view.journal, item ?? notWork(path));
|
|
65
|
+
if (options.agent === "rethink") requireUnchangedSinceApproval(view.journal, item, path, before);
|
|
56
66
|
|
|
57
67
|
const edits = new Map ();
|
|
58
68
|
let text = view.snapshot.get(path) ?? "";
|
|
@@ -103,12 +113,23 @@ export async function approveArtifact(deps , ref , options
|
|
|
103
113
|
upstream,
|
|
104
114
|
approvedAt: deps.clock.now().toISOString(),
|
|
105
115
|
by: (await deps.repository.userName()) ?? "unknown",
|
|
116
|
+
...(options.agent === undefined ? {} : { basis: options.agent }),
|
|
106
117
|
});
|
|
107
118
|
for (const [changed, content] of edits) await deps.files.writeText(changed, content);
|
|
108
119
|
await deps.files.writeText(approvalsPath(folder), serializeApprovals(approvals));
|
|
109
|
-
|
|
120
|
+
const restamped = options.agent === "rethink" ? before.changed : undefined;
|
|
121
|
+
await appendJournal(deps, folder, "approve", {
|
|
122
|
+
artifact: path,
|
|
123
|
+
hash,
|
|
124
|
+
...(replanned.length > 0 ? { replanned } : {}),
|
|
125
|
+
...(rebased.length > 0 ? { rebased } : {}),
|
|
126
|
+
...(options.agent === undefined ? {} : { basis: options.agent }),
|
|
127
|
+
...(restamped === undefined ? {} : { restamped }),
|
|
128
|
+
});
|
|
110
129
|
const report = { artifact: path, hash, upstream: Object.keys(upstream), stamped, replanned, rebased };
|
|
111
130
|
if (bound !== undefined) report.bound = bound;
|
|
131
|
+
if (options.agent !== undefined) report.basis = options.agent;
|
|
132
|
+
if (restamped !== undefined) report.restamped = restamped;
|
|
112
133
|
return report;
|
|
113
134
|
}
|
|
114
135
|
|
|
@@ -133,6 +154,10 @@ function requireRebased(view , deltaIds , previous
|
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
function notWork(path ) {
|
|
158
|
+
throw new CruzeError("needs-user", `${path} is a project document, so the user approves it`);
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
/** A change needs its feature approved; a feature or standalone change needs the architecture approved. */
|
|
137
162
|
function requireUpstreamApproved(view , path ) {
|
|
138
163
|
const item = view.items.find((i) => i.path === path);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CruzeError } from "../cruze_error.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Why an approval was given without the user: a plan whose review closed, or work that a
|
|
8
|
+
* rethink left unchanged while something it cites moved. The user can still object, and
|
|
9
|
+
* editing the document makes the approval stale as usual.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const byTime = (a , b ) => a.at.localeCompare(b.at);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A feature's change may be approved by the agent once its plan review has closed: a review
|
|
17
|
+
* recorded since the change was last approved, no blocker left open in its last round, and a
|
|
18
|
+
* disposition for every finding of its first round. The feature's design, and a standalone
|
|
19
|
+
* change's, stay the user's to approve.
|
|
20
|
+
*/
|
|
21
|
+
export function requirePlanReviewClosed(journal , item ) {
|
|
22
|
+
if (item.kind !== "change") {
|
|
23
|
+
throw new CruzeError("needs-user", `${item.ref} carries design, so the user approves it; only a feature's change can be approved after its plan review`);
|
|
24
|
+
}
|
|
25
|
+
const entries = journal.filter((e) => e["item"] === item.ref || e["artifact"] === item.path).sort(byTime);
|
|
26
|
+
const lastApproval = entries.filter((e) => e.event === "approve").at(-1);
|
|
27
|
+
const reviews = entries.filter((e) => e.event === "review" && e["review"] === "plan" && (lastApproval === undefined || e.at > lastApproval.at));
|
|
28
|
+
const last = reviews.at(-1);
|
|
29
|
+
if (last === undefined) {
|
|
30
|
+
const since = lastApproval === undefined ? "" : " since the change was last approved";
|
|
31
|
+
throw new CruzeError("plan-review-open", `no plan review is recorded for ${item.ref}${since}; run the plan review first`);
|
|
32
|
+
}
|
|
33
|
+
const blockers = Number(last["blockers"] ?? 0);
|
|
34
|
+
if (blockers > 0 && Number(last["round"]) === 1) {
|
|
35
|
+
throw new CruzeError("plan-review-open", `the plan review's round 1 found ${blockers} blocker(s); check their fixes in round 2`);
|
|
36
|
+
}
|
|
37
|
+
if (blockers > 0) {
|
|
38
|
+
throw new CruzeError("plan-review-open", `round 2 left ${blockers} blocker(s) open; the user decides them and approves the plan`);
|
|
39
|
+
}
|
|
40
|
+
const first = reviews.filter((e) => Number(e["round"]) === 1).at(-1) ?? last;
|
|
41
|
+
const findings = Number(first["blockers"] ?? 0) + Number(first["concerns"] ?? 0);
|
|
42
|
+
const decided = entries.filter((e) => e.event === "disposition" && e["review"] === "plan" && e.at > first.at).length;
|
|
43
|
+
if (decided < findings) {
|
|
44
|
+
throw new CruzeError("plan-review-open", `the plan review raised ${findings} finding(s) but ${decided} have a disposition; record one for each`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A rethink may re-approve, for the user, a feature or change it didn't edit: one whose own
|
|
50
|
+
* design is unchanged and that is stale only because something it cites changed. A change
|
|
51
|
+
* with a task reopened since its approval goes back to the user.
|
|
52
|
+
*/
|
|
53
|
+
export function requireUnchangedSinceApproval(journal , item , path , status ) {
|
|
54
|
+
if (item === undefined) {
|
|
55
|
+
throw new CruzeError("needs-user", `${path} is a project document, so the user re-approves it; a rethink re-stamps only features and changes`);
|
|
56
|
+
}
|
|
57
|
+
if (status.state === "edited") {
|
|
58
|
+
throw new CruzeError("needs-user", `${item.ref} itself changed since its approval, so the user re-approves it`);
|
|
59
|
+
}
|
|
60
|
+
if (status.state !== "upstream-changed") {
|
|
61
|
+
throw new CruzeError("not-stale", `${item.ref} is ${status.state}; only work whose upstream changed can be re-stamped`);
|
|
62
|
+
}
|
|
63
|
+
const approvedAt = journal.filter((e) => e.event === "approve" && e["artifact"] === path).sort(byTime).at(-1)?.at ?? "";
|
|
64
|
+
const reopened = journal.filter((e) => e.event === "reopen" && e["change"] === item.ref && e.at > approvedAt).map((e) => String(e["task"]));
|
|
65
|
+
if (reopened.length > 0) {
|
|
66
|
+
throw new CruzeError("needs-user", `${item.ref} had ${reopened.join(", ")} reopened since its approval, so the user re-approves it`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/domain/config.js
CHANGED
|
@@ -23,9 +23,25 @@ import { parse as parseYaml } from "yaml";
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/** Which review findings the user decides: only the exceptions, or every one. */
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
27
41
|
|
|
28
42
|
|
|
43
|
+
export const DEFAULT_CHANGE_LIMITS = { minBuilds: 3, maxScenarios: 30, maxTasks: 25 };
|
|
44
|
+
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
|
|
@@ -97,6 +113,14 @@ export function parseConfig(text ) {
|
|
|
97
113
|
const review = asRecord(root["review"]) ?? {};
|
|
98
114
|
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
99
115
|
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
116
|
+
const decide = review["decide"] ?? "exceptions";
|
|
117
|
+
if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
|
|
118
|
+
const limits = asRecord(root["changes"]) ?? {};
|
|
119
|
+
const changes = {
|
|
120
|
+
minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
|
|
121
|
+
maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
|
|
122
|
+
maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
|
|
123
|
+
};
|
|
100
124
|
|
|
101
125
|
const config = {
|
|
102
126
|
version: 1,
|
|
@@ -109,7 +133,11 @@ export function parseConfig(text ) {
|
|
|
109
133
|
check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
|
|
110
134
|
layers,
|
|
111
135
|
commands,
|
|
112
|
-
review:
|
|
136
|
+
review: {
|
|
137
|
+
...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
|
|
138
|
+
decide: decide === "all" ? "all" : "exceptions",
|
|
139
|
+
},
|
|
140
|
+
changes,
|
|
113
141
|
};
|
|
114
142
|
return { config, problems };
|
|
115
143
|
}
|
package/dist/domain/journal.js
CHANGED
|
@@ -21,6 +21,7 @@ export const RECORDED_EVENTS = {
|
|
|
21
21
|
const OPTIONAL_FIELDS = {
|
|
22
22
|
rethink: ["missed_by"],
|
|
23
23
|
review: ["nits"],
|
|
24
|
+
disposition: ["decided"],
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/** The workflow steps and checks that find or miss a problem, so a retro can count them. */
|
|
@@ -30,6 +31,7 @@ export const FIELD_VALUES = {
|
|
|
30
31
|
level: ["task", "change", "feature", "architecture", "vision"],
|
|
31
32
|
kind: ["defect", "discovery"],
|
|
32
33
|
disposition: ["fixed", "waived", "deferred", "rejected"],
|
|
34
|
+
decided: ["agent", "user"],
|
|
33
35
|
review: ["design", "plan", "code", "realign"],
|
|
34
36
|
result: ["accepted", "sent-back"],
|
|
35
37
|
found_by: STEPS,
|
|
@@ -77,6 +79,9 @@ export function recordedEventProblems(event , fields
|
|
|
77
79
|
if (allowed !== undefined && !allowed.includes(value)) problems.push(`${field} must be one of ${allowed.join(", ")}`);
|
|
78
80
|
if (COUNT_FIELDS.includes(field) && !/^\d+$/.test(value)) problems.push(`${field} must be a whole number`);
|
|
79
81
|
}
|
|
82
|
+
if (event === "disposition" && fields["decided"] === "agent" && fields["disposition"] !== "fixed") {
|
|
83
|
+
problems.push("the agent decides only fixes; the user waives, defers or rejects a finding (decided=user)");
|
|
84
|
+
}
|
|
80
85
|
return problems;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Whether the user accepted a change at verify. The latest verification recorded for the change
|
|
6
6
|
* counts. A later approval keeps it only when the change's own design is unchanged, as when it is
|
|
7
|
-
* re-approved because something upstream moved; a changed plan means verifying again.
|
|
7
|
+
* re-approved because something upstream moved; a changed plan means verifying again. Land's
|
|
8
|
+
* re-stamps don't count, since they follow the merge; an approval the agent gave does.
|
|
8
9
|
*/
|
|
9
10
|
export function isVerified(journal , change ) {
|
|
10
11
|
const byTime = (a , b ) => a.at.localeCompare(b.at);
|
|
11
12
|
const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort(byTime).at(-1);
|
|
12
13
|
if (verification?.["result"] !== "accepted") return false;
|
|
13
|
-
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && e["basis"]
|
|
14
|
+
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && !String(e["basis"] ?? "").startsWith("land ")).sort(byTime);
|
|
14
15
|
const verifiedHash = approvals.filter((e) => e.at < verification.at).at(-1)?.["hash"];
|
|
15
16
|
return approvals.filter((e) => e.at > verification.at).every((e) => e["hash"] === verifiedHash);
|
|
16
17
|
}
|
|
@@ -7,6 +7,7 @@ import { findIds, kindOf } from "../ids.js";
|
|
|
7
7
|
import { isPlanned, readScope, readTasks, readTestPlan, } from "../project/plan_parts.js";
|
|
8
8
|
|
|
9
9
|
import { error, warning, } from "./problem.js";
|
|
10
|
+
import { planSizeProblems } from "./size_rules.js";
|
|
10
11
|
|
|
11
12
|
const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
|
|
12
13
|
|
|
@@ -31,6 +32,7 @@ export function changePlanProblems(
|
|
|
31
32
|
problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
|
|
32
33
|
}
|
|
33
34
|
if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
|
|
35
|
+
problems.push(...planSizeProblems(view, path, tasks.length));
|
|
34
36
|
|
|
35
37
|
const proved = new Set(tasks.flatMap((task) => task.proves));
|
|
36
38
|
const taskPaths = tasks.flatMap((task) => task.paths);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DEFAULT_CHANGE_LIMITS, } from "../config.js";
|
|
2
|
+
import { kindOf } from "../ids.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { warning, } from "./problem.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every change pays for its own plan review, code review, verifier run and land, so a change
|
|
9
|
+
* should carry enough work to be worth them, and no more than one review and one run can hold.
|
|
10
|
+
*/
|
|
11
|
+
export function changeLimits(view ) {
|
|
12
|
+
return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
|
|
16
|
+
export function changeRowSizeProblems(view , path , rows , landed ) {
|
|
17
|
+
const limits = changeLimits(view);
|
|
18
|
+
const problems = [];
|
|
19
|
+
for (const row of rows.filter((r) => !landed.has(r.change))) {
|
|
20
|
+
if (rows.length > 1 && row.builds.length < limits.minBuilds) {
|
|
21
|
+
problems.push(warning(path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
|
|
22
|
+
}
|
|
23
|
+
const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
|
|
24
|
+
if (scenarios > limits.maxScenarios) {
|
|
25
|
+
problems.push(warning(path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return problems;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A plan with more tasks than one code review and one verify can hold. */
|
|
32
|
+
export function planSizeProblems(view , path , tasks ) {
|
|
33
|
+
const { maxTasks } = changeLimits(view);
|
|
34
|
+
if (tasks <= maxTasks) return [];
|
|
35
|
+
return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
|
|
36
|
+
}
|
|
@@ -11,6 +11,7 @@ import { deltaProblems } from "./delta_rules.js";
|
|
|
11
11
|
import { malformedIdHeadings, lineOf } from "./living_rules.js";
|
|
12
12
|
import { changePlanProblems } from "./plan_rules.js";
|
|
13
13
|
import { error, warning, } from "./problem.js";
|
|
14
|
+
import { changeRowSizeProblems } from "./size_rules.js";
|
|
14
15
|
|
|
15
16
|
const REQUIRED_SECTIONS = {
|
|
16
17
|
feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
|
|
@@ -71,6 +72,7 @@ function featureScopeProblems(view , feature , delta
|
|
|
71
72
|
if (extra !== undefined) {
|
|
72
73
|
problems.push(warning(feature.path, extra.line, "feature-size", `the feature has ${rows.length} changes, over ${MAX_CHANGES}; split it into features a single design review can hold`));
|
|
73
74
|
}
|
|
75
|
+
problems.push(...changeRowSizeProblems(view, feature.path, rows, landedChanges));
|
|
74
76
|
for (const row of rows) {
|
|
75
77
|
if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
|
|
76
78
|
if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
|
|
@@ -13,6 +13,9 @@ export const RULE_SINCE = {
|
|
|
13
13
|
"validate:outside-module-map": "0.0.2",
|
|
14
14
|
"validate:readme-overview": "0.0.2",
|
|
15
15
|
"validate:feature-size": "0.0.2",
|
|
16
|
+
"validate:change-too-small": "0.0.5",
|
|
17
|
+
"validate:change-too-large": "0.0.5",
|
|
18
|
+
"validate:plan-too-large": "0.0.5",
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
/** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
|
package/package.json
CHANGED
|
@@ -21,11 +21,16 @@ The result is a `feature.md` with a complete architecture delta and an ordered l
|
|
|
21
21
|
|
|
22
22
|
## Split it into changes
|
|
23
23
|
|
|
24
|
-
9. **Slice vertically.** Each change delivers
|
|
24
|
+
9. **Slice vertically.** Each change delivers scenarios end to end, through every layer they need, and can land on its own. Every change pays for its own plan review, code review, verifier run and land, so slice by what is worth that cost, not by the smallest piece that could land. The feature's first change proves the design end to end; later changes widen it in steps of real work.
|
|
25
25
|
10. **Fill the Changes table.** Give each row a number and a slug, such as `01-local-serial`. List the scenario and requirement IDs it `Delivers`, the architecture IDs it `Builds`, any IDs it `Removes`, and the changes it `Depends on`.
|
|
26
26
|
- A change builds an element only when its scenarios prove the whole element. Each delta element is built by exactly one change, so when an element's duties arrive in different changes, split it into elements that each arrive whole, such as frame limits split out of a concurrency rule.
|
|
27
27
|
- `Depends on` names every earlier change that builds something this change's scenarios need.
|
|
28
28
|
11. **Cover everything exactly once.** Every delta ID appears in exactly one change. A module the delta adds, or one still `planned` in the living docs, is built by the first change that writes a file in it. A module already `built` isn't listed. Done when `cruze validate` reports no errors for the feature, and no `not-designed` warning.
|
|
29
|
-
12. **Size each change, and the feature.**
|
|
29
|
+
12. **Size each change, and the feature.** Aim for changes of about 8 to 25 tasks, each one a sitting's worth of review and verify:
|
|
30
|
+
- **Merge** a change that builds fewer than 3 elements into the change it continues, when they share adapters or screens, or form a chain where each depends on the one before. Several small changes to the same screens are one change. `cruze validate` warns `change-too-small`.
|
|
31
|
+
- **Split** a change that delivers more than 30 scenarios, or would need more than 25 tasks, or touches more than one context's domain. One verifier run and one code review can't hold more. `cruze validate` warns `change-too-large`, and `plan-too-large` once a plan passes 25 tasks.
|
|
32
|
+
- A feature of more than 6 changes is more than one design review can hold: split it into features that each serve part of its goals, and run `cruze features add` for the later ones. `cruze validate` warns `feature-size` until it is split.
|
|
33
|
+
|
|
34
|
+
The thresholds are the defaults under `changes:` in `.cruze/config.yaml`; a project can tune them.
|
|
30
35
|
|
|
31
36
|
Then continue with the review and walkthrough steps of the skill. When the user approves, run `cruze approve <feature>` and quote its result. The command takes the feature's full ID, such as `2026-09-25-open-console`, or its slug when no other active item shares it.
|
package/skills/formats/SKILL.md
CHANGED
|
@@ -66,7 +66,7 @@ The CLI owns some content so that routine bookkeeping never changes a design:
|
|
|
66
66
|
- A block between `<!-- cruze:managed -->` and `<!-- /cruze:managed -->` is written only by the CLI.
|
|
67
67
|
- A `- Status: planned` or `- Status: built` item in an element body is written only by the CLI. Elements that have code (`ENT`, `UC`, `PORT`, `ADP`, `FLOW`, `MOD`) and scenarios (`SCN`) carry one.
|
|
68
68
|
- Both are excluded from every hash. Read them freely; change them only through `cruze` commands:
|
|
69
|
-
- `cruze approve` adds missing statuses to the architecture, sets built elements back to planned with `--replan`, and binds a change to its branch.
|
|
69
|
+
- `cruze approve` adds missing statuses to the architecture, sets built elements back to planned with `--replan`, and binds a change to its branch. With `--by-agent` it approves a change whose plan review closed, and with `--restamp-unchanged` it re-stamps work a rethink left unchanged, recording that the agent gave the approval.
|
|
70
70
|
- `cruze task` records progress, and `cruze features` edits the feature map.
|
|
71
71
|
- `cruze land` merges work, marks it built and fills the Commands and Layout sections of `AGENTS.md`.
|
|
72
72
|
- `cruze roadmap prune` and `cruze abandon` update the roadmap's status.
|
|
@@ -27,6 +27,12 @@ commands:
|
|
|
27
27
|
|
|
28
28
|
review:
|
|
29
29
|
command: ["claude", "-p", "--allowedTools", "Read Grep Glob Bash(git diff:*) Bash(git log:*) Bash(git show:*) Bash(cruze:*) Bash(cargo:*)"]
|
|
30
|
+
decide: exceptions
|
|
31
|
+
|
|
32
|
+
changes:
|
|
33
|
+
min_builds: 3
|
|
34
|
+
max_scenarios: 30
|
|
35
|
+
max_tasks: 25
|
|
30
36
|
|
|
31
37
|
layers:
|
|
32
38
|
- name: inventory-domain
|
|
@@ -52,6 +58,10 @@ layers:
|
|
|
52
58
|
| `layers` | Each layer has a `name`, the `paths` it covers, and `may_import`, the layers it may depend on. |
|
|
53
59
|
| `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
60
|
| `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. |
|
|
61
|
+
| `review.decide` | Which review findings the user decides. `exceptions`, the default: only findings whose fix changes behaviour, a designed port or contract, or another decision the user owns, and any the agent wouldn't simply fix as proposed. The agent fixes the rest and lists them. `all`: the user decides every finding. |
|
|
62
|
+
| `changes.min_builds` | A feature's change that builds fewer elements than this gets a `change-too-small` warning: merge it into the change it continues. Default `3`. |
|
|
63
|
+
| `changes.max_scenarios` | A change that delivers more scenarios than this gets a `change-too-large` warning: split it. Default `30`. |
|
|
64
|
+
| `changes.max_tasks` | A plan with more tasks than this gets a `plan-too-large` warning. Default `25`. |
|
|
55
65
|
|
|
56
66
|
Rules for layers:
|
|
57
67
|
|
|
@@ -35,7 +35,7 @@ Templates: [../templates/feature.md](../templates/feature.md), [../templates/cha
|
|
|
35
35
|
|
|
36
36
|
A standalone change adds `## Intent`, `## Adopt or build`, `## Spec delta` and `## Architecture delta` before its `## Scope`, in the same format as a feature's. Its scope covers every ID in its own deltas, plus any `planned` elements from the living docs that it builds.
|
|
37
37
|
|
|
38
|
-
A feature is designed in two steps. Feature-scope `envision` writes the intent, settled decisions and spec delta, and leaves `## Architecture delta` as `None.` and the Changes table empty. `architect` writes the architecture delta and the changes. Until the Changes table has a row, `cruze validate` reports the feature as `not-designed` (a warning), skips the coverage rules, and `cruze approve` refuses it. A feature of more than 6 changes gets a `feature-size` warning: split it, because one design review can't hold it.
|
|
38
|
+
A feature is designed in two steps. Feature-scope `envision` writes the intent, settled decisions and spec delta, and leaves `## Architecture delta` as `None.` and the Changes table empty. `architect` writes the architecture delta and the changes. Until the Changes table has a row, `cruze validate` reports the feature as `not-designed` (a warning), skips the coverage rules, and `cruze approve` refuses it. A feature of more than 6 changes gets a `feature-size` warning: split it, because one design review can't hold it. A change outside the size band in `changes:` of `.cruze/config.yaml` gets `change-too-small` (it builds fewer than 3 elements, in a feature of several changes) or `change-too-large` (it delivers more than 30 scenarios), and a plan of more than 25 tasks gets `plan-too-large`.
|
|
39
39
|
|
|
40
40
|
`architect` writes a standalone change's design sections and its scope, and leaves the test plan table empty and the tasks section blank. Until `plan` fills them, `cruze validate` reports the change as `not-planned` (a warning), skips the task and test plan rules, and `cruze approve` refuses it.
|
|
41
41
|
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cruze-plan
|
|
3
|
-
description: Turns the next change into a test plan and ordered tasks with file targets, reviews the plan in a fresh context, and
|
|
3
|
+
description: Turns the next change into a test plan and ordered tasks with file targets, reviews the plan in a fresh context, and approves it on the branch that will build it once the review closes, leaving you to object or go straight to build.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Plan
|
|
@@ -26,7 +26,7 @@ Plan turns one designed change into work an implementer can do without guessing:
|
|
|
26
26
|
10. **Record decisions and risks.** Put task-level choices you made under `## Settled decisions`. Under `## Risks`, name the riskiest task and how its test catches the failure. Remove every template guide.
|
|
27
27
|
11. **Validate.** Run `cruze validate`. Done when it reports no errors for the change.
|
|
28
28
|
12. **Review the plan.** Commit the plan, then run the review in `.agents/skills/cruze-roles/SKILL.md` with the design reviewer and its plan rubric, `review=plan`, and `--item <ref>`.
|
|
29
|
-
13. **Approve.**
|
|
29
|
+
13. **Approve.** Run `cruze approve <ref> --by-agent` on this branch and quote its result. The plan carries no design of its own, so once its review has closed, with no blocker open and a disposition for every finding, you approve it for the user. The approval binds the change to the branch. When it refuses with `plan-review-open`, do what it names. A blocker still open after round 2 goes to the user, and once they decide it, they approve the plan themselves with `cruze approve <ref>`.
|
|
30
30
|
14. **Commit** with a message such as `docs: plan <change>`.
|
|
31
31
|
|
|
32
32
|
## When the plan needs design
|
|
@@ -49,4 +49,4 @@ The walking-skeleton change also makes CI and the project's tooling real. Its pl
|
|
|
49
49
|
|
|
50
50
|
## Next step
|
|
51
51
|
|
|
52
|
-
`build`, on this branch.
|
|
52
|
+
Show the user the test plan table, the tasks in order and the risks, and say the plan is approved. They can object, and you amend the plan, run its review on the diff, and approve again. Otherwise the next step is `build`, on this branch.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Realign notes: 0.0.5
|
|
2
|
+
|
|
3
|
+
## New work only
|
|
4
|
+
|
|
5
|
+
### review-exceptions: The user decides only the review findings that are theirs
|
|
6
|
+
- Applies to: new work only
|
|
7
|
+
- Rule: `.agents/skills/cruze-roles/SKILL.md`, "Running a review", round 1, steps 3 to 5
|
|
8
|
+
|
|
9
|
+
### plan-approved-by-agent: A plan is approved once its review closes, without stopping for the user
|
|
10
|
+
- Applies to: new work only
|
|
11
|
+
- Rule: `.agents/skills/cruze-plan/SKILL.md`, "Steps", step 13
|
|
12
|
+
|
|
13
|
+
### rethink-restamps: A rethink re-stamps the work it didn't change
|
|
14
|
+
- Applies to: new work only
|
|
15
|
+
- Rule: `.agents/skills/cruze-rethink/SKILL.md`, "Steps", step 6
|
|
16
|
+
|
|
17
|
+
### change-too-small: A feature's change builds at least 3 elements
|
|
18
|
+
- Applies to: new work only
|
|
19
|
+
- Rule: `.agents/skills/cruze-architect/feature.md`, step 12
|
|
20
|
+
- Detect: validate:change-too-small
|
|
21
|
+
|
|
22
|
+
### change-too-large: A change delivers at most 30 scenarios
|
|
23
|
+
- Applies to: new work only
|
|
24
|
+
- Rule: `.agents/skills/cruze-architect/feature.md`, step 12
|
|
25
|
+
- Detect: validate:change-too-large
|
|
26
|
+
|
|
27
|
+
### plan-too-large: A plan has at most 25 tasks
|
|
28
|
+
- Applies to: new work only
|
|
29
|
+
- Rule: `.agents/skills/cruze-architect/feature.md`, step 12
|
|
30
|
+
- Detect: validate:plan-too-large
|
|
31
|
+
|
|
32
|
+
### concerns-per-size: Reviewers list concerns in proportion to the work
|
|
33
|
+
- Applies to: new work only
|
|
34
|
+
- Rule: `.agents/skills/cruze-roles/code-reviewer.md`, "Findings"
|
|
35
|
+
|
|
36
|
+
### reverify-changed-plan: A plan changed after verify needs verifying again, whoever approved it
|
|
37
|
+
- Applies to: new work only
|
|
38
|
+
- Rule: `.agents/skills/cruze-verify/SKILL.md`
|
package/skills/realign/notes.md
CHANGED
package/skills/rethink/SKILL.md
CHANGED
|
@@ -18,7 +18,11 @@ Finding a design flaw mid-build is information, not failure. Rethink amends the
|
|
|
18
18
|
3. **Load the standards.** Read `.agents/skills/cruze-grilling/SKILL.md`. At feature or architecture level, also read `.agents/skills/cruze-hexagonal-design/SKILL.md`. Also read the format reference for the document you will amend: `.agents/skills/cruze-formats/reference/architecture.md`, `specs.md`, `work-items.md` or `durable-docs.md`.
|
|
19
19
|
4. **Amend that level,** following the level's section in [levels.md](levels.md). Settle any decision the amendment needs with the user, and record it as that level records decisions.
|
|
20
20
|
5. **Keep completed work.** A ticked task stays ticked unless the amendment changes its owner, its files, or what it proves. Reopen each task that changed with `cruze task reopen T<n> "<what changed>"`. Its commits stay in history, and build redoes the task.
|
|
21
|
-
6. **Re-approve what went stale.** Run `cruze status` and work from the top down
|
|
21
|
+
6. **Re-approve what went stale.** Run `cruze status` and work from the top down. The amended document comes first, and the user approves it as its level says. Then take each feature or change it lists as `upstream changed`, whose own text you didn't edit:
|
|
22
|
+
- Read the diff of the elements it names, and check the work still holds against them. When it doesn't, amend it too, and the user approves it as an amended document.
|
|
23
|
+
- When it holds, re-stamp it with `cruze approve <ref> --restamp-unchanged`. The CLI refuses work whose own text changed, a project document, and a change with a task reopened since its approval; the user re-approves those.
|
|
24
|
+
|
|
25
|
+
Tell the user, in one line each, what you re-stamped and why it still holds. They can object, and that document goes to them. A change's verification carries over when its own design didn't change.
|
|
22
26
|
7. **Record the rethink.** Draft the entry and show it to the user in one line. When they confirm, run:
|
|
23
27
|
|
|
24
28
|
```sh
|
package/skills/rethink/levels.md
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
1. Edit the change's `## Test plan` or `## Tasks` so they are right. The scope doesn't change at this level; a scope change is a feature-level rethink.
|
|
6
6
|
2. Run `cruze validate`. Done when it reports no errors for the change.
|
|
7
7
|
3. Run the plan review from `.agents/skills/cruze-roles/SKILL.md` on the diff alone, with the design reviewer's plan rubric and `review=plan`.
|
|
8
|
-
4. Re-approve with `cruze approve <change
|
|
8
|
+
4. Re-approve with `cruze approve <change> --by-agent` once the review has closed, as plan does.
|
|
9
9
|
|
|
10
10
|
## Feature
|
|
11
11
|
|
|
12
12
|
1. Edit the feature's `## Spec delta`, `## Architecture delta` or `## Changes`. For a standalone change, edit its own deltas and scope. Write complete bodies, as `architect` does, and keep the scope rules.
|
|
13
13
|
2. Run `cruze validate`. Done when it reports no errors for the feature or its changes.
|
|
14
14
|
3. Run the design review from the roles skill on the diff alone, with `review=design`. Walk the user through the changed parts, following `.agents/skills/cruze-architect/walkthrough.md`.
|
|
15
|
-
4. Re-approve the feature. For each of its changes that shows as stale,
|
|
15
|
+
4. Re-approve the feature. For each of its changes that shows as stale, check it against the diff. When the amendment touches the change's scope, test plan or tasks, update them to match, reopen touched tasks, run the plan review on the diff, and re-approve it with `cruze approve <change> --by-agent`. When it doesn't, re-stamp the change as the skill's step 6 describes.
|
|
16
16
|
5. When a change hasn't been created yet, only the feature's Changes row changes; `plan` picks it up.
|
|
17
17
|
|
|
18
18
|
## Architecture
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- **A prefactor**, otherwise. From an up-to-date main, run `architect "<prefactor>"`, which designs it as a standalone change, then `plan`, `build`, `verify` and `land` it before the current change continues.
|
|
27
27
|
6. Run `cruze status`. Every document citing a changed element shows `upstream changed`:
|
|
28
28
|
- When the document's own delta changes that element, rewrite the delta so it keeps your amendment. `cruze approve` refuses it with `rebase-required` until you do. Then approve it with `--rebase <ID>`.
|
|
29
|
-
- Otherwise,
|
|
29
|
+
- Otherwise, check the document against the element's diff, and re-stamp it or amend it as the skill's step 6 describes.
|
|
30
30
|
|
|
31
31
|
## Vision
|
|
32
32
|
|
package/skills/roles/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cruze-roles
|
|
3
|
-
description: Prompts for Cruze's fresh-context roles (the design reviewer, code reviewer, verifier and researcher), how to run one, and how to take a review through its two rounds to a disposition for every finding. Use when a Cruze workflow step says to run a role, a review or research in a fresh context.
|
|
3
|
+
description: Prompts for Cruze's fresh-context roles (the design reviewer, code reviewer, verifier and researcher), how to run one, and how to take a review through its two rounds to a disposition for every finding, with the user deciding only the exceptions. Use when a Cruze workflow step says to run a role, a review or research in a fresh context.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Roles
|
|
@@ -30,16 +30,26 @@ A review has at most two rounds. Only blockers can force the second one.
|
|
|
30
30
|
|
|
31
31
|
1. Commit the documents under review (`docs: <artifact> for design review`), so the fixes in round 2 have a diff. Then run the reviewer on the complete artifact.
|
|
32
32
|
2. Record the round: `cruze journal add review --set review=<design|plan|code> --set round=1 --set blockers=<n> --set concerns=<n> --set nits=<n>`, with the counts from the report's last line, adding `--item <ref>` for a feature or change.
|
|
33
|
-
3.
|
|
34
|
-
-
|
|
33
|
+
3. **Sort the findings.** A finding is an **exception**, which the user decides, when any of these holds:
|
|
34
|
+
- Its fix changes behaviour: it adds, removes or rewrites a requirement or scenario, or changes what a person sees or does beyond what the spec says. Making the code or the plan meet a scenario as written is not a change of behaviour.
|
|
35
|
+
- Its fix changes a port, a port's `Operations` or a public contract that the approved design names.
|
|
36
|
+
- Its fix changes another decision the grilling skill leaves to the user: a context, dependency direction or a dependency.
|
|
37
|
+
- You recommend anything but `fixed`: waiving, deferring or rejecting it, or a rethink because it needs a design change.
|
|
38
|
+
- You would fix it differently from the reviewer's proposed fix.
|
|
39
|
+
|
|
40
|
+
Every other finding is yours, blockers included: you fix it as the reviewer proposes. When `review.decide` in `.cruze/config.yaml` is `all`, every finding is an exception.
|
|
41
|
+
4. **Present them in one message.** First each exception, with your recommended disposition and why:
|
|
42
|
+
- `fixed`: you will change the work as the finding proposes.
|
|
35
43
|
- `waived`: the finding is right, but the user accepts the risk.
|
|
36
44
|
- `deferred`: it belongs to later work. When it is future scope, add it with `cruze features add <slug> --summary <text> --goals <ids>`.
|
|
37
45
|
- `rejected`: the finding is wrong, with the reason.
|
|
38
46
|
|
|
39
47
|
A finding whose fix would change an approved element, such as one marked `Needs design change`, is never `deferred` or `waived`. Recommend `rethink` at that element's level now. Once the rethink is done and the code follows it, record the finding as `fixed`.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
Then one line for each finding you are fixing yourself. When there are no exceptions, say what you are fixing and carry on without waiting. The user can object to any line until the work is approved, and that finding becomes an exception: revert its fix and let them decide it.
|
|
50
|
+
5. **Record every disposition:** `cruze journal add disposition --set finding="<rubric item>: <summary>" --set disposition=<fixed|waived|deferred|rejected> --set reason="<reason>" --set review=<design|plan|code> --set decided=<agent|user>`, with the same `--item`. Use `decided=user` for each exception, once the user decides it, and `decided=agent` for each finding you fixed yourself. The CLI refuses `decided=agent` for anything but `fixed`. A recorded disposition is never raised again.
|
|
51
|
+
6. Apply every `fixed` change, then run `cruze validate`. Done when it reports no errors.
|
|
52
|
+
7. When a finding could be caught by a tool, such as a layering, naming or size rule, tell the user which `cruze check` rule or linter setting would catch it next time.
|
|
43
53
|
|
|
44
54
|
### Round 2
|
|
45
55
|
|
|
@@ -49,7 +59,7 @@ Run round 2 only when a round-1 blocker was fixed.
|
|
|
49
59
|
2. Record the round with `round=2`.
|
|
50
60
|
3. Take any blocker still open to the user, who fixes it now, waives it or starts a rethink. There is no round 3.
|
|
51
61
|
|
|
52
|
-
Done when every finding has a journaled disposition and
|
|
62
|
+
Done when every finding has a journaled disposition, and every exception, and every blocker left open in round 2, has the user's decision.
|
|
53
63
|
|
|
54
64
|
## The settled ledger
|
|
55
65
|
|
|
@@ -46,7 +46,7 @@ Each finding has this shape:
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
- A **blocker** means a scenario fails or is untested, a protected test was weakened, a layering rule is broken, an unapproved dependency was added, or an adapter can't honour its port's `Operations`. A finding that needs a design change is a blocker. Everything else is a **concern**.
|
|
49
|
-
- List every blocker. List at most 5 concerns, the most important first. Count the rest as nits without listing them.
|
|
49
|
+
- List every blocker. List at most 5 concerns, or one per 5 tasks of the change when that is more, the most important first. Count the rest as nits without listing them. Report only what would go wrong: no concerns is a valid report, and a small change usually has few.
|
|
50
50
|
- Drop any finding that contradicts the settled ledger. The only exception is a blocker with new evidence, which you mark `New evidence:` and explain.
|
|
51
51
|
- When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
|
|
52
52
|
- In round 2, report only whether each round-1 blocker is fixed, as `fixed` or `open` with the reason. Raise nothing new.
|
|
@@ -29,7 +29,7 @@ Review against these items and nothing else. Each finding names one.
|
|
|
29
29
|
- each use case's `Output` and `Errors` against the `FLOW`s that serve it and the `XC` rules that apply to it;
|
|
30
30
|
- each adapter's duties against the port it implements and the `XC` rules it follows;
|
|
31
31
|
- each guarantee, such as "always fits" or "never returns", against the limits and failures that bound it.
|
|
32
|
-
9. **Slicing.** Each change is a vertical slice that delivers scenarios end to end and can land on its own. The order respects `Depends on`, and the first change
|
|
32
|
+
9. **Slicing.** Each change is a vertical slice that delivers scenarios end to end and can land on its own. The order respects `Depends on`, and the first change proves the design end to end. Each change carries enough work to be worth its own plan review, code review and verify: changes that build one or two elements each on the same adapters or screens, in a chain, are one change split too fine. A change that one verifier run can't hold is split too coarse. A change `Builds` an element only when its delivered scenarios prove the whole body; an element whose duties arrive in several changes is split into elements that each arrive whole. `Depends on` names every earlier change that builds something this change's scenarios need. At project scope, the roadmap starts with the walking skeleton, and every goal in the release is covered.
|
|
33
33
|
10. **Ambiguity.** Name each point where a fresh implementer could reasonably build something different from what the user expects.
|
|
34
34
|
|
|
35
35
|
## Plan rubric
|
|
@@ -56,7 +56,7 @@ Each finding has this shape:
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
- A **blocker** means building from the design as written would produce wrong behaviour, a layering violation or an untestable scenario. Two elements that contradict each other are always a blocker, because an implementer has to pick one. Everything else is a **concern**.
|
|
59
|
-
- List every blocker. List at most 5 concerns, the most important first. Count the rest as nits without listing them.
|
|
59
|
+
- List every blocker. List at most 5 concerns, or for a plan one per 5 tasks when that is more, the most important first. Count the rest as nits without listing them. Report only what would go wrong: no concerns is a valid report, and a small artifact usually has few.
|
|
60
60
|
- Drop any finding that contradicts the settled ledger. The only exception is a blocker with new evidence, which you mark `New evidence:` and explain.
|
|
61
61
|
- When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
|
|
62
62
|
- In round 2, report only whether each round-1 blocker is fixed, as `fixed` or `open` with the reason. Raise nothing new.
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ Verify answers one question: does this change work when someone uses it? Green t
|
|
|
13
13
|
2. **Run the mechanical checks.** Run the project's format, lint, build and test commands from `commands:` in `.cruze/config.yaml`, then `cruze validate`, `cruze check --ci` and `cruze trace --change <ref>`. Quote each result. When one fails, fix the code in a commit and run them again. When a fix needs design, stop and offer `rethink`.
|
|
14
14
|
3. **Check the dependencies.** Follow "Checking" in `.agents/skills/cruze-dependency-approval/SKILL.md` against the base commit. An unrecorded dependency is a blocker.
|
|
15
15
|
4. **Run the verifier.** Run the verifier role from `.agents/skills/cruze-roles/SKILL.md` for the change. It must be able to build and run the system. When it runs through `cruze review`, check that `review.command` in `.cruze/config.yaml` allows the project's build and run commands, and add them with the user's agreement. Done when you have its report, with a result for every delivered scenario.
|
|
16
|
-
5. **Review the code.** Run the review in the roles skill with the code reviewer, `review=code`, `--item <ref>` and the base commit. Fix each finding
|
|
16
|
+
5. **Review the code.** Run the review in the roles skill with the code reviewer, `review=code`, `--item <ref>` and the base commit. Fix each finding whose disposition is `fixed` in a commit, then check the fixes in round 2.
|
|
17
17
|
6. **Write the manual test script** as `manual-test.md` in the change folder, following [manual-test.md](manual-test.md). Commit it.
|
|
18
18
|
7. **Report.** Show the user four things:
|
|
19
19
|
- The mechanical checks, one line each.
|
|
@@ -13,8 +13,12 @@ export async function runValidate(context: CliContext): Promise<CommandResult> {
|
|
|
13
13
|
|
|
14
14
|
export async function runApprove(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
|
|
15
15
|
requireArgs(args, ["document"]);
|
|
16
|
-
|
|
16
|
+
if (options.byAgent && options.restampUnchanged) throw new UsageError("--by-agent and --restamp-unchanged are alternatives");
|
|
17
|
+
const agent = options.byAgent ? "plan-review" : options.restampUnchanged ? "rethink" : undefined;
|
|
18
|
+
const report = await approveArtifact(context, args[0] ?? "", { replan: options.replan, rebase: options.rebase, ...(agent === undefined ? {} : { agent }) });
|
|
17
19
|
const lines = [`Approved ${report.artifact} (${report.hash.slice(0, 19)}), pinned to ${report.upstream.length} upstream element(s).`];
|
|
20
|
+
if (report.basis === "plan-review") lines.push("Approved for the user: its plan review closed with no blocker open.");
|
|
21
|
+
if (report.restamped !== undefined) lines.push(`Re-stamped for the user: its own design is unchanged, and only ${report.restamped.join(", ")} changed.`);
|
|
18
22
|
if (report.stamped.length > 0) lines.push(`Marked ${report.stamped.length} element(s) planned.`);
|
|
19
23
|
if (report.bound !== undefined) lines.push(`Bound the change to branch ${report.bound}.`);
|
|
20
24
|
if (report.replanned.length > 0) lines.push(`Set back to planned: ${report.replanned.join(", ")}.`);
|
|
@@ -35,8 +35,11 @@ export async function runStatus(context: CliContext, _args: string[], options: O
|
|
|
35
35
|
return { json: report, human: lines.join("\n") };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const BASIS: Record<string, string> = { "plan-review": " (by the agent, after its plan review)", rethink: " (re-stamped by the agent after a rethink)" };
|
|
39
|
+
|
|
38
40
|
function describe(label: string, status: ApprovalStatus): string {
|
|
39
|
-
const
|
|
41
|
+
const byAgent = status.state === "approved" ? BASIS[status.record?.basis ?? ""] ?? "" : "";
|
|
42
|
+
const detail = status.changed.length > 0 ? ` (${status.changed.join(", ")})` : status.unjournaled === true ? " (no journal entry)" : byAgent;
|
|
40
43
|
return `${label}: ${status.state}${detail}`;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -22,7 +22,11 @@ export const COMMANDS: Record<string, Command> = {
|
|
|
22
22
|
validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
|
|
23
23
|
status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
|
|
24
24
|
next: { handler: runNext, options: [], usage: "next the step to run next on this branch, and why" },
|
|
25
|
-
approve: {
|
|
25
|
+
approve: {
|
|
26
|
+
handler: runApprove,
|
|
27
|
+
options: ["replan", "rebase", "by-agent", "restamp-unchanged"],
|
|
28
|
+
usage: "approve <vision|architecture|roadmap|ref|path> [--replan <ID>...] [--rebase <ID>...] [--by-agent|--restamp-unchanged] record an approval of a document",
|
|
29
|
+
},
|
|
26
30
|
new: {
|
|
27
31
|
handler: runNew,
|
|
28
32
|
options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title"] },
|
|
@@ -52,6 +52,8 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
52
52
|
rebase: { type: "string", multiple: true, default: [] },
|
|
53
53
|
base: { type: "string" },
|
|
54
54
|
blockers: { type: "string" },
|
|
55
|
+
"by-agent": { type: "boolean", default: false },
|
|
56
|
+
"restamp-unchanged": { type: "boolean", default: false },
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
} catch (error) {
|
|
@@ -74,6 +76,8 @@ export function parseCommand(argv: string[]): ParsedCommand {
|
|
|
74
76
|
full: values.full,
|
|
75
77
|
replan: values.replan,
|
|
76
78
|
rebase: values.rebase,
|
|
79
|
+
byAgent: values["by-agent"],
|
|
80
|
+
restampUnchanged: values["restamp-unchanged"],
|
|
77
81
|
...optional("name", values.name),
|
|
78
82
|
...optional("title", values.title),
|
|
79
83
|
...optional("feature", values.feature),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requirePlanReviewClosed, requireUnchangedSinceApproval, type AgentBasis } from "../../domain/approvals/agent_approval.ts";
|
|
1
2
|
import { evaluateApproval } from "../../domain/approvals/evaluate.ts";
|
|
2
3
|
import { fingerprint, isApprovable } from "../../domain/approvals/fingerprint.ts";
|
|
3
4
|
import { latestApproval, serializeApprovals, withApproval, type ApprovalRecord } from "../../domain/approvals/records.ts";
|
|
@@ -29,6 +30,10 @@ export interface ApproveReport {
|
|
|
29
30
|
replanned: string[];
|
|
30
31
|
/** Elements whose current living text this feature accepted after a land conflict. */
|
|
31
32
|
rebased: string[];
|
|
33
|
+
/** Why the agent gave this approval for the user, when it did. */
|
|
34
|
+
basis?: AgentBasis;
|
|
35
|
+
/** For a re-stamp: the upstream IDs or documents that had changed. */
|
|
36
|
+
restamped?: string[];
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface ApproveOptions {
|
|
@@ -36,6 +41,8 @@ export interface ApproveOptions {
|
|
|
36
41
|
replan?: string[];
|
|
37
42
|
/** Elements this work's delta changes whose living text changed since its last approval; the approval accepts the current text. */
|
|
38
43
|
rebase?: string[];
|
|
44
|
+
/** Approve for the user: a change whose plan review closed, or work a rethink left unchanged. */
|
|
45
|
+
agent?: AgentBasis;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/**
|
|
@@ -53,6 +60,9 @@ export async function approveArtifact(deps: ProjectDeps, ref: string, options: A
|
|
|
53
60
|
if (item !== undefined && item.kind !== "feature" && !isPlanned(item.doc)) {
|
|
54
61
|
throw new CruzeError("not-planned", `${path} has no test plan or tasks yet; plan it before approving`);
|
|
55
62
|
}
|
|
63
|
+
const before = evaluateApproval(view, path);
|
|
64
|
+
if (options.agent === "plan-review") requirePlanReviewClosed(view.journal, item ?? notWork(path));
|
|
65
|
+
if (options.agent === "rethink") requireUnchangedSinceApproval(view.journal, item, path, before);
|
|
56
66
|
|
|
57
67
|
const edits = new Map<string, string>();
|
|
58
68
|
let text = view.snapshot.get(path) ?? "";
|
|
@@ -103,12 +113,23 @@ export async function approveArtifact(deps: ProjectDeps, ref: string, options: A
|
|
|
103
113
|
upstream,
|
|
104
114
|
approvedAt: deps.clock.now().toISOString(),
|
|
105
115
|
by: (await deps.repository.userName()) ?? "unknown",
|
|
116
|
+
...(options.agent === undefined ? {} : { basis: options.agent }),
|
|
106
117
|
});
|
|
107
118
|
for (const [changed, content] of edits) await deps.files.writeText(changed, content);
|
|
108
119
|
await deps.files.writeText(approvalsPath(folder), serializeApprovals(approvals));
|
|
109
|
-
|
|
120
|
+
const restamped = options.agent === "rethink" ? before.changed : undefined;
|
|
121
|
+
await appendJournal(deps, folder, "approve", {
|
|
122
|
+
artifact: path,
|
|
123
|
+
hash,
|
|
124
|
+
...(replanned.length > 0 ? { replanned } : {}),
|
|
125
|
+
...(rebased.length > 0 ? { rebased } : {}),
|
|
126
|
+
...(options.agent === undefined ? {} : { basis: options.agent }),
|
|
127
|
+
...(restamped === undefined ? {} : { restamped }),
|
|
128
|
+
});
|
|
110
129
|
const report: ApproveReport = { artifact: path, hash, upstream: Object.keys(upstream), stamped, replanned, rebased };
|
|
111
130
|
if (bound !== undefined) report.bound = bound;
|
|
131
|
+
if (options.agent !== undefined) report.basis = options.agent;
|
|
132
|
+
if (restamped !== undefined) report.restamped = restamped;
|
|
112
133
|
return report;
|
|
113
134
|
}
|
|
114
135
|
|
|
@@ -133,6 +154,10 @@ function requireRebased(view: ProjectView, deltaIds: Set<string>, previous: Appr
|
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
function notWork(path: string): never {
|
|
158
|
+
throw new CruzeError("needs-user", `${path} is a project document, so the user approves it`);
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
/** A change needs its feature approved; a feature or standalone change needs the architecture approved. */
|
|
137
162
|
function requireUpstreamApproved(view: ProjectView, path: string): void {
|
|
138
163
|
const item = view.items.find((i) => i.path === path);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CruzeError } from "../cruze_error.ts";
|
|
2
|
+
import type { JournalEntry } from "../journal.ts";
|
|
3
|
+
import type { WorkItem } from "../project/work_items.ts";
|
|
4
|
+
import type { ApprovalStatus } from "./evaluate.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Why an approval was given without the user: a plan whose review closed, or work that a
|
|
8
|
+
* rethink left unchanged while something it cites moved. The user can still object, and
|
|
9
|
+
* editing the document makes the approval stale as usual.
|
|
10
|
+
*/
|
|
11
|
+
export type AgentBasis = "plan-review" | "rethink";
|
|
12
|
+
|
|
13
|
+
const byTime = (a: JournalEntry, b: JournalEntry): number => a.at.localeCompare(b.at);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A feature's change may be approved by the agent once its plan review has closed: a review
|
|
17
|
+
* recorded since the change was last approved, no blocker left open in its last round, and a
|
|
18
|
+
* disposition for every finding of its first round. The feature's design, and a standalone
|
|
19
|
+
* change's, stay the user's to approve.
|
|
20
|
+
*/
|
|
21
|
+
export function requirePlanReviewClosed(journal: JournalEntry[], item: WorkItem): void {
|
|
22
|
+
if (item.kind !== "change") {
|
|
23
|
+
throw new CruzeError("needs-user", `${item.ref} carries design, so the user approves it; only a feature's change can be approved after its plan review`);
|
|
24
|
+
}
|
|
25
|
+
const entries = journal.filter((e) => e["item"] === item.ref || e["artifact"] === item.path).sort(byTime);
|
|
26
|
+
const lastApproval = entries.filter((e) => e.event === "approve").at(-1);
|
|
27
|
+
const reviews = entries.filter((e) => e.event === "review" && e["review"] === "plan" && (lastApproval === undefined || e.at > lastApproval.at));
|
|
28
|
+
const last = reviews.at(-1);
|
|
29
|
+
if (last === undefined) {
|
|
30
|
+
const since = lastApproval === undefined ? "" : " since the change was last approved";
|
|
31
|
+
throw new CruzeError("plan-review-open", `no plan review is recorded for ${item.ref}${since}; run the plan review first`);
|
|
32
|
+
}
|
|
33
|
+
const blockers = Number(last["blockers"] ?? 0);
|
|
34
|
+
if (blockers > 0 && Number(last["round"]) === 1) {
|
|
35
|
+
throw new CruzeError("plan-review-open", `the plan review's round 1 found ${blockers} blocker(s); check their fixes in round 2`);
|
|
36
|
+
}
|
|
37
|
+
if (blockers > 0) {
|
|
38
|
+
throw new CruzeError("plan-review-open", `round 2 left ${blockers} blocker(s) open; the user decides them and approves the plan`);
|
|
39
|
+
}
|
|
40
|
+
const first = reviews.filter((e) => Number(e["round"]) === 1).at(-1) ?? last;
|
|
41
|
+
const findings = Number(first["blockers"] ?? 0) + Number(first["concerns"] ?? 0);
|
|
42
|
+
const decided = entries.filter((e) => e.event === "disposition" && e["review"] === "plan" && e.at > first.at).length;
|
|
43
|
+
if (decided < findings) {
|
|
44
|
+
throw new CruzeError("plan-review-open", `the plan review raised ${findings} finding(s) but ${decided} have a disposition; record one for each`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A rethink may re-approve, for the user, a feature or change it didn't edit: one whose own
|
|
50
|
+
* design is unchanged and that is stale only because something it cites changed. A change
|
|
51
|
+
* with a task reopened since its approval goes back to the user.
|
|
52
|
+
*/
|
|
53
|
+
export function requireUnchangedSinceApproval(journal: JournalEntry[], item: WorkItem | undefined, path: string, status: ApprovalStatus): void {
|
|
54
|
+
if (item === undefined) {
|
|
55
|
+
throw new CruzeError("needs-user", `${path} is a project document, so the user re-approves it; a rethink re-stamps only features and changes`);
|
|
56
|
+
}
|
|
57
|
+
if (status.state === "edited") {
|
|
58
|
+
throw new CruzeError("needs-user", `${item.ref} itself changed since its approval, so the user re-approves it`);
|
|
59
|
+
}
|
|
60
|
+
if (status.state !== "upstream-changed") {
|
|
61
|
+
throw new CruzeError("not-stale", `${item.ref} is ${status.state}; only work whose upstream changed can be re-stamped`);
|
|
62
|
+
}
|
|
63
|
+
const approvedAt = journal.filter((e) => e.event === "approve" && e["artifact"] === path).sort(byTime).at(-1)?.at ?? "";
|
|
64
|
+
const reopened = journal.filter((e) => e.event === "reopen" && e["change"] === item.ref && e.at > approvedAt).map((e) => String(e["task"]));
|
|
65
|
+
if (reopened.length > 0) {
|
|
66
|
+
throw new CruzeError("needs-user", `${item.ref} had ${reopened.join(", ")} reopened since its approval, so the user re-approves it`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/domain/config.ts
CHANGED
|
@@ -23,9 +23,25 @@ export interface CruzeConfig {
|
|
|
23
23
|
* How `cruze review` starts an agent in a fresh context: the program and its arguments.
|
|
24
24
|
* Without one, the calling agent runs the role's prompt in a helper of its own.
|
|
25
25
|
*/
|
|
26
|
-
review: { command?: string[] };
|
|
26
|
+
review: { command?: string[]; decide: ReviewDecide };
|
|
27
|
+
/** The size band of a change, so each one is worth its own plan, review and verify. */
|
|
28
|
+
changes: ChangeLimits;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** Which review findings the user decides: only the exceptions, or every one. */
|
|
32
|
+
export type ReviewDecide = "exceptions" | "all";
|
|
33
|
+
|
|
34
|
+
export interface ChangeLimits {
|
|
35
|
+
/** A feature's change building fewer elements than this merges into a neighbour. */
|
|
36
|
+
minBuilds: number;
|
|
37
|
+
/** A change delivering more scenarios than this splits. */
|
|
38
|
+
maxScenarios: number;
|
|
39
|
+
/** A plan with more tasks than this splits. */
|
|
40
|
+
maxTasks: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_CHANGE_LIMITS: ChangeLimits = { minBuilds: 3, maxScenarios: 30, maxTasks: 25 };
|
|
44
|
+
|
|
29
45
|
export interface Layer {
|
|
30
46
|
name: string;
|
|
31
47
|
paths: string[];
|
|
@@ -97,6 +113,14 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
97
113
|
const review = asRecord(root["review"]) ?? {};
|
|
98
114
|
const reviewCommand = review["command"] === undefined ? undefined : stringList(review["command"], "review.command", problems);
|
|
99
115
|
if (reviewCommand?.length === 0) problems.push("review.command needs at least the program to run");
|
|
116
|
+
const decide = review["decide"] ?? "exceptions";
|
|
117
|
+
if (decide !== "exceptions" && decide !== "all") problems.push("review.decide must be exceptions or all");
|
|
118
|
+
const limits = asRecord(root["changes"]) ?? {};
|
|
119
|
+
const changes: ChangeLimits = {
|
|
120
|
+
minBuilds: positiveInt(limits["min_builds"], "changes.min_builds", problems) ?? DEFAULT_CHANGE_LIMITS.minBuilds,
|
|
121
|
+
maxScenarios: positiveInt(limits["max_scenarios"], "changes.max_scenarios", problems) ?? DEFAULT_CHANGE_LIMITS.maxScenarios,
|
|
122
|
+
maxTasks: positiveInt(limits["max_tasks"], "changes.max_tasks", problems) ?? DEFAULT_CHANGE_LIMITS.maxTasks,
|
|
123
|
+
};
|
|
100
124
|
|
|
101
125
|
const config: CruzeConfig = {
|
|
102
126
|
version: 1,
|
|
@@ -109,7 +133,11 @@ export function parseConfig(text: string): ConfigResult {
|
|
|
109
133
|
check: { maxLines: maxLines ?? 250, maxTypes: maxTypes ?? 5, exceptions },
|
|
110
134
|
layers,
|
|
111
135
|
commands,
|
|
112
|
-
review:
|
|
136
|
+
review: {
|
|
137
|
+
...(reviewCommand === undefined || reviewCommand.length === 0 ? {} : { command: reviewCommand }),
|
|
138
|
+
decide: decide === "all" ? "all" : "exceptions",
|
|
139
|
+
},
|
|
140
|
+
changes,
|
|
113
141
|
};
|
|
114
142
|
return { config, problems };
|
|
115
143
|
}
|
package/src/domain/journal.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const RECORDED_EVENTS: Record<string, string[]> = {
|
|
|
21
21
|
const OPTIONAL_FIELDS: Record<string, string[]> = {
|
|
22
22
|
rethink: ["missed_by"],
|
|
23
23
|
review: ["nits"],
|
|
24
|
+
disposition: ["decided"],
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/** The workflow steps and checks that find or miss a problem, so a retro can count them. */
|
|
@@ -30,6 +31,7 @@ export const FIELD_VALUES: Record<string, string[]> = {
|
|
|
30
31
|
level: ["task", "change", "feature", "architecture", "vision"],
|
|
31
32
|
kind: ["defect", "discovery"],
|
|
32
33
|
disposition: ["fixed", "waived", "deferred", "rejected"],
|
|
34
|
+
decided: ["agent", "user"],
|
|
33
35
|
review: ["design", "plan", "code", "realign"],
|
|
34
36
|
result: ["accepted", "sent-back"],
|
|
35
37
|
found_by: STEPS,
|
|
@@ -77,6 +79,9 @@ export function recordedEventProblems(event: string, fields: Record<string, stri
|
|
|
77
79
|
if (allowed !== undefined && !allowed.includes(value)) problems.push(`${field} must be one of ${allowed.join(", ")}`);
|
|
78
80
|
if (COUNT_FIELDS.includes(field) && !/^\d+$/.test(value)) problems.push(`${field} must be a whole number`);
|
|
79
81
|
}
|
|
82
|
+
if (event === "disposition" && fields["decided"] === "agent" && fields["disposition"] !== "fixed") {
|
|
83
|
+
problems.push("the agent decides only fixes; the user waives, defers or rejects a finding (decided=user)");
|
|
84
|
+
}
|
|
80
85
|
return problems;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -4,13 +4,14 @@ import type { WorkItem } from "../project/work_items.ts";
|
|
|
4
4
|
/**
|
|
5
5
|
* Whether the user accepted a change at verify. The latest verification recorded for the change
|
|
6
6
|
* counts. A later approval keeps it only when the change's own design is unchanged, as when it is
|
|
7
|
-
* re-approved because something upstream moved; a changed plan means verifying again.
|
|
7
|
+
* re-approved because something upstream moved; a changed plan means verifying again. Land's
|
|
8
|
+
* re-stamps don't count, since they follow the merge; an approval the agent gave does.
|
|
8
9
|
*/
|
|
9
10
|
export function isVerified(journal: JournalEntry[], change: WorkItem): boolean {
|
|
10
11
|
const byTime = (a: JournalEntry, b: JournalEntry): number => a.at.localeCompare(b.at);
|
|
11
12
|
const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort(byTime).at(-1);
|
|
12
13
|
if (verification?.["result"] !== "accepted") return false;
|
|
13
|
-
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && e["basis"]
|
|
14
|
+
const approvals = journal.filter((e) => e.event === "approve" && e["artifact"] === change.path && !String(e["basis"] ?? "").startsWith("land ")).sort(byTime);
|
|
14
15
|
const verifiedHash = approvals.filter((e) => e.at < verification.at).at(-1)?.["hash"];
|
|
15
16
|
return approvals.filter((e) => e.at > verification.at).every((e) => e["hash"] === verifiedHash);
|
|
16
17
|
}
|
|
@@ -7,6 +7,7 @@ import type { Delta } from "../project/deltas.ts";
|
|
|
7
7
|
import { isPlanned, readScope, readTasks, readTestPlan, type Scope, type Task } from "../project/plan_parts.ts";
|
|
8
8
|
import type { ProjectView } from "../project/project_view.ts";
|
|
9
9
|
import { error, warning, type Problem } from "./problem.ts";
|
|
10
|
+
import { planSizeProblems } from "./size_rules.ts";
|
|
10
11
|
|
|
11
12
|
const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
|
|
12
13
|
|
|
@@ -31,6 +32,7 @@ export function changePlanProblems(
|
|
|
31
32
|
problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
|
|
32
33
|
}
|
|
33
34
|
if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
|
|
35
|
+
problems.push(...planSizeProblems(view, path, tasks.length));
|
|
34
36
|
|
|
35
37
|
const proved = new Set(tasks.flatMap((task) => task.proves));
|
|
36
38
|
const taskPaths = tasks.flatMap((task) => task.paths);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DEFAULT_CHANGE_LIMITS, type ChangeLimits } from "../config.ts";
|
|
2
|
+
import { kindOf } from "../ids.ts";
|
|
3
|
+
import type { ChangeRow } from "../project/plan_parts.ts";
|
|
4
|
+
import type { ProjectView } from "../project/project_view.ts";
|
|
5
|
+
import { warning, type Problem } from "./problem.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every change pays for its own plan review, code review, verifier run and land, so a change
|
|
9
|
+
* should carry enough work to be worth them, and no more than one review and one run can hold.
|
|
10
|
+
*/
|
|
11
|
+
export function changeLimits(view: ProjectView): ChangeLimits {
|
|
12
|
+
return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
|
|
16
|
+
export function changeRowSizeProblems(view: ProjectView, path: string, rows: ChangeRow[], landed: Set<string>): Problem[] {
|
|
17
|
+
const limits = changeLimits(view);
|
|
18
|
+
const problems: Problem[] = [];
|
|
19
|
+
for (const row of rows.filter((r) => !landed.has(r.change))) {
|
|
20
|
+
if (rows.length > 1 && row.builds.length < limits.minBuilds) {
|
|
21
|
+
problems.push(warning(path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
|
|
22
|
+
}
|
|
23
|
+
const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
|
|
24
|
+
if (scenarios > limits.maxScenarios) {
|
|
25
|
+
problems.push(warning(path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return problems;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A plan with more tasks than one code review and one verify can hold. */
|
|
32
|
+
export function planSizeProblems(view: ProjectView, path: string, tasks: number): Problem[] {
|
|
33
|
+
const { maxTasks } = changeLimits(view);
|
|
34
|
+
if (tasks <= maxTasks) return [];
|
|
35
|
+
return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
|
|
36
|
+
}
|
|
@@ -11,6 +11,7 @@ import { deltaProblems } from "./delta_rules.ts";
|
|
|
11
11
|
import { malformedIdHeadings, lineOf } from "./living_rules.ts";
|
|
12
12
|
import { changePlanProblems } from "./plan_rules.ts";
|
|
13
13
|
import { error, warning, type Problem } from "./problem.ts";
|
|
14
|
+
import { changeRowSizeProblems } from "./size_rules.ts";
|
|
14
15
|
|
|
15
16
|
const REQUIRED_SECTIONS: Record<WorkItem["kind"], string[]> = {
|
|
16
17
|
feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
|
|
@@ -71,6 +72,7 @@ function featureScopeProblems(view: ProjectView, feature: WorkItem, delta: Delta
|
|
|
71
72
|
if (extra !== undefined) {
|
|
72
73
|
problems.push(warning(feature.path, extra.line, "feature-size", `the feature has ${rows.length} changes, over ${MAX_CHANGES}; split it into features a single design review can hold`));
|
|
73
74
|
}
|
|
75
|
+
problems.push(...changeRowSizeProblems(view, feature.path, rows, landedChanges));
|
|
74
76
|
for (const row of rows) {
|
|
75
77
|
if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
|
|
76
78
|
if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
|
|
@@ -13,6 +13,9 @@ export const RULE_SINCE: Record<string, string> = {
|
|
|
13
13
|
"validate:outside-module-map": "0.0.2",
|
|
14
14
|
"validate:readme-overview": "0.0.2",
|
|
15
15
|
"validate:feature-size": "0.0.2",
|
|
16
|
+
"validate:change-too-small": "0.0.5",
|
|
17
|
+
"validate:change-too-large": "0.0.5",
|
|
18
|
+
"validate:plan-too-large": "0.0.5",
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
/** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
|