@printpractical/cruze 0.0.7 → 0.0.9

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 (96) hide show
  1. package/README.md +6 -5
  2. package/dist/adapters/inbound/cli/cli_context.js +2 -0
  3. package/dist/adapters/inbound/cli/commands/code.js +2 -1
  4. package/dist/adapters/inbound/cli/commands/journal.js +1 -1
  5. package/dist/adapters/inbound/cli/commands/show.js +32 -0
  6. package/dist/adapters/inbound/cli/commands/status.js +19 -3
  7. package/dist/adapters/inbound/cli/commands.js +3 -1
  8. package/dist/adapters/outbound/git_repository.js +40 -7
  9. package/dist/adapters/outbound/project_root.js +13 -0
  10. package/dist/app/use_cases/approve_artifact.js +19 -1
  11. package/dist/app/use_cases/check_code.js +10 -3
  12. package/dist/app/use_cases/journal_events.js +8 -5
  13. package/dist/app/use_cases/project_status.js +4 -1
  14. package/dist/app/use_cases/show_scope.js +37 -0
  15. package/dist/domain/check/budget_text.js +61 -0
  16. package/dist/domain/check/check_source.js +7 -4
  17. package/dist/domain/config.js +23 -1
  18. package/dist/domain/edits/agents_file.js +6 -1
  19. package/dist/domain/journal.js +13 -2
  20. package/dist/domain/project/scope_view.js +134 -0
  21. package/dist/domain/status/next_step.js +25 -2
  22. package/dist/domain/status/verification.js +12 -0
  23. package/dist/domain/status/work_status.js +13 -2
  24. package/dist/domain/validation/element_rules.js +5 -0
  25. package/dist/domain/validation/plan_rules.js +1 -1
  26. package/dist/domain/validation/size_rules.js +14 -5
  27. package/dist/domain/validation/work_item_rules.js +25 -3
  28. package/dist/domain/versions/rule_versions.js +1 -0
  29. package/dist/main.js +14 -4
  30. package/package.json +1 -1
  31. package/skills/architect/SKILL.md +6 -1
  32. package/skills/architect/feature.md +1 -1
  33. package/skills/architect/walkthrough.md +7 -4
  34. package/skills/build/SKILL.md +10 -8
  35. package/skills/dependency-approval/SKILL.md +8 -1
  36. package/skills/envision/SKILL.md +2 -0
  37. package/skills/envision/feature.md +1 -1
  38. package/skills/explore/SKILL.md +1 -1
  39. package/skills/formats/reference/architecture.md +1 -1
  40. package/skills/formats/reference/config.md +13 -4
  41. package/skills/formats/reference/specs.md +1 -1
  42. package/skills/formats/reference/work-items.md +1 -1
  43. package/skills/grilling/SKILL.md +19 -7
  44. package/skills/grilling/illustrating.md +37 -0
  45. package/skills/hexagonal-design/SKILL.md +2 -1
  46. package/skills/hexagonal-design/languages/rust.md +2 -1
  47. package/skills/improve/SKILL.md +3 -3
  48. package/skills/land/SKILL.md +2 -2
  49. package/skills/next/SKILL.md +1 -1
  50. package/skills/plan/SKILL.md +3 -3
  51. package/skills/plan/tasks.md +9 -5
  52. package/skills/realign/SKILL.md +2 -2
  53. package/skills/realign/notes/0.0.8.md +97 -0
  54. package/skills/realign/notes/0.0.9.md +59 -0
  55. package/skills/realign/notes.md +2 -0
  56. package/skills/research/SKILL.md +2 -1
  57. package/skills/rethink/SKILL.md +3 -3
  58. package/skills/rethink/levels.md +2 -2
  59. package/skills/roadmap/SKILL.md +2 -0
  60. package/skills/roles/SKILL.md +8 -10
  61. package/skills/roles/code-reviewer.md +10 -9
  62. package/skills/roles/design-reviewer.md +22 -12
  63. package/skills/roles/surveyor.md +1 -1
  64. package/skills/roles/verifier.md +8 -5
  65. package/skills/triage/SKILL.md +2 -0
  66. package/skills/verify/SKILL.md +6 -4
  67. package/src/adapters/inbound/cli/cli_context.ts +2 -0
  68. package/src/adapters/inbound/cli/commands/code.ts +2 -1
  69. package/src/adapters/inbound/cli/commands/journal.ts +1 -1
  70. package/src/adapters/inbound/cli/commands/show.ts +32 -0
  71. package/src/adapters/inbound/cli/commands/status.ts +19 -3
  72. package/src/adapters/inbound/cli/commands.ts +3 -1
  73. package/src/adapters/outbound/git_repository.ts +40 -7
  74. package/src/adapters/outbound/project_root.ts +13 -0
  75. package/src/app/use_cases/approve_artifact.ts +19 -1
  76. package/src/app/use_cases/check_code.ts +10 -3
  77. package/src/app/use_cases/journal_events.ts +8 -5
  78. package/src/app/use_cases/project_status.ts +4 -1
  79. package/src/app/use_cases/show_scope.ts +37 -0
  80. package/src/domain/check/budget_text.ts +61 -0
  81. package/src/domain/check/check_source.ts +7 -4
  82. package/src/domain/config.ts +23 -1
  83. package/src/domain/edits/agents_file.ts +6 -1
  84. package/src/domain/journal.ts +13 -2
  85. package/src/domain/project/scope_view.ts +134 -0
  86. package/src/domain/status/next_step.ts +25 -2
  87. package/src/domain/status/verification.ts +12 -0
  88. package/src/domain/status/work_status.ts +13 -2
  89. package/src/domain/validation/element_rules.ts +5 -0
  90. package/src/domain/validation/plan_rules.ts +1 -1
  91. package/src/domain/validation/size_rules.ts +14 -5
  92. package/src/domain/validation/work_item_rules.ts +25 -3
  93. package/src/domain/versions/rule_versions.ts +1 -0
  94. package/src/main.ts +14 -4
  95. package/templates/init/AGENTS.md +1 -1
  96. package/templates/init/config.yaml +5 -4
@@ -20,7 +20,7 @@ A role is a prompt with fixed inputs and a report as its only output. It runs in
20
20
 
21
21
  1. Read the role file and gather exactly the inputs it lists. Done when you have every input path, plus any command output the role asks for.
22
22
  2. Start a fresh context: a helper agent with no conversation history, where the agent can start one. Give it the role file's full text and the inputs the role lists, including draft documents it names, and nothing else. Your own opinions, the conversation and your reasons for the design stay out.
