@printpractical/cruze 0.0.8 → 0.0.10

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.
Files changed (152) hide show
  1. package/README.md +17 -13
  2. package/dist/adapters/inbound/cli/cli_context.js +6 -0
  3. package/dist/adapters/inbound/cli/commands/code.js +2 -1
  4. package/dist/adapters/inbound/cli/commands/diff.js +23 -0
  5. package/dist/adapters/inbound/cli/commands/documents.js +1 -1
  6. package/dist/adapters/inbound/cli/commands/show.js +72 -3
  7. package/dist/adapters/inbound/cli/commands/status.js +7 -3
  8. package/dist/adapters/inbound/cli/commands.js +9 -3
  9. package/dist/adapters/inbound/cli/parse_command.js +8 -0
  10. package/dist/adapters/outbound/git_repository.js +49 -7
  11. package/dist/adapters/outbound/project_root.js +13 -0
  12. package/dist/app/ports/repository.js +4 -0
  13. package/dist/app/use_cases/approve_artifact.js +18 -10
  14. package/dist/app/use_cases/check_code.js +5 -3
  15. package/dist/app/use_cases/journal_events.js +6 -2
  16. package/dist/app/use_cases/new_project_doc.js +10 -2
  17. package/dist/app/use_cases/project_status.js +4 -1
  18. package/dist/app/use_cases/run_review.js +10 -2
  19. package/dist/app/use_cases/show_elements.js +18 -0
  20. package/dist/app/use_cases/show_notes.js +11 -0
  21. package/dist/app/use_cases/show_scope.js +17 -1
  22. package/dist/app/use_cases/upstream_diff.js +55 -0
  23. package/dist/domain/approvals/rebase.js +39 -0
  24. package/dist/domain/approvals/upstream_text.js +36 -0
  25. package/dist/domain/check/budget_text.js +43 -6
  26. package/dist/domain/check/check_source.js +20 -3
  27. package/dist/domain/check/decision_comments.js +38 -0
  28. package/dist/domain/config.js +12 -0
  29. package/dist/domain/decision_citations.js +62 -0
  30. package/dist/domain/edits/agents_file.js +15 -1
  31. package/dist/domain/ids.js +3 -1
  32. package/dist/domain/journal.js +16 -3
  33. package/dist/domain/land/merge_plan.js +7 -2
  34. package/dist/domain/line_diff.js +56 -0
  35. package/dist/domain/project/element_lookup.js +124 -0
  36. package/dist/domain/project/living_model.js +2 -1
  37. package/dist/domain/project/notes.js +74 -0
  38. package/dist/domain/project/scope_view.js +20 -4
  39. package/dist/domain/skill_budget.js +36 -0
  40. package/dist/domain/status/next_step.js +52 -7
  41. package/dist/domain/status/pending_rethink.js +22 -0
  42. package/dist/domain/status/verification.js +12 -0
  43. package/dist/domain/status/work_status.js +17 -2
  44. package/dist/domain/validation/decision_rules.js +76 -0
  45. package/dist/domain/validation/delta_rules.js +2 -0
  46. package/dist/domain/validation/future_rules.js +40 -0
  47. package/dist/domain/validation/living_rules.js +2 -0
  48. package/dist/domain/validation/plan_rules.js +1 -1
  49. package/dist/domain/validation/validate_project.js +4 -0
  50. package/dist/domain/validation/vision_rules.js +41 -0
  51. package/dist/domain/versions/rule_versions.js +3 -0
  52. package/dist/main.js +8 -4
  53. package/package.json +1 -1
  54. package/skills/architect/SKILL.md +6 -2
  55. package/skills/architect/feature.md +6 -4
  56. package/skills/architect/project.md +2 -2
  57. package/skills/behavioural-testing/SKILL.md +3 -2
  58. package/skills/behavioural-testing/fakes.md +3 -3
  59. package/skills/build/SKILL.md +3 -2
  60. package/skills/envision/SKILL.md +3 -2
  61. package/skills/envision/feature.md +3 -1
  62. package/skills/envision/intake.md +28 -0
  63. package/skills/envision/possibilities.md +13 -0
  64. package/skills/envision/project.md +5 -3
  65. package/skills/explore/SKILL.md +2 -2
  66. package/skills/formats/SKILL.md +13 -2
  67. package/skills/formats/reference/architecture.md +8 -4
  68. package/skills/formats/reference/config.md +9 -4
  69. package/skills/formats/reference/durable-docs.md +4 -1
  70. package/skills/formats/reference/work-items.md +1 -1
  71. package/skills/formats/templates/vision.md +12 -0
  72. package/skills/grilling/SKILL.md +10 -0
  73. package/skills/hexagonal-design/SKILL.md +2 -2
  74. package/skills/hexagonal-design/languages/rust.md +2 -1
  75. package/skills/hexagonal-design/runtime.md +1 -0
  76. package/skills/land/SKILL.md +6 -2
  77. package/skills/plan/SKILL.md +15 -5
  78. package/skills/plan/tasks.md +4 -4
  79. package/skills/realign/SKILL.md +5 -3
  80. package/skills/realign/notes/0.0.10.md +131 -0
  81. package/skills/realign/notes/0.0.2.md +1 -1
  82. package/skills/realign/notes/0.0.6.md +1 -1
  83. package/skills/realign/notes/0.0.8.md +2 -2
  84. package/skills/realign/notes/0.0.9.md +59 -0
  85. package/skills/realign/notes.md +2 -0
  86. package/skills/research/SKILL.md +3 -2
  87. package/skills/rethink/SKILL.md +6 -4
  88. package/skills/rethink/levels.md +8 -8
  89. package/skills/roadmap/SKILL.md +2 -1
  90. package/skills/roles/SKILL.md +4 -4
  91. package/skills/roles/auditor.md +1 -1
  92. package/skills/roles/code-reviewer.md +5 -4
  93. package/skills/roles/design-reviewer.md +5 -29
  94. package/skills/roles/design-rubric.md +40 -0
  95. package/skills/roles/plan-rubric.md +11 -0
  96. package/skills/roles/surveyor.md +3 -2
  97. package/skills/roles/verifier.md +2 -0
  98. package/skills/triage/SKILL.md +1 -1
  99. package/skills/verify/SKILL.md +3 -3
  100. package/src/adapters/inbound/cli/cli_context.ts +6 -0
  101. package/src/adapters/inbound/cli/commands/code.ts +2 -1
  102. package/src/adapters/inbound/cli/commands/diff.ts +23 -0
  103. package/src/adapters/inbound/cli/commands/documents.ts +1 -1
  104. package/src/adapters/inbound/cli/commands/show.ts +72 -3
  105. package/src/adapters/inbound/cli/commands/status.ts +7 -3
  106. package/src/adapters/inbound/cli/commands.ts +9 -3
  107. package/src/adapters/inbound/cli/parse_command.ts +8 -0
  108. package/src/adapters/outbound/git_repository.ts +49 -7
  109. package/src/adapters/outbound/project_root.ts +13 -0
  110. package/src/app/ports/repository.ts +4 -0
  111. package/src/app/use_cases/approve_artifact.ts +18 -10
  112. package/src/app/use_cases/check_code.ts +5 -3
  113. package/src/app/use_cases/journal_events.ts +6 -2
  114. package/src/app/use_cases/new_project_doc.ts +10 -2
  115. package/src/app/use_cases/project_status.ts +4 -1
  116. package/src/app/use_cases/run_review.ts +10 -2
  117. package/src/app/use_cases/show_elements.ts +18 -0
  118. package/src/app/use_cases/show_notes.ts +11 -0
  119. package/src/app/use_cases/show_scope.ts +17 -1
  120. package/src/app/use_cases/upstream_diff.ts +55 -0
  121. package/src/domain/approvals/rebase.ts +39 -0
  122. package/src/domain/approvals/upstream_text.ts +36 -0
  123. package/src/domain/check/budget_text.ts +43 -6
  124. package/src/domain/check/check_source.ts +20 -3
  125. package/src/domain/check/decision_comments.ts +38 -0
  126. package/src/domain/config.ts +12 -0
  127. package/src/domain/decision_citations.ts +62 -0
  128. package/src/domain/edits/agents_file.ts +15 -1
  129. package/src/domain/ids.ts +3 -1
  130. package/src/domain/journal.ts +16 -3
  131. package/src/domain/land/merge_plan.ts +7 -2
  132. package/src/domain/line_diff.ts +56 -0
  133. package/src/domain/project/element_lookup.ts +124 -0
  134. package/src/domain/project/living_model.ts +2 -1
  135. package/src/domain/project/notes.ts +74 -0
  136. package/src/domain/project/scope_view.ts +20 -4
  137. package/src/domain/skill_budget.ts +36 -0
  138. package/src/domain/status/next_step.ts +52 -7
  139. package/src/domain/status/pending_rethink.ts +22 -0
  140. package/src/domain/status/verification.ts +12 -0
  141. package/src/domain/status/work_status.ts +17 -2
  142. package/src/domain/validation/decision_rules.ts +76 -0
  143. package/src/domain/validation/delta_rules.ts +2 -0
  144. package/src/domain/validation/future_rules.ts +40 -0
  145. package/src/domain/validation/living_rules.ts +2 -0
  146. package/src/domain/validation/plan_rules.ts +1 -1
  147. package/src/domain/validation/validate_project.ts +4 -0
  148. package/src/domain/validation/vision_rules.ts +41 -0
  149. package/src/domain/versions/rule_versions.ts +3 -0
  150. package/src/main.ts +8 -4
  151. package/templates/init/AGENTS.md +1 -1
  152. package/templates/init/config.yaml +5 -4
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.8 is the eighth preview. It cuts what each step reads and tightens the gates: every step runs in a new session, plan, build and the reviewers read a change's scope through `cruze show` instead of the living docs whole, round 2 of a review is checked by the agent, `cruze approve` refuses a size warning the user hasn't accepted, build waits for the changes it depends on and runs the same check as CI, and neither a file budget nor avoiding a dependency shapes code. 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.
13
+ Version 0.0.10 is the tenth preview. Intent now survives from the first idea to every design: the vision holds the principles every design follows and the capabilities that must stay possible, envision takes in any input and hands each part to the step that takes it up, and the design reviewer checks every design against them. Steps read less, through an element index and lookup, one rubric per review and a word budget on the skills. 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.8/printpractical-cruze-0.0.8.tgz
38
+ npm install -g https://github.com/PrintPractical/cruze/releases/download/v0.0.10/printpractical-cruze-0.0.10.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.8
50
+ npm install -g --install-links github:PrintPractical/cruze#v0.0.10
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.8/printpractical-cruze-0.0.8.tgz
64
+ cruze init --package https://github.com/PrintPractical/cruze/releases/download/v0.0.10/printpractical-cruze-0.0.10.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,25 +118,29 @@ 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, and warns when a change falls outside the size band in `changes:` |
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:` or a document cites a decision recorded elsewhere |
122
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
- | `cruze next` | Names the step to run next on this branch, and why, from computed status |
123
+ | `cruze next` | Names the step to run next on this branch, and why, from computed status: for stale work, whether a re-stamp or a rebase is due |
124
124
  | `cruze show <ref>` | Prints the design a change builds, or a feature's delta: each element's full body, the delivered scenarios, what they cite with where to find it, the settled decisions and the item's dispositions, so a step reads its scope instead of the whole architecture |
125
- | `cruze status` | Computes each document's state (approved, edited, upstream changed or unapproved) from content, never from stored state |
126
- | `cruze status --gate build` | Passes only when this branch's change, its feature and the architecture are approved and current |
125
+ | `cruze show <ID>...` | Prints the chosen elements' bodies, with each in-flight delta's version beside the living one |
126
+ | `cruze show --notes [<target>]` | Lists every note with its kind and what it is about, and the parts of the user's input envision handed to each step or feature; with a target, such as `architect` or a feature slug, only those for it |
127
+ | `cruze show --index [<context>]` | Lists every element of the architecture and specs on one line, with its status, module and the work in flight that changes it, or one context or capability in full, and gives the size of the living docs |
128
+ | `cruze status` | Computes each document's state (approved, edited, upstream changed or unapproved) from content, never from stored state, and counts the findings waiting on a rethink |
129
+ | `cruze diff <ref>` | Shows what changed upstream of a document since its approval, each element's approved text against its current text, and whether the work's delta needs a rebase |
130
+ | `cruze status --gate build` | Passes only when this branch's change, its feature and the architecture are approved and current, and no finding on them waits on a rethink |
127
131
  | `cruze status --overlap` | Lists changes on other branches that touch the same elements as this branch's change |
128
- | `cruze new <kind>` | Creates a project document (`vision`, `glossary`, `architecture`, `roadmap`) or work (`feature`, `change`, `adr`, `note`) from its template, with a dated ID that is never reused |
132
+ | `cruze new <kind>` | Creates a project document (`vision`, `glossary`, `architecture`, `roadmap`) or work (`feature`, `change`, `adr`, `note`) from its template, with a dated ID that is never reused; a note says its kind and what it is about (`--kind`, `--about`) |
129
133
  | `cruze task done <T#>` | Ticks a task with its commit; `cruze task reopen` unticks one a rethink changed, and `cruze task deviation` records a small departure from the plan |
130
134
  | `cruze features <add\|update\|drop>` | Edits the feature map: adds or drops a future feature, or corrects a summary |
131
135
  | `cruze roadmap prune` | Clears landed items from the roadmap's status when a release closes |
132
136
  | `cruze trace` | Fails when a delivered scenario has no test carrying its ID; `--all` checks every built scenario |
133
- | `cruze check` | Enforces the layer rules and the file budgets; `--ci` also fails on budget warnings |
137
+ | `cruze check` | Enforces the layer rules and the file budgets, and warns on decision numbers in code comments; `--ci` also fails on warnings |
134
138
  | `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 |
135
139
  | `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 |
136
140
  | `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 |
137
141
  | `cruze realign done` | Moves `standards:` up once every finding is fixed, waived or scheduled in a change named with `--change` |
138
142
  | `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 |
139
- | `cruze journal add <event>` | Records a rethink, review round, disposition, verification, override or bug; `cruze journal list` reads the work in flight's entries, or landed work's too with `--all`; `cruze feedback export` bundles the journal for improving Cruze itself |
143
+ | `cruze journal add <event>` | Records a rethink, review round, disposition, verification, override, bug or lesson; `cruze journal list` reads the work in flight's entries, or landed work's too with `--all`; `cruze feedback export` bundles the journal for improving Cruze itself |
140
144
 