23
- 3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier`, `researcher`, `auditor` or `surveyor`. Add `--base <commit>` for the code under review. It starts the agent configured under `review:` in `.cruze/config.yaml` in a fresh context and prints the role's report. When no agent is configured, or it isn't installed, it reports `status: run-in-helper` with the role's full prompt instead: start a helper, such as a subagent, and give it exactly that prompt. If no fresh context can run, the reviewers, the verifier and the auditor must still run fresh: tell the user, and have them run the role in a new session. The researcher may run in your own context, since its evidence is checkable; say that you did.
23
+ 3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier`, `researcher`, `auditor` or `surveyor`. Add `--base <commit>` for the code under review. It starts the agent configured under `review:` in `.cruze/config.yaml` in a fresh context and prints the role's report. When no agent is configured, or it isn't installed, it reports `status: run-in-helper` with the role's full prompt instead: start a helper, such as a subagent, and give it exactly that prompt. The configured agent can run only the commands `review.command` allows: before running the verifier, or a design reviewer whose design spells out commands to check against their `--help`, add those commands with the user's agreement. If no fresh context can run, the reviewers, the verifier and the auditor must still run fresh: tell the user, and have them run the role in a new session. The researcher may run in your own context, since its evidence is checkable; say that you did.
24
24
  4. Take the report as it comes back. Check its evidence against the files before you act on any finding.
25
25
 
26
26
  ## Running a review
@@ -38,8 +38,8 @@ A review has at most two rounds. Only blockers can force the second one.
38
38
  - You recommend anything but `fixed`: waiving, deferring or rejecting it, or a rethink because it needs a design change.
39
39
  - You would fix it differently from the reviewer's proposed fix.
40
40
 
41
- 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.
42
- 4. **Present them in one message.** First each exception, with your recommended disposition and why:
41
+ 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. Nits are a count, not findings: when a report lists any, they get no disposition and you don't bring them to the user.
42
+ 4. **Present them in one message,** written to be read cold and illustrated as `.agents/skills/cruze-grilling/illustrating.md` says. First each exception: what goes wrong, shown as the reviewer's failure scenario played out (the interaction, the code or the diagram), what the fix changes, shown before and after, and your recommended disposition and why:
43
43
  - `fixed`: you will change the work as the finding proposes.
44
44
  - `waived`: the finding is right, but the user accepts the risk.
45
45
  - `deferred`: it belongs to later work. When it is future scope, add it with `cruze features add <slug> --summary <text> --goals <ids>`.
@@ -54,13 +54,11 @@ A review has at most two rounds. Only blockers can force the second one.
54
54
 
55
55
  ### Round 2
56
56
 
57
- Run round 2 only when a round-1 blocker was fixed. It checks only whether those blockers are fixed, and raises nothing new.
57
+ Run round 2 only when a round-1 blocker was fixed. It checks only whether those blockers are fixed, and raises nothing new. You check them: in 49 recorded rounds across two projects, no reviewer re-reading a fix found a blocker still open, and the user sees every fix at the walkthrough, in the plan, or in verify's report.
58
58
 
59
- 1. **Choose who checks.** When every blocker was fixed exactly as the reviewer proposed, check the fixes yourself: a fresh reviewer re-reading a fix it proposed finds nothing new. When any blocker was fixed differently from the proposal, or its fix reaches past what the finding names, run the reviewer.
60
- 2. **Check them yourself:** read each blocker's finding against the diff of its fix, and run `cruze validate`. A blocker is fixed when the diff does what the proposal says and validate reports no new errors.
61
- 3. **Or run the reviewer** again with its usual inputs, plus the round-1 blockers and the diff of their fixes. It may be the round-1 reviewer resumed, since that context never saw the conversation that produced the work (with `cruze review`: `--round 2 --blockers <file> --base <round-1 commit>`).
62
- 4. Record the round with `round=2`, the blockers still open, `concerns=0`, and `checked_by=agent` or `checked_by=reviewer`.
63
- 5. Take any blocker still open to the user, who fixes it now, waives it or starts a rethink. There is no round 3.
59
+ 1. **Check each blocker** against the diff of its fix, and run `cruze validate`. A blocker is fixed when the diff does what the proposal says and validate reports no new errors. When a fix differs from the proposal or reaches past what the finding names, say so in the message that presents it, so the user can object.
60
+ 2. Record the round with `round=2`, the blockers still open, `concerns=0` and `checked_by=agent`.
61
+ 3. Take any blocker still open to the user, who fixes it now, waives it or starts a rethink. There is no round 3.
64
62
 
65
63
  Done when every finding has a journaled disposition, and every exception, and every blocker left open in round 2, has the user's decision.
66
64
 
@@ -70,4 +68,4 @@ Reviewers read the settled ledger so they never re-raise a decision. It consists
70
68
 
71
69
  - The `## Settled decisions` of the feature or change under review.
72
70
  - The `D<n>` lines in the `## Decisions` section of `docs/architecture.md`, and the accepted ADRs in `docs/adr/`.
73
- - Earlier dispositions, from `cruze journal list --event disposition`.
71
+ - Earlier dispositions of the item under review, from `cruze journal list --event disposition --item <ref>`. `cruze show <ref>` includes them. Never list the whole project's: two projects' reviewers read every disposition ever recorded, about 36k tokens a time, a hundred times each.
@@ -4,13 +4,14 @@ You review the code a change adds, before it lands. You work in two lanes. The s
4
4
 
5
5
  ## Inputs
6
6
 
7
- - The change's `change.md`, and its `feature.md` when it belongs to a feature.
7
+ - The change's `change.md`, and the output of `cruze show <ref>`: the elements it builds, the scenarios it delivers, the settled decisions and the ledger. Read an element it lists as cited only when the diff calls it.
8
8
  - The diff under review, from `git diff <base>` for the base commit you are given.
9
- - `docs/architecture.md`, the ADRs in `docs/adr/`, and the specs in `docs/specs/` for the capabilities the change touches.
10
- - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, with the output of `cruze journal list --event disposition`.
9
+ - The ADRs in `docs/adr/`, and the specs in `docs/specs/` for the capabilities the change touches.
10
+ - The references `cruze show` lists, at the sections the change's elements cite.
11
+ - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, with the output of `cruze journal list --event disposition --item <ref>`.
12
+ - The deviations the change recorded, from `cruze journal list --event deviation --item <ref>`.
11
13
  - The output of `cruze check` and `cruze trace --change <ref>`.
12
14
  - The standards: `.agents/skills/cruze-hexagonal-design/SKILL.md`, its language file for the project's language when one exists, `.agents/skills/cruze-behavioural-testing/SKILL.md` and `.agents/skills/cruze-dependency-approval/SKILL.md`.