141
145
  Stepping back is editing: change an upstream document and every approval that cites a changed element shows as stale, with the element named. Re-approving is the rewind; there are no phases to reset.
142
146
 
@@ -147,7 +151,7 @@ Stepping back is editing: change an upstream document and every approval that ci
147
151
  | Skill | What it holds |
148
152
  | --- | --- |
149
153
  | `cruze-explore` | Optional thinking partner before any step: options and trade-offs, captured as a note only when you ask |
150
- | `cruze-envision` | The what: the project's vision, goals and feature map, or one feature's intent and requirements with scenarios |
154
+ | `cruze-envision` | The what: the project's vision, goals, principles, what must stay possible later and feature map, asking about the capabilities the product may grow into, or one feature's intent and requirements with scenarios |
151
155
  | `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 |
152
156
  | `cruze-roadmap` | Orders the release into phases with blocking edges and goal coverage, and closes a release |
153
157
  | `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 |
@@ -170,7 +174,7 @@ Stepping back is editing: change an upstream document and every approval that ci
170
174
 
171
175
  ## Reporting back
172
176
 
173
- Cruze improves from what goes wrong in real projects. Every rethink, review finding, bug and override is recorded in the project's journal. To send them back:
177
+ Cruze improves from what goes wrong in real projects. Every rethink, review finding, bug and override is recorded in the project's journal, along with each repeated lesson about Cruze's own steps. To send them back, with the `## Known pitfalls` of `AGENTS.md`:
174
178
 