13
- - In round 2 only: the round-1 blockers and the diff of their fixes.
14
15
 
15
16
  ## Rubric
16
17
 
@@ -19,7 +20,7 @@ Review against these items and nothing else. Each finding names one.
19
20
  ### Spec lane
20
21
 
21
22
  1. **Scenarios.** Every scenario the change delivers is implemented, and has a test that carries its ID, drives the seam the test plan names, and takes its expected values from the scenario.
22
- 2. **Failures.** Each failure scenario produces the error it specifies and leaves the state it specifies.
23
+ 2. **Failures.** Each failure scenario produces the error it specifies and leaves the state it specifies. Wire formats, codes and behaviour that a reference governs follow the section the design cites.
23
24
  3. **Scope.** The code adds no behaviour the change's scope doesn't deliver, and no public item, variant or operation that only a later change uses. Unrequested or early code is a finding, even when it looks useful or the next change needs it.
24
25
  4. **Protected tests.** No behaviour, contract or smoke test for an approved scenario, port or flow was weakened, skipped or deleted without a delta that changes its subject.
25
26
 
@@ -27,10 +28,11 @@ Review against these items and nothing else. Each finding names one.
27
28
 
28
29
  5. **Placement.** Every file the diff adds or changes is where the architecture puts its owner, and matches the task's file targets or a recorded deviation.
29
30
  6. **Direction.** Dependencies point inward. No technology type crosses a port. Inbound adapters hold no domain rules or workflow.
30
- 7. **Size.** No file holds two responsibilities or passes its budget without a recorded exception.
31
+ 7. **Size.** No file holds two responsibilities, and none passes its budget without a recorded exception. No file was split by length alone, leaving one responsibility across files, and no code is bent to fit a budget, such as a type alias, a string where an error type belongs, types merged or a function placed away from its owner to stay under a limit.
31
32
  8. **Idiom.** The code is idiomatic for the language, uses its standard tooling, and follows the language file's rules, including where tests live. Tests reuse the fixtures and harnesses in the test support module rather than redefining them.
32
- 9. **Libraries.** Nothing hand-writes what a mature library does without a recorded `build` decision, and every new dependency has a recorded approval.
33
+ 9. **Libraries.** Nothing hand-writes what a mature library does without a recorded `build` decision, and every new dependency has a recorded approval. No code works around a dependency or a feature: hand-written code, a shell command or a less idiomatic construct chosen because the clean one needs a dependency is a finding.
33
34
  10. **Contracts.** Each adapter honours its port's `Operations`, and each fake passes the same contract tests as the real adapter.
35
+ 11. **Deviations.** Each deviation the change recorded is a choice build may make on its own: a private helper or a new file inside the owner's module, a `check.exceptions` entry, a fixture in the test support module, or two tasks swapped. A deviation that added an element, port operation or scenario, wrote a file outside the owner's module, changed an approved scenario's behaviour or its test, worked around a dependency or a feature, or settled a limit, timeout, message or error case the design doesn't give, needed a rethink: a blocker, with `Needs design change` naming the element. Two projects recorded 13 such deviations.
34
36
 
35
37
  ## Findings
36
38
 
@@ -46,10 +48,9 @@ Each finding has this shape:
46
48
  ```
47
49
 
48
50
  - 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, 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.
51
+ - 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: give only their number, never their text. Report only what would go wrong: no concerns is a valid report, and a small change usually has few.
50
52
  - 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
53
  - When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
52
- - In round 2, report only whether each round-1 blocker is fixed, as `fixed` or `open` with the reason. Raise nothing new.
53
54
 
54
55
  ## Report
55
56
 
@@ -5,14 +5,14 @@ You review a design, or a change's plan, before any code exists. You find the pl
5
5
  ## Inputs
6
6
 
7
7
  - The artifact under review:
8
- - Feature scope: the feature's `feature.md`.
8
+ - Feature scope: the feature's `feature.md`, and the output of `cruze show <ref>` for the elements its delta cites, each with where it sits, and the settled ledger.
9
9
  - Project scope: `docs/architecture.md`, `.cruze/config.yaml` and `docs/roadmap.md`, plus the walking-skeleton change once it exists.
10
- - A plan: the change's `change.md`, its `feature.md` when it has one, and the output of `cruze status --overlap --change <ref>`.
11
- - The living docs it builds on: `docs/vision.md`, `docs/glossary.md`, `docs/architecture.md`, the ADRs in `docs/adr/`, and the specs in `docs/specs/` that the artifact cites.
12
- - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, with the output of `cruze journal list --event disposition`.
10
+ - A plan: the change's `change.md`, the output of `cruze show <ref>`, and the output of `cruze status --overlap --change <ref>`.
11
+ - The living docs it builds on: `docs/vision.md`, `docs/glossary.md`, the ADRs in `docs/adr/`, the specs in `docs/specs/` that the artifact cites, and, from `docs/architecture.md`, its Overview, Dependency rules, Cross-cutting concerns and Decisions sections plus each element `cruze show` lists as cited. Read the whole architecture only at project scope.
12
+ - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, with the output of `cruze journal list --event disposition --item <ref>`.
13
13
  - The output of `cruze validate`.
14
+ - The references `cruze show` lists, from `references:` in `.cruze/config.yaml`: read the sections the design cites or relies on.
14
15
  - The standards: `.agents/skills/cruze-hexagonal-design/SKILL.md` and `.agents/skills/cruze-formats/SKILL.md`.
15
- - In round 2 only: the round-1 blockers and the diff of their fixes.
16
16
 
17
17
  ## Rubric
18
18
 
@@ -22,26 +22,37 @@ Review against these items and nothing else. Each finding names one.
22
22
  2. **Seams.** Every scenario can be driven through a use case, or through an inbound adapter when the outcome is what that adapter shows. Every driven port the use case needs can be faked.
23
23
  3. **Placement.** Every new or changed element with code has a `Module` whose `Layer` fits its role and a `File` with a real path. No generic modules, and no file is likely to hold two responsibilities. The layout is the one normal for the kind of system and the language, and every layer, module, package or target split beyond a context's `core` and its adapters has a recorded reason. Every element, fact and value serves a requirement in scope or a `Future` fact the feature map calls for. Anything reserved for a need nobody stated is a finding.
24
24
  4. **Direction.** Every dependency points inward and is allowed by the `RULE` elements and the `layers:` in `.cruze/config.yaml`.
25
- 5. **Adopt or build.** Every component that isn't domain logic has an adopt-or-build decision with a reason, and every new dependency is named with the features the design uses. Every command the design spells out, such as a CI step, is spelled as that version's `--help` or source shows it. A claim the design relies on that the research left unverified is a blocker.
26
- 6. **Contracts.** Every new or changed port's `Operations` state what each operation guarantees, how it fails, and who owns cleanup.
25
+ 5. **Adopt or build.** Every component that isn't domain logic has an adopt-or-build decision with a reason, and every new dependency is named with the features the design uses. Every type, trait, macro, framework or tool the design names from outside the project comes from a dependency and feature the design approves, declared by each crate, package or target that uses it, test support included, and allowed by the import rules in `XC` and the `layers:` in `.cruze/config.yaml`. A choice whose reason is avoiding a dependency, a crate or a feature, where the idiomatic design needs one, is a concern: name the idiomatic design. Every command the design spells out, such as a CI step, is spelled as that version's `--help` or source shows it. A claim the design relies on that the research left unverified is a blocker.
26
+ 6. **Contracts.** Every new or changed port's `Operations` state what each operation guarantees, how it fails, and who owns cleanup. Check each use case and each port operation for the values build would otherwise have to pick: a named error for every failure a caller can meet, where a panic, an `expect` or "caller error" counts as missing; a number for every timeout, limit, buffer and poll interval; what a caller observes after each state transition, and what happens to queued or in-flight work at it; and one owner for every message a scenario quotes. Each one missing is a blocker: two projects settled 35 of them during build, as deviations.
27
27
  7. **Flows.** The key scenarios have a `FLOW` whose participants are all in its `Elements`, including the failure paths the scenarios need.
28
- 8. **Consistency.** The design agrees with `docs/architecture.md`, the accepted ADRs and what earlier changes landed, or changes them explicitly through a `MODIFIED` operation or a new ADR. Its own elements agree with each other. Check every pair, not a sample:
28
+ 8. **Consistency.** The design agrees with `docs/architecture.md`, the accepted ADRs, the references and what earlier changes landed, or changes them explicitly through a `MODIFIED` operation or a new ADR. Its own elements agree with each other. Check every pair, not a sample:
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
32
  - each `XC` rule against the other `XC` rules, the `RULE` elements and the `layers:` in `.cruze/config.yaml`, such as a logging rule that every module logs against a build rule that forbids the logging import;
33
33
  - each pair of use cases that change the same state, for an interleaving that leaves it wrong, against the concurrency rule in `XC`;
34
34
  - at feature scope, the prose in `docs/architecture.md` the delta makes untrue, such as an Overview sentence or a `D<n>` line describing an element the delta splits or replaces.
35
+ - each new or renamed operation, method or builder step against the others the same type, trait or caller reaches, for two names a caller can't tell apart, such as an `events` method beside an `events` trait method the type also implements.
35
36
  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-too-small`, `change-too-large` or `feature-size` warning from `cruze validate` is a finding, unless a decision records the user accepting it. 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.