175
179
  ```sh
176
180
  cruze feedback export --out cruze-feedback.json
@@ -13,6 +13,8 @@
13
13
 
14
14
 
15
15
 
16
+
17
+
16
18
 
17
19
 
18
20
  /** What a command produces: a JSON result for agents, a summary for people, and whether it passed. */
@@ -54,6 +56,10 @@
54
56
 
55
57
 
56
58
 
59
+
60
+
61
+
62
+
57
63
 
58
64
 
59
65
 
@@ -1,9 +1,10 @@
1
+ import { posix } from "node:path";
1
2
  import { checkCode, traceTests } from "../../../../app/use_cases/check_code.js";
2
3
  import { landChange } from "../../../../app/use_cases/land_change.js";
3
4
 
4
5
 
5
6
  export async function runCheck(context , args , options ) {
6
- const files = [...options.file, ...args];
7
+ const files = [...options.file, ...args].map((file) => posix.normalize(posix.join(context.workingDirectory, file)));
7
8
  const report = await checkCode(context, { ci: options.ci, ...(files.length === 0 ? {} : { files }) });
8
9
  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
10
  lines.push(`Checked ${report.checked} file(s): ${report.passed ? "passed" : "failed"}.`);
@@ -0,0 +1,23 @@
1
+ import { upstreamDiff, } from "../../../../app/use_cases/upstream_diff.js";
2
+ import { requireArgs, } from "../cli_context.js";
3
+
4
+ export async function runDiff(context , args ) {
5
+ requireArgs(args, ["ref"]);
6
+ const report = await upstreamDiff(context, args[0] ?? "");
7
+ return { json: report, human: render(report) };
8
+ }
9
+
10
+ function render(report ) {
11
+ const lines = [`${report.artifact}: ${report.state}`];
12
+ if (report.changes.length === 0) lines.push("Nothing it cites changed since its approval.");
13
+ for (const change of report.changes) {
14
+ lines.push("", `${change.key}${change.path === undefined ? "" : ` in ${change.path}`}:`);
15
+ lines.push(...(change.diff ?? [change.note ?? ""]).map((line) => ` ${line}`));
16
+ }
17
+ if (report.rebase.length > 0) {
18
+ lines.push("", `Its delta changes ${report.rebase.join(", ")}: rewrite the delta to keep the new text, then approve with ${report.rebase.map((id) => `--rebase ${id}`).join(" ")}.`);
19
+ } else if (report.ref !== undefined && report.state === "upstream-changed") {
20
+ lines.push("", `No rebase needed. When the work still holds against this, re-stamp it with cruze approve ${report.ref} --restamp-unchanged.`);
21
+ }
22
+ return lines.join("\n");
23
+ }
@@ -34,7 +34,7 @@ export async function runNew(context , args , options
34
34
  }
35
35
  requireArgs(args, ["kind", "slug"]);
36
36
  if (args[0] === "note") {
37
- const report = await createNote(context, args[1] ?? "", requireOption(options.title, "--title"));
37
+ const report = await createNote(context, args[1] ?? "", requireOption(options.title, "--title"), requireOption(options.kind, "--kind"), requireOption(options.about, "--about"));
38
38
  return { json: report, human: `Created ${report.path}` };
39
39
  }
40
40
  const kind = args[0] ;
@@ -1,22 +1,69 @@
1
+ import { showElements, showIndex } from "../../../../app/use_cases/show_elements.js";
2
+ import { showNotes } from "../../../../app/use_cases/show_notes.js";
3
+
1
4
  import { showScope, } from "../../../../app/use_cases/show_scope.js";
5
+ import { isId } from "../../../../domain/ids.js";
6
+
2
7
 
3
- import { requireArgs, } from "../cli_context.js";
8
+ import { UsageError, requireArgs, } from "../cli_context.js";
4
9
 
5
- export async function runShow(context , args ) {
10
+ export async function runShow(context , args , options ) {
11
+ if (options.notes) {
12
+ if (args.length > 1 || options.index) throw new UsageError("--notes takes at most one target: project, a step, a feature slug or a context");
13
+ const report = await showNotes(context, args[0]);
14
+ return { json: report, human: "target" in report ? renderNotesFor(report).join("\n").trimEnd() : renderNotes(report.notes) };
15
+ }
16
+ if (options.index) {
17
+ if (args.length > 1) throw new UsageError("--index takes at most one context or capability");
18
+ const index = await showIndex(context, args[0]);
19
+ return { json: index, human: renderIndex(index) };
20
+ }
6
21
  requireArgs(args, ["ref"]);
22
+ if (args.every(isId)) {
23
+ const report = await showElements(context, args);
24
+ return { json: report, human: blocks(report.elements).join("\n").trimEnd() };
25
+ }
26
+ if (args.length > 1) throw new UsageError("show takes one feature or change, or one or more element IDs");
7
27
  const report = await showScope(context, args[0] ?? "");
8
28
  return { json: report, human: render(report) };
9
29
  }
10
30
 
31
+ /** The index as a list an agent scans for what exists, before fetching bodies with `cruze show <ID>`. */
32
+ function renderIndex(index ) {
33
+ const { architecture, specs } = index.words;
34
+ const lines = [`# Elements`, "", `docs/architecture.md holds ${architecture} words, and docs/specs/ ${specs}. Read an element's body with \`cruze show <ID>\`.`];
35
+ let source = "";
36
+ for (const entry of [...index.entries].sort((a, b) => Number(a.addedBy !== undefined) - Number(b.addedBy !== undefined))) {
37
+ const heading = entry.addedBy === undefined ? entry.source : "In flight, not yet merged";
38
+ if (heading !== source) lines.push("", `## ${heading}`, "");
39
+ source = heading;
40
+ const facts = [
41
+ entry.status,
42
+ entry.place,
43
+ entry.scenarios === undefined ? undefined : `${entry.scenarios} scenarios`,
44
+ entry.addedBy === undefined ? undefined : `added by ${entry.addedBy}`,
45
+ entry.changedBy.length === 0 ? undefined : `changed by ${entry.changedBy.join(", ")}`,
46
+ ].filter((f) => f !== undefined);
47
+ lines.push(`- ${entry.id}: ${entry.title}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`);
48
+ }
49
+ return lines.join("\n");
50
+ }
51
+
11
52
  /** The scope as a document an agent reads in place of the living docs. */