36
37
  10. **Ambiguity.** Name each point where a fresh implementer could reasonably build something different from what the user expects.
38
+ 11. **Flow control and recovery.** Follow each message, request and event from its source to its consumer through every task, channel and queue the design names, against `.agents/skills/cruze-hexagonal-design/runtime.md`. Check:
39
+ - each bounded queue or buffer: what a sender does when it is full, and which other work waits meanwhile. One slow peer, link, subscriber or consumer holds up only its own work unless a decision says otherwise, so one queue or one task serving several peers needs a reason. A limit meant to tell a stalled consumer from a burst says how it tells them apart;
40
+ - each place several sources feed one consumer: the order it takes them in, so that none starves;
41
+ - each asynchronous result: how it reaches the request that waits for it, including across a reconnect or a retry;
42
+ - each recovery, such as a reconnect, resubscribe, relist or retry: its schedule and limit, what happens when the recovery itself fails, what is lost between the failure and the recovery and how the consumer learns of it, and the order of steps that loses nothing, such as subscribing before relisting;
43
+ - each loop, task and retry: what stops it, and what shutdown does with the work it holds;
44
+ - each value the design compares with observed state: an adapter can observe it, or the difference is reported for good;
45
+ - each exclusive resource, such as a socket, a device or kernel state: what happens when a second instance or caller claims it.
46
+
47
+ Each one missing that a scenario or an `XC` rule depends on is a blocker: in two projects, 7 of 14 defect rethinks after a design review were one of these, found as late as verify.
37
48
 
38
49
  ## Plan rubric
39
50
 
40
51
  When the artifact is a change's plan, review against these items instead.
41
52
 
42
53
  1. **Test plan.** Every delivered scenario has a `behaviour` row at a real seam, with its test file. Every port that a built adapter implements has a `contract` row. Seams need fakes of driven ports only, never mocks of internal code.
43
- 2. **Tasks.** Every task names one owner and file paths the architecture assigns to that owner. Every scope ID is proved by a task. Each task is small enough to build red to green and commit on its own. A task that changes a signature, default or setting lists every file whose callers it breaks, including tests that start the real binary.
44
- 3. **Order.** Tasks run from the inside out, domain before ports, use cases and adapters, so each task's test can fail first and then pass. The last tasks wire the composition root.
54
+ 2. **Tasks.** Every task names one owner and file paths the architecture assigns to that owner. Every scope ID is proved by a task. Each task is small enough to build red to green and commit on its own. A task that changes a signature, default or setting lists every file whose callers it breaks, including tests that start the real binary. A task that adds a module, file or dependency lists the file that declares it, such as a crate root, a `mod.rs`, a package manifest or an Xcode project. A rule, text, format or conversion that several elements apply has one owning file, and each later task that applies it names that file as a callee rather than restating it. An accessor, variant or type arrives in the task whose code first handles, matches or constructs it, never in the task that only tests it.
55
+ 3. **Order.** Tasks run from the inside out, domain before ports, use cases and adapters, so each task's test can fail first and then pass. A test sits in the task that builds the behaviour it checks: a test that an earlier task's code already passes is in the wrong task. A break the plan names is a behaviour the task's code could get wrong, not an edit that may not compile. The last tasks wire the composition root.
45
56
  4. **No design.** Nothing in the plan changes the architecture or the specs. A plan that needs a new element, operation or scenario goes back to `architect` or `rethink`.
46
57
  5. **Risks and overlap.** The riskiest task is named, with how it is checked. Overlap with work on other branches is resolved or accepted.
47
58
  6. **Ambiguity.** Name each task a fresh implementer could reasonably build differently from what the plan means.
@@ -59,10 +70,9 @@ Each finding has this shape:
59
70
  ```
60
71
 
61
72
  - 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**.
62
- - 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.
73
+ - List every blocker. List at most 5 concerns, or one per 5 scenarios in a feature's spec delta, or for a plan one per 5 tasks, when that is more, the most important first. Count the rest as nits: give only their number, never their text. Report only what would go wrong: no concerns is a valid report, and a small artifact usually has few.
63
74
  - Drop any finding that contradicts the settled ledger. The only exception is a blocker with new evidence, which you mark `New evidence:` and explain.
64
75
  - When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
65
- - In round 2, report only whether each round-1 blocker is fixed, as `fixed` or `open` with the reason. Raise nothing new.
66
76
 
67
77
  ## Report
68
78
 
@@ -8,7 +8,7 @@ You survey existing code for structure that departs from Cruze's current standar
8
8
  - The standards: `.agents/skills/cruze-hexagonal-design/SKILL.md`, `.agents/skills/cruze-hexagonal-design/shapes.md`, the language file for the project's language when one exists, and `.agents/skills/cruze-behavioural-testing/SKILL.md`.
9
9
  - `docs/architecture.md`, for the module map and the elements the code implements, and `.cruze/config.yaml`.
10
10
  - The files in scope: for the layout, the build manifests, such as `Cargo.toml` or `Package.swift`, and each module's `Path`; for a module, every source and test file under its `Path`.
11
- - The journal: the output of `cruze journal list --event deviation`, `cruze journal list --event rethink` and `cruze journal list --event disposition`.
11
+ - The journal, landed work included: the output of `cruze journal list --event deviation --all`, `cruze journal list --event rethink --all` and `cruze journal list --event disposition --all`.
12
12
  - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, and the earlier `improve` and `realign` dispositions. Never raise a candidate they waived.
13
13
 
14
14
  ## Work
@@ -4,23 +4,26 @@ You check that a change actually works when someone uses it. The tests passing i
4
4
 
5
5
  ## Inputs
6
6
 
7
- - The change's `change.md`, and its `feature.md` when it belongs to a feature. The scenarios it delivers are listed under `Delivers` in its scope, and written out in the spec delta.
7
+ - The change's `change.md`, for its test plan, and the output of `cruze show <ref>`: the scenarios it delivers, written out, and the flows and elements it builds.
8
8
  - The project's commands, from `commands:` in `.cruze/config.yaml`, and from `README.md` when they aren't there.
9
9
  - The mechanical checks' results, with the commit they ran at.
10
- - The flows in `docs/architecture.md` that serve those scenarios.
11
10
 
12
11
  ## Work
13
12
 
14
13
  1. **Build.** Build the system with the project's build command, and record whether it passed. The test suite already ran at the commit in your inputs, and CI runs it again before the merge, so don't run it. Run a single test only when it is how a scenario reaches its real front door, such as a UI test that drives the app.
15
14
  2. **Trace.** Run `cruze trace --change <ref>` and record its result.
16
- 3. **Run each scenario.** For every scenario the change delivers:
15
+ 3. **Run each scenario that has a front door.** A scenario whose test plan row names an inbound adapter, a screen or a real adapter as its seam is one a user meets, so run it:
17
16
  - Set up its GIVEN with real things where you can: configuration files, directories, a local test server, a pseudo-terminal. Put them in a temporary directory outside the project.
18
17
  - Perform its WHEN through the real inbound adapter, such as the CLI binary or an HTTP request, the way a user or client would.
19
18
  - Compare what you observe with its THEN, word for word where the scenario quotes output.
20
- 4. **Probe one step further.** For each scenario, try the nearest thing a user would plausibly do next or get wrong, such as the same command twice, a typo in the input, or an interrupt. Report surprises; they are not failures of the scenario.
19
+
20
+ A scenario whose row runs through a use case with fakes is proved by its behaviour test, which passed at the checks commit: give the test's file and name as `Ran:` and the commit as `Observed:`, and don't drive it. Driving every scenario cost one app project 31 minutes a change, for one defect in 28 runs.
21
+
22
+ A change that delivers no scenarios, such as a prefactor that keeps behaviour, is proved by the suite that passed at the checks commit. Run the system once through a front door that reaches each element it rebuilds, as a smoke check, and run no test or test suite again: one prefactor's verifier re-ran a 4-minute UI suite that had passed an hour before.
23
+ 4. **Probe one step further.** For each scenario you ran, try the nearest thing a user would plausibly do next or get wrong, such as the same command twice, a typo in the input, or an interrupt. Report surprises; they are not failures of the scenario.
21
24
  5. **Clean up** everything you created outside the project.
22
25
 
23
- When a scenario needs something you don't have, such as real hardware or a remote host, don't fake it. Mark it `could not run`, and say exactly what it needs, so a person can run it by hand.
26
+ When a scenario needs something you don't have, such as real hardware or a remote host, don't fake it. Mark it `could not run`, and say exactly what it needs, so a person can run it by hand. The same holds for time: don't idle through a timer, a scheduled notification or a device settling. Drive time through the project's seam for it when there is one; otherwise a wait over 30 seconds is `could not run`, with the wait a person needs.
24
27
 
25
28
  ## Report
26
29
 
@@ -33,3 +33,5 @@ Triage starts from a symptom and ends with a fix, or with the right step to fix
33
33
  - Code defect fixed: `land`, which lands bug fixes too.
34
34
  - Spec gap: `architect "<tweak>"`.
35
35
  - Design flaw: `rethink`.
36
+
37
+ Start it in a new session, so its context starts empty.
@@ -16,7 +16,7 @@ Verify answers one question: does this change work when someone uses it? Green t
16
16
  - When one fails, fix the code in a commit and run them again. When a fix needs design, stop and offer `rethink`.
17
17
  3. **Check the dependencies.** Follow "Checking" in `.agents/skills/cruze-dependency-approval/SKILL.md` against the base commit. An unrecorded dependency is a blocker.
18
18
  4. **Run the verifier.** Run the verifier role from `.agents/skills/cruze-roles/SKILL.md` for the change, with the mechanical checks' results and the commit they ran at. 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.
19
- 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, running the tests each fix touches, and `test` when `commands:` has a `test_full`. Then check the fixes in round 2.
19
+ 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, running the tests each fix touches, and `test` when `commands:` has a `test_full`. Then check the fixes yourself, as round 2 in the roles skill says.
20
20
  6. **Write the manual test script** as `manual-test.md` in the change folder, following [manual-test.md](manual-test.md). Commit it.
21
21
  7. **Check the fixes once.** When code changed since the last `checks` entry, run the format, lint and build commands and the full suite once more, and record them as in step 2. Done when the last `checks` entry is at a commit with no code changes since.
22
22
  8. **Report.** Show the user four things:
@@ -25,9 +25,9 @@ Verify answers one question: does this change work when someone uses it? Green t
25
25
  - The review's outcome, with each finding's disposition.
26
26
  - The path to `manual-test.md`, with a recommendation to run it before accepting.
27
27
  9. **Ask for the decision:** "Do you accept this change, or send it back?"
28
- 10. **Record it** with `cruze journal add verification --set result=accepted --set summary="<one line>" --item <ref>`, or `result=sent-back`. On sent-back, say what goes back:
29
- - A defect in code a task already built: `cruze task reopen T<n> "<reason>"`, then `build`.
30
- - A design that can't deliver the scenario: `rethink`.
28
+ 10. **Record it** with `cruze journal add verification --set result=accepted --set summary="<one line>" --item <ref>`, or `--set result=sent-back --set back_to=<build|rethink>`. On sent-back, say what goes back:
29
+ - A defect in code a task already built: `back_to=build`, then `cruze task reopen T<n> "<reason>"`.
30
+ - A design that can't deliver the scenario: `back_to=rethink`. Leave the tasks to the rethink, which reopens the ones its amendment changes, and `cruze next` names `rethink` until it is recorded.
31
31
 
32
32
  Done when the decision is recorded.
33
33
 
@@ -41,3 +41,5 @@ Done when the decision is recorded.
41
41
 
42
42
  - Accepted: `land`.
43
43
  - Sent back: `build` for reopened tasks, or `rethink` for design.
44
+
45
+ Start it in a new session, so its context starts empty.
@@ -13,6 +13,8 @@ export interface CliContext extends ProjectDeps {
13
13
  runner: AgentRunner;
14
14
  /** Name of the working directory, the default project name. */
15
15
  directoryName: string;
16
+ /** The working directory relative to the project root, empty at the root; file arguments are relative to it. */
17
+ workingDirectory: string;
16
18
  }
17
19
 
18
20
  /** What a command produces: a JSON result for agents, a summary for people, and whether it passed. */
@@ -1,9 +1,10 @@
1
+ import { posix } from "node:path";
1
2
  import { checkCode, traceTests } from "../../../../app/use_cases/check_code.ts";
2
3
  import { landChange } from "../../../../app/use_cases/land_change.ts";
3
4
  import type { CliContext, CommandResult, Options } from "../cli_context.ts";
4
5
 
5
6
  export async function runCheck(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
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"}.`);
@@ -15,7 +15,7 @@ export async function runJournal(context: CliContext, args: string[], options: O
15
15
  return { json: entry, human: `Recorded ${entry.event}.` };
16
16
  }