12
53
  function render(report ) {
13
54
  const lines = [`# ${report.ref}${report.feature === undefined ? "" : ` (feature ${report.feature})`}`, ""];
14
55
  lines.push("## Elements", "", ...blocks(report.elements), "## Scenarios", "", ...blocks(report.scenarios));
15
56
  lines.push("## Cited, not shown", "");
16
57
  lines.push(...(report.cited.length === 0 ? ["None."] : report.cited.map((c) => `- ${c.id}: ${c.title} (${c.source})`)), "");
17
- lines.push("## Settled decisions", "", ...(report.decisions.length === 0 ? ["None."] : report.decisions), "");
58
+ if (report.feature === undefined) lines.push("## Settled decisions", "", ...(report.decisions.length === 0 ? ["None."] : report.decisions), "");
59
+ else {
60
+ lines.push("## Settled decisions of the feature", "", ...(report.decisions.length === 0 ? ["None."] : report.decisions), "");
61
+ lines.push("## Settled decisions of the change", "", ...(report.changeDecisions.length === 0 ? ["None."] : report.changeDecisions), "");
62
+ }
18
63
  lines.push("## Dispositions", "");
19
64
  lines.push(...(report.dispositions.length === 0 ? ["None."] : report.dispositions.map((d) => `- [${d.disposition}, ${d.review}] ${d.finding}: ${d.reason}`)), "");
65
+ if (report.references.length > 0) lines.push("## References", "", ...report.references.map((r) => `- \`${r.path}\`: ${r.use}`), "");
66
+ lines.push(...renderNotesFor(report.notes));
20
67
  if (report.missing.length > 0) lines.push("## Missing", "", ...report.missing.map((id) => `- ${id} is defined nowhere`), "");
21
68
  return lines.join("\n").trimEnd();
22
69
  }
@@ -25,3 +72,25 @@ function blocks(elements ) {
25
72
  if (elements.length === 0) return ["None.", ""];
26
73
  return elements.flatMap((element) => [`<!-- ${element.source} -->`, ...element.body.join("\n").trimEnd().split("\n"), ""]);
27
74
  }