17
17
  if (args[0] === "list") {
18
- const entries = await listEvents(context, options.event, options.item);
18
+ const entries = await listEvents(context, options.event, options.item, options.all);
19
19
  return { json: entries, human: entries.map((e) => `${e.at} ${e.event} ${JSON.stringify({ ...e, at: undefined, event: undefined, by: undefined })}`).join("\n") || "No entries." };
20
20
  }
21
21
  throw new UsageError(`cruze journal takes add or list, not "${args[0]}"`);
@@ -0,0 +1,32 @@
1
+ import { showScope, type ScopeReport } from "../../../../app/use_cases/show_scope.ts";
2
+ import type { ShownElement } from "../../../../domain/project/scope_view.ts";
3
+ import { requireArgs, type CliContext, type CommandResult } from "../cli_context.ts";
4
+
5
+ export async function runShow(context: CliContext, args: string[]): Promise<CommandResult> {
6
+ requireArgs(args, ["ref"]);
7
+ const report = await showScope(context, args[0] ?? "");
8
+ return { json: report, human: render(report) };
9
+ }
10
+
11
+ /** The scope as a document an agent reads in place of the living docs. */
12
+ function render(report: ScopeReport): string {
13
+ const lines = [`# ${report.ref}${report.feature === undefined ? "" : ` (feature ${report.feature})`}`, ""];
14
+ lines.push("## Elements", "", ...blocks(report.elements), "## Scenarios", "", ...blocks(report.scenarios));
15
+ lines.push("## Cited, not shown", "");
16
+ lines.push(...(report.cited.length === 0 ? ["None."] : report.cited.map((c) => `- ${c.id}: ${c.title} (${c.source})`)), "");
17
+ if (report.feature === undefined) lines.push("## Settled decisions", "", ...(report.decisions.length === 0 ? ["None."] : report.decisions), "");
18
+ else {
19
+ lines.push("## Settled decisions of the feature", "", ...(report.decisions.length === 0 ? ["None."] : report.decisions), "");
20
+ lines.push("## Settled decisions of the change", "", ...(report.changeDecisions.length === 0 ? ["None."] : report.changeDecisions), "");
21
+ }
22
+ lines.push("## Dispositions", "");
23
+ lines.push(...(report.dispositions.length === 0 ? ["None."] : report.dispositions.map((d) => `- [${d.disposition}, ${d.review}] ${d.finding}: ${d.reason}`)), "");
24
+ if (report.references.length > 0) lines.push("## References", "", ...report.references.map((r) => `- \`${r.path}\`: ${r.use}`), "");
25
+ if (report.missing.length > 0) lines.push("## Missing", "", ...report.missing.map((id) => `- ${id} is defined nowhere`), "");
26
+ return lines.join("\n").trimEnd();
27
+ }
28
+
29
+ function blocks(elements: ShownElement[]): string[] {
30
+ if (elements.length === 0) return ["None.", ""];
31
+ return elements.flatMap((element) => [`<!-- ${element.source} -->`, ...element.body.join("\n").trimEnd().split("\n"), ""]);
32
+ }
@@ -32,9 +32,25 @@ export async function runStatus(context: CliContext, _args: string[], options: O
32
32
  }
33
33
  }
34
34
  for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
35
- return { json: report, human: lines.join("\n") };
35
+ const json = {
36
+ ...report,
37
+ documents: report.documents.map(compact),
38
+ features: report.features.map((f) => ({ ...f, approval: compact(f.approval), changes: f.changes.map((c) => ({ ...c, approval: compact(c.approval) })) })),
39
+ standalone: report.standalone.map((c) => ({ ...c, approval: compact(c.approval) })),
40
+ };
41
+ return { json, human: lines.join("\n") };
36
42
  }
37
43
 