75
+
76
+ /** The notes about a piece of work and the parts of the user's input handed to it; nothing when there are none. */
77
+ function renderNotesFor(found ) {
78
+ if (found.notes.length === 0 && found.handoffs.length === 0) return [];
79
+ return [
80
+ "## Notes",
81
+ "",
82
+ ...found.notes.map((n) => `- \`${n.path}\`: ${n.title}${n.kind === undefined ? "" : ` (${n.kind})`}`),
83
+ ...found.handoffs.map((h) => `- ${h.part}, from ${h.source} (${h.status}, handed off in \`${h.note}\`)`),
84
+ "",
85
+ ];
86
+ }
87
+
88
+ function renderNotes(notes ) {
89
+ if (notes.length === 0) return "No notes.";
90
+ return notes
91
+ .flatMap((n) => [
92
+ `- \`${n.path}\`: ${n.title} (${n.kind ?? "no kind"}; about ${n.about.length === 0 ? "nothing named" : n.about.join(", ")})`,
93
+ ...n.handoff.map((h) => ` - ${h.part} goes to ${h.goesTo} (${h.status})`),
94
+ ])
95
+ .join("\n");
96
+ }
@@ -25,13 +25,13 @@ export async function runStatus(context , _args , options
25
25
  const lines = [`Branch: ${report.branch ?? "(none)"}${report.active === undefined ? "" : `, building ${report.active}`}`];
26
26
  for (const doc of report.documents) lines.push(describe(doc.artifact, doc));
27
27
  for (const feature of report.features) {
28
- lines.push(describe(`feature ${feature.ref}`, feature.approval));
28
+ lines.push(`${describe(`feature ${feature.ref}`, feature.approval)}${pendingNote(feature.rethinkPending)}`);
29
29
  for (const change of feature.changes) {
30
30
  const name = change.ref.split("/")[1] ?? change.ref;
31
- lines.push(change.landed !== undefined ? ` ${name}: landed ${change.landed}` : ` ${describe(name, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
31
+ lines.push(change.landed !== undefined ? ` ${name}: landed ${change.landed}` : ` ${describe(name, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}${pendingNote(change.rethinkPending)}`);
32
32
  }
33
33
  }
34
- for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
34
+ for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}${pendingNote(change.rethinkPending)}`);
35
35
  const json = {
36
36
  ...report,
37
37
  documents: report.documents.map(compact),
@@ -48,6 +48,10 @@ function compact(status )
48
48
  return { ...rest, approvedAt: record.approvedAt, by: record.by, ...(record.basis === undefined ? {} : { basis: record.basis }) };
49
49
  }
50
50
 
51
+ function pendingNote(findings ) {
52
+ return findings === undefined ? "" : `, ${findings.length} finding(s) waiting on a rethink`;
53
+ }
54
+
51
55
  /** Every step starts a new session: one session that ran plan, build, verify and land reached 745k tokens of context. */
52
56
  const FRESH_SESSION = "Run it in a new session, so its context starts empty.";
53
57
 
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { runCheck, runLand, runTrace } from "./commands/code.js";
3
+ import { runDiff } from "./commands/diff.js";
3
4
  import { runApprove, runNew, runValidate } from "./commands/documents.js";
4
5
  import { runInit } from "./commands/init.js";
5
6
  import { runInstall } from "./commands/install.js";
@@ -23,7 +24,12 @@ export const COMMANDS = {
23
24
  validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
24
25
  status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
25
26
  next: { handler: runNext, options: [], usage: "next the step to run next on this branch, and why" },
26
- show: { handler: runShow, options: [], usage: "show <ref> the design a change builds, or a feature's delta, with what it cites and its settled ledger" },
27
+ diff: { handler: runDiff, options: [], usage: "diff <ref> what changed upstream of a document since its approval, and whether its delta needs a rebase" },
28
+ show: {
29
+ handler: runShow,
30
+ options: ["index", "notes"],
31
+ usage: "show <ref> | show <ID>... | show --index [<context>] | show --notes [<target>] a change's design, chosen elements, one line per element, or the notes and hand-offs for a target",
32
+ },
27
33
  approve: {
28
34
  handler: runApprove,
29
35
  options: ["replan", "rebase", "by-agent", "restamp-unchanged"],
@@ -31,8 +37,8 @@ export const COMMANDS = {
31
37
  },
32
38
  new: {
33
39
  handler: runNew,
34
- options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title"] },
35
- usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr|note> <slug> --title <title> [--feature <ref>] [--roadmap <slug>]",
40
+ options: { vision: [], glossary: [], architecture: [], roadmap: [], feature: ["title", "roadmap"], change: ["title", "feature", "roadmap"], adr: ["title"], note: ["title", "kind", "about"] },
41
+ usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr> <slug> --title <title> [--feature <ref>] [--roadmap <slug>] | new note <slug> --title <title> --kind <input|intake|explore|research> --about <subjects>",
36
42
  },
37
43
  task: { handler: runTask, options: { done: ["change", "commit"], deviation: ["change"], reopen: ["change"] }, usage: "task <done|deviation|reopen> <T#> [text] [--change <ref>] [--commit <sha>]" },
38
44
  features: { handler: runFeatures, options: { add: ["summary", "goals"], update: ["summary"], drop: ["reason"] }, usage: "features <add|update|drop> <slug> [--summary <text>] [--goals <ids>] [--reason <text>]" },
@@ -54,6 +54,10 @@ export function parseCommand(argv ) {
54
54
  blockers: { type: "string" },
55
55
  "by-agent": { type: "boolean", default: false },
56
56
  "restamp-unchanged": { type: "boolean", default: false },
57
+ index: { type: "boolean", default: false },
58
+ notes: { type: "boolean", default: false },
59
+ kind: { type: "string" },
60
+ about: { type: "string" },
57
61
  },
58
62
  });
59
63
  } catch (error) {
@@ -78,6 +82,8 @@ export function parseCommand(argv ) {
78
82
  rebase: values.rebase,
79
83
  byAgent: values["by-agent"],
80
84
  restampUnchanged: values["restamp-unchanged"],
85
+ index: values.index,
86
+ notes: values.notes,
81
87
  ...optional("name", values.name),
82
88
  ...optional("title", values.title),
83
89
  ...optional("feature", values.feature),
@@ -96,6 +102,8 @@ export function parseCommand(argv ) {
96
102
  ...optional("package", values.package),
97
103
  ...optional("base", values.base),
98
104
  ...optional("blockers", values.blockers),
105
+ ...optional("kind", values.kind),
106
+ ...optional("about", values.about),
99
107
  };
100
108
  const given = [...new Set(tokens.flatMap((token) => (token.kind === "option" ? [token.name] : [])))];
101
109
  return { words: positionals, options, given, help: values.help, version: values.version };
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
 
4
4
 
@@ -37,13 +37,33 @@ export class GitRepository {
37
37
  }
38
38
 
39
39
  async filesOnBranch(branch , dir ) {
40
- const files = new Map ();
41
40
  const listing = await this.git("ls-tree", "-r", "--name-only", branch, "--", dir);
42
- for (const path of (listing ?? "").split("\n").filter((p) => p !== "")) {
43
- const text = await this.git("show", `${branch}:${path}`);
44
- if (text !== null) files.set(path, text);
45
- }
46
- return files;
41
+ const paths = (listing ?? "").split("\n").filter((p) => p !== "");
42
+ if (paths.length === 0) return new Map();
43
+ // One git process for every file: a process per file took up to two minutes on a project with 26 branches.
44
+ const blobs = await this.catFiles(paths.map((path) => `${branch}:${path}`));
45
+ return new Map(paths.flatMap((path, i) => (blobs[i] === undefined ? [] : [[path, blobs[i]] ])));
46
+ }
47
+
48
+ async firstCommitAdding(path , text ) {
49
+ const commits = await this.git("log", "--reverse", "--format=%H", `-S${text}`, "--", path);
50
+ return commits?.split("\n").find((c) => c !== "") ?? null;
51
+ }
52
+
53
+ async fileAt(commit , path ) {
54
+ return (await this.catFiles([`${commit}:${path}`]))[0] ?? null;
55
+ }
56
+
57
+ /** The contents of each object, in order, through one `git cat-file --batch`; undefined for one that is missing. */
58
+ catFiles(objects ) {
59
+ return new Promise((resolve) => {
60
+ const child = spawn("git", ["cat-file", "--batch"], { cwd: this.root, stdio: ["pipe", "pipe", "ignore"] });
61
+ const chunks = [];
62
+ child.stdout.on("data", (chunk ) => chunks.push(chunk));
63
+ child.on("error", () => resolve(objects.map(() => undefined)));
64
+ child.on("close", () => resolve(parseBatch(Buffer.concat(chunks), objects.length)));
65
+ child.stdin.end(`${objects.join("\n")}\n`);
66
+ });
47
67
  }
48
68
 
49
69
  async git(...args ) {
@@ -55,3 +75,25 @@ export class GitRepository {
55
75
  }
56
76
  }
57
77
  }
78
+
79
+ /** Splits `git cat-file --batch` output: a `<sha> <type> <size>` header, the bytes and a newline, or `<name> missing`. */
80
+ function parseBatch(output , count ) {
81
+ const results = [];
82
+ let at = 0;
83
+ while (results.length < count && at < output.length) {
84
+ const headerEnd = output.indexOf(10, at);
85
+ if (headerEnd === -1) break;
86
+ const header = output.subarray(at, headerEnd).toString("utf8");
87
+ const size = /^[0-9a-f]+ \w+ (\d+)$/.exec(header)?.[1];
88
+ if (size === undefined) {
89
+ results.push(undefined);
90
+ at = headerEnd + 1;
91
+ continue;
92
+ }
93
+ const start = headerEnd + 1;
94
+ results.push(output.subarray(start, start + Number(size)).toString("utf8"));
95
+ at = start + Number(size) + 1;
96
+ }
97
+ while (results.length < count) results.push(undefined);
98
+ return results;
99
+ }
@@ -0,0 +1,13 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /** The nearest directory at or above `start` that holds `.cruze/config.yaml`, or undefined when none does. */
5
+ export function findProjectRoot(start ) {
6
+ let dir = start;
7
+ for (;;) {
8
+ if (existsSync(join(dir, ".cruze", "config.yaml"))) return dir;
9
+ const parent = dirname(dir);
10
+ if (parent === dir) return undefined;
11
+ dir = parent;
12
+ }
13
+ }
@@ -1,7 +1,8 @@
1
1
  import { requirePlanReviewClosed, requireUnchangedSinceApproval, } from "../../domain/approvals/agent_approval.js";
2
2
  import { evaluateApproval } from "../../domain/approvals/evaluate.js";
3
3
  import { fingerprint, isApprovable } from "../../domain/approvals/fingerprint.js";
4
- import { latestApproval, serializeApprovals, withApproval, } from "../../domain/approvals/records.js";
4
+ import { rebaseNeeded } from "../../domain/approvals/rebase.js";
5
+ import { serializeApprovals, withApproval } from "../../domain/approvals/records.js";
5
6
  import { CruzeError } from "../../domain/cruze_error.js";
6
7
  import { updateProgress } from "../../domain/edits/progress_block.js";
7
8
  import { setStatus, stampMissingStatuses } from "../../domain/edits/status_lines.js";
@@ -14,7 +15,7 @@ import { PATHS } from "../../domain/project/layout.js";
14
15
  import { isPlanned, readChangesTable } from "../../domain/project/plan_parts.js";
15
16
  import { readProgress } from "../../domain/project/progress.js";
16
17
  import { approvalFolder, approvalsPath, } from "../../domain/project/project_view.js";
17
- import { featureOf } from "../../domain/project/work_items.js";
18
+ import { featureOf, } from "../../domain/project/work_items.js";
18
19
 
19
20
  import { SIZE_RULES } from "../../domain/validation/size_rules.js";
20
21
  import { validateProject } from "../../domain/validation/validate_project.js";
@@ -96,13 +97,14 @@ export async function approveArtifact(deps , ref , options
96
97
  throw new CruzeError("invalid", `${path} has ${errors.length} problem(s) to fix before approval:\n${listing}`);
97
98
  }
98
99
  requireSizesAccepted(problems, path);
100
+ if (item !== undefined) requireNoDecisionCitations(problems, path);
99
101
 
100
102
  const { hash, upstream } = fingerprint(view, path, parseMarkdown(text));
101
103
  const folder = approvalFolder(view, path);
102
104
  const current = view.approvals.get(folder) ?? { version: 1, approvals: [], merged: {} };
103
105
  const rebased = options.rebase ?? [];
104
106
  const deltaIds = item === undefined || item.kind === "change" ? new Set () : readDelta(item.doc).ids;
105
- requireRebased(view, deltaIds, latestApproval(current, path), rebased);
107
+ if (item !== undefined) requireRebased(view, item, rebased);
106
108
  const merged = { ...current.merged };
107
109
  for (const id of rebased) {
108
110
  const living = view.living.elements.get(id);
@@ -143,13 +145,8 @@ export async function approveArtifact(deps , ref , options
143
145
  * a rethink or another land, the delta would overwrite that edit, so approval waits until the
144
146
  * delta is reconciled and the element is named with --rebase.
145
147
  */
146
- function requireRebased(view , deltaIds , previous , rebased ) {
147
- if (previous === undefined) return;
148
- const moved = [...deltaIds].filter((id) => {
149
- const living = view.living.elements.get(id);
150
- const before = previous.upstream[id];
151
- return living !== undefined && before !== undefined && before !== elementHash(living.doc, living.element) && !rebased.includes(id);
152
- });
148
+ function requireRebased(view , item , rebased ) {
149
+ const moved = rebaseNeeded(view, item).filter((id) => !rebased.includes(id));
153
150
  if (moved.length > 0) {
154
151
  throw new CruzeError(
155
152
  "rebase-required",
@@ -172,6 +169,17 @@ function requireSizesAccepted(problems , path ) {
172
169
  );
173
170
  }
174
171
 
172
+ /**
173
+ * A delta that cites decisions by number would merge them into the living docs, where they dangle once
174
+ * the work is archived. Existing docs only warn, so approval of new or redesigned work is where it stops.
175
+ */
176
+ function requireNoDecisionCitations(problems , path ) {
177
+ const standing = problems.filter((p) => p.rule === "decision-citation");
178
+ if (standing.length === 0) return;
179
+ const listing = standing.map((p) => ` ${p.path}:${p.line ?? ""} ${p.message}`).join("\n");
180
+ throw new CruzeError("decision-cited", `${path} cites decisions by number in its delta, which merges into the living docs:\n${listing}`);
181
+ }
182
+
175
183
  function notWork(path ) {
176
184
  throw new CruzeError("needs-user", `${path} is a project document, so the user approves it`);
177
185
  }
@@ -27,16 +27,18 @@ export async function checkCode(deps , options
27
27
  const view = await loadView(deps.files);
28
28
  const config = requireConfig(view);
29
29
  const sources = await readMatching(deps.files, config.source);
30
+ const tests = await readMatching(deps.files, config.tests);
30
31
  for (const file of options.files ?? []) {
31
- if (!sources.has(file)) {
32
+ if (!sources.has(file) && !tests.has(file)) {
32
33
  const text = await deps.files.readText(file);
33
34
  if (text === undefined) throw new CruzeError("not-found", `${file} does not exist`);
34
35
  if (matchesAny(file, config.source)) sources.set(file, text);
36
+ else if (matchesAny(file, config.tests)) tests.set(file, text);
35
37
  }
36
38
  }
37
- const only = options.files?.filter((file) => sources.has(file));
39
+ const only = options.files?.filter((file) => sources.has(file) || tests.has(file));
38
40
  const standards = config.standards ?? FIRST_RELEASE;
39
- const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
41
+ const findings = checkSource({ config, sources, tests, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
40
42
  isPending("check", f.rule, standards) ? { ...f, pending: true } : f,
41
43
  );
42
44
  const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
@@ -1,4 +1,5 @@
1
1
  import { CruzeError } from "../../domain/cruze_error.js";
2
+ import { knownPitfalls } from "../../domain/edits/agents_file.js";
2
3
  import { journalPath, parseJournal, recordedEventProblems, recordedFields, } from "../../domain/journal.js";
3
4
  import { resolveArtifact } from "../../domain/project/artifact_ref.js";
4
5
  import { appendJournal, loadView, } from "../project_context.js";
@@ -40,10 +41,12 @@ export async function listEvents(deps , event , item
40
41
 
41
42
 
42
43
 
44
+
45
+
43
46
 
44
47
 
45
48
  /**
46
- * Bundles the journal for the framework feedback loop. By default the project name and
49
+ * Bundles the journal, and the project's known pitfalls, for the framework feedback loop. By default the project name and
47
50
  * the approver's identity are redacted, since the bundle may leave the project.
48
51
  */
49
52
  export async function exportFeedback(deps , redact ) {
@@ -56,5 +59,6 @@ export async function exportFeedback(deps , redact )
56
59
  return value;
57
60
  };
58
61
  const entries = redact ? view.journal.map((entry) => ({ ...(scrub(entry) ), by: "<redacted>" })) : view.journal;
59
- return { format: "cruze-feedback/1", cruze: deps.bundle.version, exported_at: deps.clock.now().toISOString(), redacted: redact, entries };
62
+ const pitfalls = knownPitfalls((await deps.files.readText("AGENTS.md")) ?? "").map((line) => (redact ? (scrub(line) ) : line));
63
+ return { format: "cruze-feedback/1", cruze: deps.bundle.version, exported_at: deps.clock.now().toISOString(), redacted: redact, entries, pitfalls };
60
64
  }
@@ -1,5 +1,6 @@
1
1
  import { CruzeError } from "../../domain/cruze_error.js";
2
2
  import { PATHS } from "../../domain/project/layout.js";
3
+ import { NOTE_KINDS } from "../../domain/project/notes.js";
3
4
  import { requireSlug } from "../../domain/project/new_items.js";
4
5
  import { renderTemplate } from "../../domain/scaffold.js";
5
6
  import { appendJournal, loadView, } from "../project_context.js";
@@ -27,14 +28,21 @@ export async function createProjectDoc(deps , kind )
27
28
  }
28
29
 
29
30
  /** Creates a dated note in `.cruze/notes/`, such as a captured explore session or research evidence. */
30
- export async function createNote(deps , slug , title ) {
31
+ /**
32
+ * A dated note, saying what kind it is and what it is about, so `cruze show` finds it for the work it
33
+ * informs: `project`, feature slugs, or context and capability names.
34
+ */
35
+ export async function createNote(deps , slug , title , kind , about ) {
31
36
  requireSlug(slug);
32
37
  if (title.trim() === "") throw new CruzeError("missing-title", "a title is required");
38
+ if (!(NOTE_KINDS ).includes(kind)) throw new CruzeError("note-kind", `a note's kind is one of ${NOTE_KINDS.join(", ")}`);
39
+ const subjects = about.split(",").map((a) => a.trim()).filter((a) => a !== "");
40
+ if (subjects.length === 0) throw new CruzeError("note-about", "name what the note is about: project, feature slugs, or contexts, comma-separated");
33
41
  const view = await loadView(deps.files);
34
42
  const date = deps.clock.today();
35
43
  const base = `${PATHS.notesDir}/${date}-${slug}`;
36
44
  let path = `${base}.md`;
37
45
  for (let n = 2; view.snapshot.has(path); n++) path = `${base}-${n}.md`;
38
- await deps.files.writeText(path, `# ${title}\n\n- Date: ${date}\n\n`);
46
+ await deps.files.writeText(path, `# ${title}\n\n- Date: ${date}\n- Kind: ${kind}\n- About: ${subjects.join(", ")}\n\n`);
39
47
  return { kind: "note", path };
40
48
  }