44
+ /** The state without the record's hashes: an agent reads status often, and the hashes were a third of each read. */
45
+ function compact(status: ApprovalStatus): Omit<ApprovalStatus, "record"> & { approvedAt?: string; by?: string; basis?: string } {
46
+ const { record, ...rest } = status;
47
+ if (record === undefined) return rest;
48
+ return { ...rest, approvedAt: record.approvedAt, by: record.by, ...(record.basis === undefined ? {} : { basis: record.basis }) };
49
+ }
50
+
51
+ /** Every step starts a new session: one session that ran plan, build, verify and land reached 745k tokens of context. */
52
+ const FRESH_SESSION = "Run it in a new session, so its context starts empty.";
53
+
38
54
  const BASIS: Record<string, string> = { "plan-review": " (by the agent, after its plan review)", rethink: " (re-stamped by the agent after a rethink)" };
39
55
 
40
56
  function describe(label: string, status: ApprovalStatus): string {
@@ -46,6 +62,6 @@ function describe(label: string, status: ApprovalStatus): string {
46
62
  export async function runNext(context: CliContext): Promise<CommandResult> {
47
63
  const report = await suggestNext(context);
48
64
  const describe = (s: { step: string; target?: string; reason: string }): string => `${s.step}${s.target === undefined ? "" : ` ${s.target}`}: ${s.reason}`;
49
- const lines = [`Next: ${describe(report.next)}`, ...report.also.map((s) => `Also: ${describe(s)}`)];
50
- return { json: report, human: lines.join("\n") };
65
+ const lines = [`Next: ${describe(report.next)}`, ...report.also.map((s) => `Also: ${describe(s)}`), FRESH_SESSION];
66
+ return { json: { ...report, session: FRESH_SESSION }, human: lines.join("\n") };
51
67
  }
@@ -7,6 +7,7 @@ import { runRealign } from "./commands/realign.ts";
7
7
  import { runFeedback, runJournal } from "./commands/journal.ts";
8
8
  import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.ts";
9
9
  import { runReviewCommand } from "./commands/review.ts";
10
+ import { runShow } from "./commands/show.ts";
10
11
  import { runNext, runStatus } from "./commands/status.ts";
11
12
 
12
13
  /** A command, and the options it takes: one list, or one per subcommand for a command that has them. */
@@ -22,6 +23,7 @@ export const COMMANDS: Record<string, Command> = {
22
23
  validate: { handler: runValidate, options: [], usage: "validate check every document against the formats" },
23
24
  status: { handler: runStatus, options: ["gate", "override", "overlap", "change"], usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
24
25
  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" },
25
27
  approve: {
26
28
  handler: runApprove,
27
29
  options: ["replan", "rebase", "by-agent", "restamp-unchanged"],
@@ -35,7 +37,7 @@ export const COMMANDS: Record<string, Command> = {
35
37
  task: { handler: runTask, options: { done: ["change", "commit"], deviation: ["change"], reopen: ["change"] }, usage: "task <done|deviation|reopen> <T#> [text] [--change <ref>] [--commit <sha>]" },
36
38
  features: { handler: runFeatures, options: { add: ["summary", "goals"], update: ["summary"], drop: ["reason"] }, usage: "features <add|update|drop> <slug> [--summary <text>] [--goals <ids>] [--reason <text>]" },
37
39
  roadmap: { handler: runRoadmap, options: { prune: [] }, usage: "roadmap prune clear landed items from the roadmap status when a release closes" },
38
- journal: { handler: runJournal, options: { add: ["set", "item"], list: ["event", "item"] }, usage: "journal <add <event> --set key=value... [--item <ref>]|list [--event <event>] [--item <ref>]>" },
40
+ journal: { handler: runJournal, options: { add: ["set", "item"], list: ["event", "item", "all"] }, usage: "journal <add <event> --set key=value... [--item <ref>]|list [--event <event>] [--item <ref>] [--all]>" },
39
41
  review: { handler: runReviewCommand, options: ["item", "round", "blockers", "base"], usage: "review <role> [--item <ref>] [--round 1|2 --blockers <file>] [--base <commit>] run a role in a fresh agent context" },
40
42
  feedback: { handler: runFeedback, options: { export: ["out", "no-redact"] }, usage: "feedback export [--out <file>] [--no-redact]" },
41
43
  trace: { handler: runTrace, options: ["all", "change"], usage: "trace [--all] [--change <ref>] link scenarios to the tests that prove them" },
@@ -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
  import type { Repository } from "../../app/ports/repository.ts";
4
4
 
@@ -37,13 +37,24 @@ export class GitRepository implements Repository {
37
37
  }
38
38
 
39
39
  async filesOnBranch(branch: string, dir: string): Promise<Map<string, string>> {
40
- const files = new Map<string, string>();
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]] as [string, string]])));
46
+ }
47
+
48
+ /** The contents of each object, in order, through one `git cat-file --batch`; undefined for one that is missing. */
49
+ private catFiles(objects: string[]): Promise<Array<string | undefined>> {
50
+ return new Promise((resolve) => {
51
+ const child = spawn("git", ["cat-file", "--batch"], { cwd: this.root, stdio: ["pipe", "pipe", "ignore"] });
52
+ const chunks: Buffer[] = [];
53
+ child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
54
+ child.on("error", () => resolve(objects.map(() => undefined)));
55
+ child.on("close", () => resolve(parseBatch(Buffer.concat(chunks), objects.length)));
56
+ child.stdin.end(`${objects.join("\n")}\n`);
57
+ });
47
58
  }
48
59
 
49
60
  private async git(...args: string[]): Promise<string | null> {
@@ -55,3 +66,25 @@ export class GitRepository implements Repository {
55
66
  }
56
67
  }
57
68
  }
69
+
70
+ /** Splits `git cat-file --batch` output: a `<sha> <type> <size>` header, the bytes and a newline, or `<name> missing`. */
71
+ function parseBatch(output: Buffer, count: number): Array<string | undefined> {
72
+ const results: Array<string | undefined> = [];
73
+ let at = 0;
74
+ while (results.length < count && at < output.length) {
75
+ const headerEnd = output.indexOf(10, at);
76
+ if (headerEnd === -1) break;
77
+ const header = output.subarray(at, headerEnd).toString("utf8");
78
+ const size = /^[0-9a-f]+ \w+ (\d+)$/.exec(header)?.[1];
79
+ if (size === undefined) {
80
+ results.push(undefined);
81
+ at = headerEnd + 1;
82
+ continue;
83
+ }
84
+ const start = headerEnd + 1;
85
+ results.push(output.subarray(start, start + Number(size)).toString("utf8"));
86
+ at = start + Number(size) + 1;
87
+ }
88
+ while (results.length < count) results.push(undefined);
89
+ return results;
90
+ }
@@ -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: string): string | undefined {
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
+ }
@@ -15,6 +15,8 @@ import { isPlanned, readChangesTable } from "../../domain/project/plan_parts.ts"
15
15
  import { readProgress } from "../../domain/project/progress.ts";
16
16
  import { approvalFolder, approvalsPath, type ProjectView } from "../../domain/project/project_view.ts";
17
17
  import { featureOf } from "../../domain/project/work_items.ts";
18
+ import type { Problem } from "../../domain/validation/problem.ts";
19
+ import { SIZE_RULES } from "../../domain/validation/size_rules.ts";
18
20
  import { validateProject } from "../../domain/validation/validate_project.ts";
19
21
  import { appendJournal, loadView, withFiles, type ProjectDeps } from "../project_context.ts";
20
22
 
@@ -87,11 +89,13 @@ export async function approveArtifact(deps: ProjectDeps, ref: string, options: A
87
89
  if (text !== view.snapshot.get(path)) edits.set(path, text);
88
90
  view = withFiles(view, edits);
89
91
 
90
- const errors = validateProject(view).filter((p) => p.path === path && p.severity === "error");
92
+ const problems = validateProject(view).filter((p) => p.path === path);
93
+ const errors = problems.filter((p) => p.severity === "error");
91
94
  if (errors.length > 0) {
92
95
  const listing = errors.map((p) => ` ${p.path}:${p.line ?? ""} ${p.message}`).join("\n");
93
96
  throw new CruzeError("invalid", `${path} has ${errors.length} problem(s) to fix before approval:\n${listing}`);
94
97
  }
98
+ requireSizesAccepted(problems, path);
95
99
 
96
100
  const { hash, upstream } = fingerprint(view, path, parseMarkdown(text));
97
101
  const folder = approvalFolder(view, path);
@@ -154,6 +158,20 @@ function requireRebased(view: ProjectView, deltaIds: Set<string>, previous: Appr
154
158
  }
155
159
  }
156
160
 
161
+ /**
162
+ * A size warning stands until the work is split or the user accepts it under `changes.exceptions`.
163
+ * A 28-task change approved over its warning went on to 26 deviations, so approval waits for one or the other.
164
+ */
165
+ function requireSizesAccepted(problems: Problem[], path: string): void {
166
+ const standing = problems.filter((p) => SIZE_RULES.includes(p.rule));
167
+ if (standing.length === 0) return;
168
+ const listing = standing.map((p) => ` ${p.message} [${p.rule}]`).join("\n");
169
+ throw new CruzeError(
170
+ "size-unaccepted",
171
+ `${path} has ${standing.length} size warning(s). Split the work through rethink, or record the user's acceptance under changes.exceptions in .cruze/config.yaml, as the item's ref and their reason:\n${listing}`,
172
+ );
173
+ }
174
+
157
175
  function notWork(path: string): never {
158
176
  throw new CruzeError("needs-user", `${path} is a project document, so the user approves it`);
159
177
  }
@@ -6,6 +6,7 @@ import { FIRST_RELEASE, isPending } from "../../domain/versions/rule_versions.ts
6
6
  import type { CruzeConfig } from "../../domain/config.ts";
7
7
  import type { ProjectView } from "../../domain/project/project_view.ts";
8
8
  import { resolveChange } from "../../domain/project/artifact_ref.ts";
9
+ import { readDelta } from "../../domain/project/deltas.ts";
9
10
  import { activeChange } from "../../domain/status/work_status.ts";
10
11
  import { traceScenarios, type TraceReport } from "../../domain/trace.ts";
11
12
  import { loadView, type ProjectDeps } from "../project_context.ts";
@@ -88,8 +89,14 @@ function globBase(glob: string): string {
88
89
  return fixed.join("/");
89
90
  }
90
91
 
92
+ /**
93
+ * Every module's Path: the living docs' modules, and the modules that work in progress adds or
94
+ * changes in its architecture delta, since their files are written before land merges them.
95
+ */
91
96
  function modulePaths(view: ProjectView): string[] {
92
- return [...view.living.elements.values()]
93
- .filter((e) => e.id.startsWith("MOD-"))
94
- .flatMap((e) => (factsOf(e.doc, e.element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")));
97
+ const living = [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-")).map((e) => ({ doc: e.doc, element: e.element }));
98
+ const inFlight = view.items
99
+ .filter((item) => item.kind !== "change" && !item.archived)
100
+ .flatMap((item) => readDelta(item.doc).entries.filter((entry) => entry.element.id.startsWith("MOD-") && entry.element.op !== "REMOVED").map((entry) => ({ doc: item.doc, element: entry.element })));
101
+ return [...living, ...inFlight].flatMap(({ doc, element }) => (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")));
95
102
  }
@@ -18,16 +18,19 @@ export async function recordEvent(deps: ProjectDeps, event: string, fields: Reco
18
18
  return appendJournal(deps, folder, event, { ...recorded, cruze: deps.bundle.version });
19
19
  }
20
20
 
21
- /** The journal, or one feature's or change's part of it: its own journal, and a feature's changes' too. */
22
- export async function listEvents(deps: ProjectDeps, event?: string, item?: string): Promise<JournalEntry[]> {
21
+ /**
22
+ * The journal of the work in flight and the project, or of one feature or change (a feature's changes' too).
23
+ * Landed work's entries are history, listed only with `all`, since every entry ever recorded is too much to read.
24
+ */
25
+ export async function listEvents(deps: ProjectDeps, event?: string, item?: string, all = false): Promise<JournalEntry[]> {
23
26
  const view = await loadView(deps.files);
24
- let entries = view.journal;
27
+ let folders = [".cruze", ...view.items.filter((i) => all || !i.archived).map((i) => i.folder)];
25
28
  if (item !== undefined) {
26
29
  const resolved = resolveArtifact(view, item).item;
27
30
  if (resolved === undefined) throw new CruzeError("not-found", `"${item}" is not a feature or change`);
28
- const parts = [resolved, ...view.items.filter((i) => resolved.kind === "feature" && i.featureRef === resolved.ref)];
29
- entries = parts.flatMap((part) => parseJournal(view.snapshot.get(journalPath(part.folder))).entries).sort((a, b) => a.at.localeCompare(b.at));
31
+ folders = [resolved, ...view.items.filter((i) => resolved.kind === "feature" && i.featureRef === resolved.ref)].map((part) => part.folder);
30
32
  }
33
+ const entries = folders.flatMap((folder) => parseJournal(view.snapshot.get(journalPath(folder))).entries).sort((a, b) => a.at.localeCompare(b.at));
31
34
  return event === undefined ? entries : entries.filter((entry) => entry.event === event);
32
35
  }
33
36