@printpractical/cruze 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +41 -17
  2. package/dist/adapters/inbound/cli/cli_context.js +1 -0
  3. package/dist/adapters/inbound/cli/commands/code.js +2 -1
  4. package/dist/adapters/inbound/cli/commands/install.js +4 -0
  5. package/dist/adapters/inbound/cli/commands/realign.js +28 -0
  6. package/dist/adapters/inbound/cli/commands/review.js +1 -1
  7. package/dist/adapters/inbound/cli/commands.js +3 -1
  8. package/dist/adapters/inbound/cli/parse_command.js +2 -0
  9. package/dist/adapters/inbound/cli/run_cli.js +9 -0
  10. package/dist/adapters/outbound/node_project_files.js +2 -1
  11. package/dist/adapters/outbound/process_agent_runner.js +8 -3
  12. package/dist/app/ports/agent_runner.js +5 -6
  13. package/dist/app/project_context.js +4 -2
  14. package/dist/app/use_cases/check_code.js +8 -3
  15. package/dist/app/use_cases/init_project.js +31 -3
  16. package/dist/app/use_cases/install_skills.js +8 -1
  17. package/dist/app/use_cases/journal_events.js +4 -3
  18. package/dist/app/use_cases/land_change.js +27 -6
  19. package/dist/app/use_cases/realign_done.js +45 -0
  20. package/dist/app/use_cases/realign_status.js +106 -0
  21. package/dist/app/use_cases/record_version.js +56 -0
  22. package/dist/app/use_cases/run_review.js +26 -12
  23. package/dist/app/use_cases/validate_project.js +10 -4
  24. package/dist/domain/check/check_source.js +23 -2
  25. package/dist/domain/config.js +18 -8
  26. package/dist/domain/journal.js +29 -5
  27. package/dist/domain/project/layout.js +1 -0
  28. package/dist/domain/scaffold.js +15 -1
  29. package/dist/domain/status/next_step.js +19 -2
  30. package/dist/domain/validation/plan_rules.js +18 -1
  31. package/dist/domain/validation/project_rules.js +17 -1
  32. package/dist/domain/validation/validate_project.js +2 -1
  33. package/dist/domain/validation/work_item_rules.js +7 -0
  34. package/dist/domain/versions/ci_pin.js +40 -0
  35. package/dist/domain/versions/cruze_version.js +51 -0
  36. package/dist/domain/versions/realign_notes.js +72 -0
  37. package/dist/domain/versions/rule_versions.js +22 -0
  38. package/dist/domain/versions/scheduled_exceptions.js +41 -0
  39. package/package.json +1 -1
  40. package/skills/architect/feature.md +4 -2
  41. package/skills/behavioural-testing/SKILL.md +6 -0
  42. package/skills/build/SKILL.md +1 -1
  43. package/skills/envision/project.md +4 -2
  44. package/skills/formats/reference/config.md +7 -3
  45. package/skills/formats/reference/work-items.md +1 -1
  46. package/skills/hexagonal-design/SKILL.md +7 -1
  47. package/skills/hexagonal-design/languages/rust.md +7 -2
  48. package/skills/land/SKILL.md +1 -1
  49. package/skills/next/SKILL.md +1 -0
  50. package/skills/plan/tasks.md +7 -3
  51. package/skills/plan/test-plan.md +1 -1
  52. package/skills/realign/SKILL.md +35 -0
  53. package/skills/realign/notes/0.0.2.md +69 -0
  54. package/skills/realign/notes/0.0.3.md +11 -0
  55. package/skills/realign/notes.md +30 -0
  56. package/skills/research/SKILL.md +2 -1
  57. package/skills/rethink/SKILL.md +3 -1
  58. package/skills/rethink/levels.md +1 -1
  59. package/skills/roles/SKILL.md +5 -2
  60. package/skills/roles/auditor.md +35 -0
  61. package/skills/roles/code-reviewer.md +4 -3
  62. package/skills/roles/design-reviewer.md +9 -6
  63. package/skills/roles/researcher.md +1 -0
  64. package/src/adapters/inbound/cli/cli_context.ts +1 -0
  65. package/src/adapters/inbound/cli/commands/code.ts +2 -1
  66. package/src/adapters/inbound/cli/commands/install.ts +4 -0
  67. package/src/adapters/inbound/cli/commands/realign.ts +28 -0
  68. package/src/adapters/inbound/cli/commands/review.ts +1 -1
  69. package/src/adapters/inbound/cli/commands.ts +3 -1
  70. package/src/adapters/inbound/cli/parse_command.ts +2 -0
  71. package/src/adapters/inbound/cli/run_cli.ts +9 -0
  72. package/src/adapters/outbound/node_project_files.ts +2 -1
  73. package/src/adapters/outbound/process_agent_runner.ts +8 -3
  74. package/src/app/ports/agent_runner.ts +5 -6
  75. package/src/app/project_context.ts +4 -2
  76. package/src/app/use_cases/check_code.ts +8 -3
  77. package/src/app/use_cases/init_project.ts +31 -3
  78. package/src/app/use_cases/install_skills.ts +8 -1
  79. package/src/app/use_cases/journal_events.ts +4 -3
  80. package/src/app/use_cases/land_change.ts +27 -6
  81. package/src/app/use_cases/realign_done.ts +45 -0
  82. package/src/app/use_cases/realign_status.ts +106 -0
  83. package/src/app/use_cases/record_version.ts +56 -0
  84. package/src/app/use_cases/run_review.ts +26 -12
  85. package/src/app/use_cases/validate_project.ts +10 -4
  86. package/src/domain/check/check_source.ts +23 -2
  87. package/src/domain/config.ts +18 -8
  88. package/src/domain/journal.ts +29 -5
  89. package/src/domain/project/layout.ts +1 -0
  90. package/src/domain/scaffold.ts +15 -1
  91. package/src/domain/status/next_step.ts +19 -2
  92. package/src/domain/validation/plan_rules.ts +18 -1
  93. package/src/domain/validation/project_rules.ts +17 -1
  94. package/src/domain/validation/validate_project.ts +2 -1
  95. package/src/domain/validation/work_item_rules.ts +7 -0
  96. package/src/domain/versions/ci_pin.ts +40 -0
  97. package/src/domain/versions/cruze_version.ts +51 -0
  98. package/src/domain/versions/realign_notes.ts +72 -0
  99. package/src/domain/versions/rule_versions.ts +22 -0
  100. package/src/domain/versions/scheduled_exceptions.ts +41 -0
  101. package/templates/init/config.yaml +5 -6
  102. package/templates/init/review/claude.yaml +7 -0
  103. package/templates/init/review/other.yaml +7 -0
  104. package/templates/init/CLAUDE.md +0 -1
@@ -0,0 +1,30 @@
1
+ # Realign notes
2
+
3
+ Each release that changes what Cruze expects of a project ships a notes file, `notes/<version>.md`. `cruze realign status` reads the notes between the project's `standards:` and the installed version.
4
+
5
+ ## Releases
6
+
7
+ - [0.0.2](notes/0.0.2.md)
8
+ - [0.0.3](notes/0.0.3.md)
9
+
10
+ ## Format
11
+
12
+ A note is a `###` heading with an ID and a title, then facts:
13
+
14
+ ```markdown
15
+ ### test-placement: Rust tests live in tests/
16
+ - Applies to: existing code
17
+ - Rule: `.agents/skills/cruze-hexagonal-design/languages/rust.md`, "Dependencies and tooling"
18
+ - Detect: check:test-placement
19
+ - Migrate: move `src/<module>/tests.rs` to `tests/<module>.rs`
20
+ ```
21
+
22
+ | Fact | Holds |
23
+ | --- | --- |
24
+ | `Applies to` | `existing code`, when work that met the last release must change; `new work only`, when only future work follows it |
25
+ | `Rule` | The skill file and heading that state the rule |
26
+ | `Detect` | The CLI rule that finds it, as `check:<rule>` or `validate:<rule>` |
27
+ | `Ask` | The question the auditor answers when no CLI rule can |
28
+ | `Migrate` | How existing work changes to meet it |
29
+
30
+ A note for existing code has `Migrate`, and `Detect` or `Ask`.
@@ -17,8 +17,9 @@ Run these steps for each component in a design that isn't domain logic. Domain l
17
17
  4. Recommend one. Adopt a library, or build with a concrete reason: the behaviour is specific to this product, no candidate meets a stated constraint, every candidate brings disproportionate risk or complexity, or the capability is the product itself. Avoiding a dependency is not a reason on its own.
18
18
  5. Record the result as a row in the `## Adopt or build` table of the feature or standalone change, as Component, Decision, Choice and Reason. At project scope, record it as the adapter's `Adopts` fact, and put rows for anything no adapter owns in the walking-skeleton change's `## Adopt or build`. When the evidence runs longer than the reason, write it to `.cruze/notes/<date>-<slug>.md` and cite that note in the reason.
19
19
  6. Take the recommendation to the user. Adopting means a new dependency, which follows `.agents/skills/cruze-dependency-approval/SKILL.md`. Building commodity functionality is the user's call as well.
20
+ 7. Verify what the design will rely on. List each claim the design makes about an adopted library or tool: the features or flags it needs, and each command the design spells out, such as a CI step. Once the user has approved the dependency, check each claim by running it: build a scratch project outside the repository with exactly the declared features, and run each command there or with `--help`. When a claim can't be run, check it in that version's source. Write the features into the `Adopts` fact or the row's Choice.
20
21
 
21
- Done when every non-domain component in the design has a row with a decision and a reason.
22
+ Done when every non-domain component in the design has a row with a decision and a reason, and every claim the design relies on is verified, with how.
22
23
 
23
24
  ## Answering a question
24
25
 
@@ -24,10 +24,12 @@ Finding a design flaw mid-build is information, not failure. Rethink amends the
24
24
  ```sh
25
25
  cruze journal add rethink --set level=<task|change|feature|architecture|vision> --set kind=<defect|discovery> \
26
26
  --set summary="<what changed>" --set wrong="<document and element, and what was wrong>" \
27
- --set caught_by="<for a defect, the step that should have caught it; for a discovery, the step that found it>" \
27
+ --set found_by=<step> [--set missed_by=<step>] \
28
28
  --set agent="<the agent you run in>" [--item <ref>]
29
29
  ```
30
30
 
31
+ `found_by` is the step that found it. A defect also needs `missed_by`, the earliest step whose job was to catch it. Both take one of `envision`, `architect`, `research`, `design-review`, `walkthrough`, `plan`, `plan-review`, `build`, `code-review`, `verify`, `land`, `validate`, `check` or `user`.
32
+
31
33
  Done when `cruze status` shows every document approved and current, or names only work the user chose to leave for later.
32
34
 
33
35
  ## Levels
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## Vision
32
32
 
33
- 1. Edit `docs/vision.md` with the user: goals, non-goals or constraints. The feature map changes only through `cruze features add` and `cruze features drop`.
33
+ 1. Edit `docs/vision.md` with the user: goals, non-goals or constraints. The feature map changes only through `cruze features add` and `cruze features drop`. When the problem, users or goals changed, update the overview under the title of `README.md` to match.
34
34
  2. Update `docs/roadmap.md` to match, following `.agents/skills/cruze-roadmap/SKILL.md`.
35
35
  3. For each feature in progress that served a dropped goal, the user decides: keep it, or stop it with `cruze abandon <ref> --reason "<why>"`. Abandoning archives the folder and puts the feature back on the future list; changes that already landed stay.
36
36
  4. Re-approve with `cruze approve vision`, then `cruze approve roadmap`.
@@ -13,12 +13,13 @@ A role is a prompt with fixed inputs and a report as its only output. It runs in
13
13
  | Code reviewer | [code-reviewer.md](code-reviewer.md) | verify, for the code review |
14
14
  | Verifier | [verifier.md](verifier.md) | verify, to run the system |
15
15
  | Researcher | [researcher.md](researcher.md) | architect, for adopt-or-build decisions |
16
+ | Auditor | [auditor.md](auditor.md) | realign, to compare existing code with what a release now expects |
16
17
 
17
18
  ## Running a role
18
19
 
19
20
  1. Read the role file and gather exactly the inputs it lists. Done when you have every input path, plus any command output the role asks for.
20
21
  2. Start a fresh context: a helper agent with no conversation history, where the agent can start one. Give it the role file's full text and the inputs the role lists, including draft documents it names, and nothing else. Your own opinions, the conversation and your reasons for the design stay out.
21
- 3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier` or `researcher`. 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. If that can't run either, the reviewers and the verifier 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.
22
+ 3. If the agent can't start a helper, run `cruze review <role> --item <ref>`, where the role is `design-reviewer`, `code-reviewer`, `verifier`, `researcher` or `auditor`. Add `--base <commit>` for the code under review. It starts the agent configured under `review:` in `.cruze/config.yaml` in a fresh context and prints the role's report. When no agent is configured, or it isn't installed, it reports `status: run-in-helper` with the role's full prompt instead: start a helper, such as a subagent, and give it exactly that prompt. If no fresh context can run, the reviewers, the verifier and the auditor must still run fresh: tell the user, and have them run the role in a new session. The researcher may run in your own context, since its evidence is checkable; say that you did.
22
23
  4. Take the report as it comes back. Check its evidence against the files before you act on any finding.
23
24
 
24
25
  ## Running a review
@@ -28,12 +29,14 @@ A review has at most two rounds. Only blockers can force the second one.
28
29
  ### Round 1
29
30
 
30
31
  1. Commit the documents under review (`docs: <artifact> for design review`), so the fixes in round 2 have a diff. Then run the reviewer on the complete artifact.
31
- 2. Record the round: `cruze journal add review --set review=<design|plan|code> --set round=1 --set blockers=<n> --set concerns=<n>`, adding `--item <ref>` for a feature or change.
32
+ 2. Record the round: `cruze journal add review --set review=<design|plan|code> --set round=1 --set blockers=<n> --set concerns=<n> --set nits=<n>`, with the counts from the report's last line, adding `--item <ref>` for a feature or change.
32
33
  3. Present every finding to the user with your recommended disposition and why:
33
34
  - `fixed`: you will change the design as the finding proposes.
34
35
  - `waived`: the finding is right, but the user accepts the risk.
35
36
  - `deferred`: it belongs to later work. When it is future scope, add it with `cruze features add <slug> --summary <text> --goals <ids>`.
36
37
  - `rejected`: the finding is wrong, with the reason.
38
+
39
+ A finding whose fix would change an approved element, such as one marked `Needs design change`, is never `deferred` or `waived`. Recommend `rethink` at that element's level now. Once the rethink is done and the code follows it, record the finding as `fixed`.
37
40
  4. The user decides each one. Record each decision: `cruze journal add disposition --set finding="<rubric item>: <summary>" --set disposition=<fixed|waived|deferred|rejected> --set reason="<reason>" --set review=<design|plan|code>`, with the same `--item`. A recorded disposition is never raised again.
38
41
  5. Apply every `fixed` change, then run `cruze validate`. Done when it reports no errors.
39
42
  6. When a finding could be caught by a tool, such as a layering, naming or size rule, tell the user which `cruze check` rule or linter setting would catch it next time.
@@ -0,0 +1,35 @@
1
+ # Auditor
2
+
3
+ You compare a project's existing code and documents with what the installed Cruze now expects, and report every place they differ. You report findings, and you edit nothing.
4
+
5
+ ## Inputs
6
+
7
+ - The notes to audit, from `cruze realign status`: each note's ID, `Rule`, `Ask` and `Migrate`.
8
+ - In a full audit, the standards instead of notes: `.agents/skills/cruze-hexagonal-design/SKILL.md` and its language file, `.agents/skills/cruze-behavioural-testing/SKILL.md`, and `.agents/skills/cruze-formats/reference/architecture.md`. You audit one module, named with its `Path`.
9
+ - `docs/architecture.md`, for the module map and the elements the code implements.
10
+ - The files in scope: every source and test file under the project's `source` and `tests` globs in `.cruze/config.yaml`, or under the module's `Path`.
11
+ - The settled ledger, as `.agents/skills/cruze-roles/SKILL.md` defines it, and the earlier realign dispositions from `cruze journal list --event disposition`. Never raise a finding they waived.
12
+
13
+ ## Work
14
+
15
+ 1. For each note, answer its `Ask` for every file in scope, not a sample. Work module by module.
16
+ 2. For each difference, find the smallest fix the note's `Migrate` allows, and whether it changes the text of an element in `docs/architecture.md`.
17
+ 3. Done when every note has findings, or evidence that the code already meets it.
18
+
19
+ ## Findings
20
+
21
+ Group findings by note, in this shape:
22
+
23
+ ```markdown
24
+ ### <n>. <note ID>: <one-line summary>
25
+ - Evidence: <path:line, quoting the code>
26
+ - Standard: <the rule text it departs from>
27
+ - Fix: <the specific change>
28
+ - Design change: <the element in docs/architecture.md whose text must change, or `no`>
29
+ ```
30
+
31
+ List every finding. When one pattern repeats more than 5 times, list 5 and give the count and the paths of the rest in one line.
32
+
33
+ ## Report
34
+
35
+ End with one line: `Findings: <n> across <m> notes.` For each note with no findings, give the evidence that the code meets it.
@@ -20,7 +20,7 @@ Review against these items and nothing else. Each finding names one.
20
20
 
21
21
  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
22
  2. **Failures.** Each failure scenario produces the error it specifies and leaves the state it specifies.
23
- 3. **Scope.** The code adds no behaviour the change's scope doesn't deliver. Unrequested behaviour is a finding, even when it looks useful.
23
+ 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
24
  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
25
 
26
26
  ### Architecture lane
@@ -28,7 +28,7 @@ Review against these items and nothing else. Each finding names one.
28
28
  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
29
  6. **Direction.** Dependencies point inward. No technology type crosses a port. Inbound adapters hold no domain rules or workflow.
30
30
  7. **Size.** No file holds two responsibilities or passes its budget without a recorded exception.
31
- 8. **Idiom.** The code is idiomatic for the language, uses its standard tooling, and follows the language file's rules.
31
+ 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
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
33
  10. **Contracts.** Each adapter honours its port's `Operations`, and each fake passes the same contract tests as the real adapter.
34
34
 
@@ -42,9 +42,10 @@ Each finding has this shape:
42
42
  - Evidence: <path:line, quoting the code>
43
43
  - Failure scenario: <what goes wrong, concretely, when this runs>
44
44
  - Proposed fix: <the specific change>
45
+ - Needs design change: <the approved element whose text the fix must change, such as a port's `Operations`, or `no`>
45
46
  ```
46
47
 
47
- - A **blocker** means a scenario fails or is untested, a protected test was weakened, a layering rule is broken, or an unapproved dependency was added. Everything else is a **concern**.
48
+ - A **blocker** means a scenario fails or is untested, a protected test was weakened, a layering rule is broken, an unapproved dependency was added, or an adapter can't honour its port's `Operations`. A finding that needs a design change is a blocker. Everything else is a **concern**.
48
49
  - List every blocker. List at most 5 concerns, the most important first. Count the rest as nits without listing them.
49
50
  - Drop any finding that contradicts the settled ledger. The only exception is a blocker with new evidence, which you mark `New evidence:` and explain.
50
51
  - When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
@@ -20,13 +20,16 @@ Review against these items and nothing else. Each finding names one.
20
20
 
21
21
  1. **Behaviour.** Every requirement has at least one scenario. Every scenario has concrete values, one WHEN, and a THEN a person could check. Failure behaviour the user will meet has its own scenario.
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
- 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.
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. 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.
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
26
  6. **Contracts.** Every new or changed port's `Operations` state what each operation guarantees, how it fails, and who owns cleanup.
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` and the accepted ADRs, or changes them explicitly through a `MODIFIED` operation or a new ADR.
29
- 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 is the thinnest useful tracer bullet. At project scope, the roadmap starts with the walking skeleton, and every goal in the release is covered.
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:
29
+ - each use case's `Output` and `Errors` against the `FLOW`s that serve it and the `XC` rules that apply to it;
30
+ - each adapter's duties against the port it implements and the `XC` rules it follows;
31
+ - each guarantee, such as "always fits" or "never returns", against the limits and failures that bound it.
32
+ 9. **Slicing.** Each change is a vertical slice that delivers scenarios end to end and can land on its own. The order respects `Depends on`, and the first change is the thinnest useful tracer bullet. 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.
30
33
  10. **Ambiguity.** Name each point where a fresh implementer could reasonably build something different from what the user expects.
31
34
 
32
35
  ## Plan rubric
@@ -34,7 +37,7 @@ Review against these items and nothing else. Each finding names one.
34
37
  When the artifact is a change's plan, review against these items instead.
35
38
 
36
39
  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.
37
- 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.
40
+ 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.
38
41
  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.
39
42
  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`.
40
43
  5. **Risks and overlap.** The riskiest task is named, with how it is checked. Overlap with work on other branches is resolved or accepted.
@@ -52,7 +55,7 @@ Each finding has this shape:
52
55
  - Proposed fix: <the specific edit>
53
56
  ```
54
57
 
55
- - A **blocker** means building from the design as written would produce wrong behaviour, a layering violation or an untestable scenario. Everything else is a **concern**.
58
+ - A **blocker** means building from the design as written would produce wrong behaviour, a layering violation or an untestable scenario. Two elements that contradict each other are always a blocker, because an implementer has to pick one. Everything else is a **concern**.
56
59
  - List every blocker. List at most 5 concerns, the most important first. Count the rest as nits without listing them.
57
60
  - Drop any finding that contradicts the settled ledger. The only exception is a blocker with new evidence, which you mark `New evidence:` and explain.
58
61
  - When a finding could be caught mechanically, add `Check: <the cruze check rule or linter setting that would catch it>`.
@@ -26,6 +26,7 @@ For components, report one block each:
26
26
 
27
27
  Evidence: <the evaluation table for the serious candidates, with sources and the date checked>
28
28
  New dependency: yes or no
29
+ Claims to verify: <each feature, flag or command the design would rely on, with where the source shows it>
29
30
  ```
30
31
 
31
32
  For a question, report the answer, its evidence (version or commit, and file and line or URL) and what you couldn't verify.
@@ -48,6 +48,7 @@ export interface Options {
48
48
  gate?: string;
49
49
  override?: string;
50
50
  overlap: boolean;
51
+ full: boolean;
51
52
  replan: string[];
52
53
  rebase: string[];
53
54
  round?: string;
@@ -5,7 +5,7 @@ import type { CliContext, CommandResult, Options } from "../cli_context.ts";
5
5
  export async function runCheck(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
6
6
  const files = [...options.file, ...args];
7
7
  const report = await checkCode(context, { ci: options.ci, ...(files.length === 0 ? {} : { files }) });
8
- const lines = report.findings.map((f) => `${f.severity} ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message} [${f.rule}]`);
8
+ const lines = report.findings.map((f) => `${f.severity} ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message} [${f.rule}]${f.pending === true ? " (pending realign)" : ""}`);
9
9
  lines.push(`Checked ${report.checked} file(s): ${report.passed ? "passed" : "failed"}.`);
10
10
  return { json: report, human: lines.join("\n"), failed: !report.passed };
11
11
  }
@@ -25,6 +25,7 @@ export async function runLand(context: CliContext, args: string[], options: Opti
25
25
  if (report.built.length > 0) lines.push(`Now built: ${report.built.join(", ")}`);
26
26
  if (report.removed.length > 0) lines.push(`Removed: ${report.removed.join(", ")}`);
27
27
  if (report.restamped.length > 0) lines.push(`Re-stamped approvals: ${report.restamped.join(", ")}`);
28
+ if (report.unexcepted.length > 0) lines.push(`CI checks these again, now that the change realign scheduled them in has landed: ${report.unexcepted.join(", ")}`);
28
29
  if (report.archivedTo !== undefined) lines.push(`Finished; archived to ${report.archivedTo}.`);
29
30
  return { json: report, human: lines.join("\n") };
30
31
  }
@@ -11,5 +11,9 @@ export function describeSkillInstall(report: InstallSkillsReport): string[] {
11
11
  const lines = [`Installed ${count} Cruze skill${count === 1 ? "" : "s"} into .agents/skills/: ${report.installed.join(", ")}`];
12
12
  if (report.removed.length > 0) lines.push(`Removed skills no longer shipped: ${report.removed.join(", ")}`);
13
13
  for (const link of report.linked) lines.push(`Linked them for ${link.agent} in ${link.dir}/`);
14
+ const { from, to, ci } = report.recorded;
15
+ if (from !== to) lines.push(`Recorded Cruze ${to} in .cruze/config.yaml${from === undefined ? "" : `, up from ${from}`}`);
16
+ if (ci.repinned.length > 0) lines.push(`CI runs ${ci.repinned.join(", ")}`);
17
+ for (const spec of ci.unrecognised) lines.push(`CI runs ${spec}, which Cruze can't move to ${to}; update .github/workflows/ci.yml yourself`);
14
18
  return lines;
15
19
  }
@@ -0,0 +1,28 @@
1
+ import { realignDone } from "../../../../app/use_cases/realign_done.ts";
2
+ import { realignStatus } from "../../../../app/use_cases/realign_status.ts";
3
+ import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
4
+
5
+ export async function runRealign(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
6
+ requireArgs(args, ["status|done"]);
7
+ if (args[0] === "status") {
8
+ const report = await realignStatus(context, { full: options.full });
9
+ const lines = [`Code meets Cruze ${report.standards}; the installed skills are ${report.target}${report.full ? " (full audit)" : ""}.`];
10
+ for (const note of report.notes) {
11
+ const open = note.findings.filter((f) => !f.waived);
12
+ const how = note.detect !== undefined ? `${open.length} open finding(s) from ${note.detect}` : `for the auditor: ${note.ask ?? ""}`;
13
+ lines.push(`${note.version} ${note.id}: ${note.title} (${how})`);
14
+ for (const f of open) lines.push(` ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message}`);
15
+ }
16
+ if (report.newWorkOnly.length > 0) lines.push(`New work only, nothing to move: ${report.newWorkOnly.join(", ")}`);
17
+ if (report.notes.length === 0) lines.push("Nothing to realign.");
18
+ return { json: report, human: lines.join("\n") };
19
+ }
20
+ if (args[0] === "done") {
21
+ const report = await realignDone(context, options.change === undefined ? {} : { change: options.change });
22
+ const lines = [report.from === report.to ? `The code already meets Cruze ${report.to}.` : `The code now meets Cruze ${report.to}, up from ${report.from}.`];
23
+ if (report.scheduled !== undefined) lines.push(`${report.scheduled} fixes the rest.`);
24
+ if (report.excepted.length > 0) lines.push(`Listed under check.exceptions until it lands: ${report.excepted.join(", ")}`);
25
+ return { json: report, human: lines.join("\n") };
26
+ }
27
+ throw new UsageError(`cruze realign takes status or done, not "${args[0]}"`);
28
+ }
@@ -14,5 +14,5 @@ export async function runReviewCommand(context: CliContext, args: string[], opti
14
14
  ...(blockers === undefined ? {} : { blockers }),
15
15
  ...(options.base === undefined ? {} : { base: options.base }),
16
16
  });
17
- return { json: report, human: report.report };
17
+ return { json: report, human: report.status === "reported" ? report.report : `${report.instruction}\n\n${report.prompt}` };
18
18
  }
@@ -3,13 +3,14 @@ import { runCheck, runLand, runTrace } from "./commands/code.ts";
3
3
  import { runApprove, runNew, runValidate } from "./commands/documents.ts";
4
4
  import { runInit } from "./commands/init.ts";
5
5
  import { runInstall } from "./commands/install.ts";
6
+ import { runRealign } from "./commands/realign.ts";
6
7
  import { runFeedback, runJournal } from "./commands/journal.ts";
7
8
  import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.ts";
8
9
  import { runReviewCommand } from "./commands/review.ts";
9
10
  import { runNext, runStatus } from "./commands/status.ts";
10
11
 
11
12
  export const COMMANDS: Record<string, { handler: Handler; usage: string }> = {
12
- init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent <agent>] [--package <npm spec>] set up this repository and install the skills" },
13
+ init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent claude|other] [--package <npm spec>] set up this repository and install the skills" },
13
14
  install: { handler: runInstall, usage: "install [--agent <agent>] install or update the Cruze skills" },
14
15
  validate: { handler: runValidate, usage: "validate check every document against the formats" },
15
16
  status: { handler: runStatus, usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
@@ -26,6 +27,7 @@ export const COMMANDS: Record<string, { handler: Handler; usage: string }> = {
26
27
  check: { handler: runCheck, usage: "check [<file>...] [--ci] enforce layer rules and file budgets" },
27
28
  abandon: { handler: runAbandon, usage: "abandon <ref> --reason <text> archive a feature or standalone change that stops for good" },
28
29
  land: { handler: runLand, usage: "land [<ref>] [--override <reason>] merge a verified change into the living docs" },
30
+ realign: { handler: runRealign, usage: "realign <status [--full]|done [--change <ref>]> what the installed Cruze expects of existing code, and recording that the code meets it" },
29
31
  };
30
32
 
31
33
  export const USAGE = `Usage: cruze <command> [options]
@@ -41,6 +41,7 @@ export function parseCommand(argv: string[]): ParsedCommand {
41
41
  gate: { type: "string" },
42
42
  override: { type: "string" },
43
43
  overlap: { type: "boolean", default: false },
44
+ full: { type: "boolean", default: false },
44
45
  round: { type: "string" },
45
46
  package: { type: "string" },
46
47
  replan: { type: "string", multiple: true, default: [] },
@@ -65,6 +66,7 @@ export function parseCommand(argv: string[]): ParsedCommand {
65
66
  file: values.file,
66
67
  ci: values.ci,
67
68
  overlap: values.overlap,
69
+ full: values.full,
68
70
  replan: values.replan,
69
71
  rebase: values.rebase,
70
72
  ...optional("name", values.name),
@@ -1,3 +1,4 @@
1
+ import { versionNotice } from "../../../app/use_cases/record_version.ts";
1
2
  import { CruzeError } from "../../../domain/cruze_error.ts";
2
3
  import { type CliContext, UsageError } from "./cli_context.ts";
3
4
  import { COMMANDS, USAGE } from "./commands.ts";
@@ -10,12 +11,17 @@ export interface Terminal {
10
11
  stdoutIsTerminal: boolean;
11
12
  }
12
13
 
14
+ /** Commands that set the version themselves, or report the skew as a problem of their own. */
15
+ const NO_VERSION_NOTICE = new Set(["init", "install", "validate"]);
16
+
13
17
  /**
14
18
  * Runs one CLI invocation and returns the exit code: 0 on success, 1 when a check fails or
15
19
  * an expected error occurs, 2 for usage errors. Agents get JSON on stdout; people get a summary on stderr.
20
+ * When the project's skills don't match this CLI, a note on stderr says so, even when the command fails.
16
21
  */
17
22
  export async function runCli(argv: string[], context: CliContext, terminal: Terminal): Promise<number> {
18
23
  let json = !terminal.stdoutIsTerminal || argv.includes("--json");
24
+ let notice: string | undefined;
19
25
  try {
20
26
  const parsed = parseCommand(argv);
21
27
  json ||= parsed.options.json;
@@ -30,6 +36,7 @@ export async function runCli(argv: string[], context: CliContext, terminal: Term
30
36
  }
31
37
  const command = COMMANDS[name];
32
38
  if (command === undefined) throw new UsageError(`Unknown command: ${name}`);
39
+ if (!NO_VERSION_NOTICE.has(name)) notice = await versionNotice(context.files, context.bundle.version);
33
40
  const result = await command.handler(context, args, parsed.options);
34
41
  terminal.stderr(result.human);
35
42
  if (json) terminal.stdout(JSON.stringify(result.json, null, 2));
@@ -46,5 +53,7 @@ export async function runCli(argv: string[], context: CliContext, terminal: Term
46
53
  return 1;
47
54
  }
48
55
  throw error;
56
+ } finally {
57
+ if (notice !== undefined) terminal.stderr(`cruze: note: ${notice}`);
49
58
  }
50
59
  }
@@ -78,7 +78,8 @@ export class NodeProjectFiles implements ProjectFiles {
78
78
  this.assertInsideRoot(resolve(dirname(linkPath), target));
79
79
  await mkdir(dirname(linkPath), { recursive: true });
80
80
  await rm(linkPath, { recursive: true, force: true });
81
- await symlink(target, linkPath, "dir");
81
+ // No link type: on Windows, Node picks a file or directory link from what the target is.
82
+ await symlink(target, linkPath);
82
83
  }
83
84
 
84
85
  /** Resolves a project-relative path, refusing anything outside the root. */
@@ -11,15 +11,20 @@ export class ProcessAgentRunner implements AgentRunner {
11
11
 
12
12
  run(command: string[], prompt: string): Promise<AgentRun> {
13
13
  const [program, ...args] = command;
14
- if (program === undefined) return Promise.resolve({ exitCode: 127, output: "", error: "no command configured" });
14
+ if (program === undefined) return Promise.resolve({ kind: "missing" });
15
15
  return new Promise((resolve) => {
16
16
  const child = spawn(program, args, { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"] });
17
17
  let output = "";
18
18
  let error = "";
19
19
  child.stdout.on("data", (chunk: Buffer) => (output += chunk.toString("utf8")));
20
20
  child.stderr.on("data", (chunk: Buffer) => (error += chunk.toString("utf8")));
21
- child.on("error", (failure) => resolve({ exitCode: 127, output, error: `${error}${failure.message}` }));
22
- child.on("close", (code) => resolve({ exitCode: code ?? 1, output, error }));
21
+ // Spawning reports ENOENT when the program isn't on the PATH.
22
+ child.on("error", (failure: NodeJS.ErrnoException) =>
23
+ resolve(failure.code === "ENOENT" ? { kind: "missing" } : { kind: "finished", exitCode: 127, output, error: `${error}${failure.message}` }),
24
+ );
25
+ child.on("close", (code) => resolve({ kind: "finished", exitCode: code ?? 1, output, error }));
26
+ // A program that never started closes stdin; ignore the write error, since the spawn error reports it.
27
+ child.stdin.on("error", () => {});
23
28
  child.stdin.end(prompt);
24
29
  });
25
30
  }
@@ -1,11 +1,10 @@
1
1
  /** Starts a coding agent in a fresh context, non-interactively, and collects what it reports. */
2
2
  export interface AgentRunner {
3
- /** Runs `command` in the project root with `prompt` on standard input. */
3
+ /**
4
+ * Runs `command` in the project root with `prompt` on standard input. Reports `missing`,
5
+ * without running anything, when the command's program isn't installed.
6
+ */
4
7
  run(command: string[], prompt: string): Promise<AgentRun>;
5
8
  }
6
9
 
7
- export interface AgentRun {
8
- exitCode: number;
9
- output: string;
10
- error: string;
11
- }
10
+ export type AgentRun = { kind: "finished"; exitCode: number; output: string; error: string } | { kind: "missing" };
@@ -1,5 +1,5 @@
1
1
  import { buildProjectView, type ProjectView } from "../domain/project/project_view.ts";
2
- import { SNAPSHOT_ROOTS } from "../domain/project/layout.ts";
2
+ import { PATHS, SNAPSHOT_ROOTS } from "../domain/project/layout.ts";
3
3
  import { journalPath, serializeEntry, type JournalEntry } from "../domain/journal.ts";
4
4
  import type { Bundle } from "./ports/bundle.ts";
5
5
  import type { Clock } from "./ports/clock.ts";
@@ -14,7 +14,7 @@ export interface ProjectDeps {
14
14
  bundle: Bundle;
15
15
  }
16
16
 
17
- /** Reads every document under docs/ and .cruze/ into a view. */
17
+ /** Reads every document under docs/ and .cruze/, and the README, into a view. */
18
18
  export async function loadView(files: ProjectFiles): Promise<ProjectView> {
19
19
  const snapshot = new Map<string, string>();
20
20
  for (const root of SNAPSHOT_ROOTS) {
@@ -23,6 +23,8 @@ export async function loadView(files: ProjectFiles): Promise<ProjectView> {
23
23
  if (text !== undefined) snapshot.set(path, text);
24
24
  }
25
25
  }
26
+ const readme = await files.readText(PATHS.readme);
27
+ if (readme !== undefined) snapshot.set(PATHS.readme, readme);
26
28
  return buildProjectView(snapshot);
27
29
  }
28
30
 
@@ -2,6 +2,7 @@ import { factsOf } from "../../domain/elements.ts";
2
2
  import { checkSource, type CheckFinding } from "../../domain/check/check_source.ts";
3
3
  import { CruzeError } from "../../domain/cruze_error.ts";
4
4
  import { matchesAny } from "../../domain/glob.ts";
5
+ import { FIRST_RELEASE, isPending } from "../../domain/versions/rule_versions.ts";
5
6
  import type { CruzeConfig } from "../../domain/config.ts";
6
7
  import type { ProjectView } from "../../domain/project/project_view.ts";
7
8
  import { resolveChange } from "../../domain/project/artifact_ref.ts";
@@ -18,7 +19,8 @@ export interface CheckReport {
18
19
 
19
20
  /**
20
21
  * Enforces layer rules and file budgets. Layer violations always fail; budget and
21
- * module-map warnings fail only with `ci`.
22
+ * module-map warnings fail only with `ci`. A rule newer than the project's `standards:`
23
+ * is pending: it reports, but fails nothing until the project realigns.
22
24
  */
23
25
  export async function checkCode(deps: ProjectDeps, options: { files?: string[]; ci: boolean }): Promise<CheckReport> {
24
26
  const view = await loadView(deps.files);
@@ -32,8 +34,11 @@ export async function checkCode(deps: ProjectDeps, options: { files?: string[];
32
34
  }
33
35
  }
34
36
  const only = options.files?.filter((file) => sources.has(file));
35
- const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) });
36
- const failing = findings.filter((f) => f.severity === "error" || options.ci);
37
+ const standards = config.standards ?? FIRST_RELEASE;
38
+ const findings = checkSource({ config, sources, modulePaths: modulePaths(view), ...(only === undefined ? {} : { only }) }).map((f) =>
39
+ isPending("check", f.rule, standards) ? { ...f, pending: true as const } : f,
40
+ );
41
+ const failing = findings.filter((f) => f.pending !== true && (f.severity === "error" || options.ci));
37
42
  return { passed: failing.length === 0, checked: only?.length ?? sources.size, findings };
38
43
  }
39
44
 
@@ -1,5 +1,7 @@
1
+ import { CruzeError } from "../../domain/cruze_error.ts";
2
+ import { agentSkillHome } from "../../domain/agent_skill_homes.ts";
1
3
  import { parseProjectName } from "../../domain/project_name.ts";
2
- import { INIT_SCAFFOLD, renderTemplate } from "../../domain/scaffold.ts";
4
+ import { CLAUDE_INSTRUCTIONS, INIT_AGENTS, INIT_SCAFFOLD, type InitAgent, renderTemplate, reviewSettingTemplate } from "../../domain/scaffold.ts";
3
5
  import type { Bundle } from "../ports/bundle.ts";
4
6
  import type { ProjectFiles } from "../ports/project_files.ts";
5
7
  import type { Prompter } from "../ports/prompter.ts";
@@ -10,6 +12,7 @@ export interface InitProjectRequest {
10
12
  name?: string;
11
13
  /** Offered as the default answer when asking for the name. */
12
14
  defaultName: string;
15
+ /** From `--agent`: `claude`, `other`, or agents to link skills for. Asked for when it names neither. */
13
16
  agents: string[];
14
17
  /**
15
18
  * How CI runs this version of Cruze through npx, such as `github:PrintPractical/cruze#v0.0.1`
@@ -20,6 +23,7 @@ export interface InitProjectRequest {
20
23
 
21
24
  export interface InitProjectReport {
22
25
  project: string;
26
+ agent: InitAgent;
23
27
  created: string[];
24
28
  /** Files that already existed and were left untouched. */
25
29
  skipped: string[];
@@ -36,11 +40,13 @@ export async function initProject(
36
40
  ): Promise<InitProjectReport> {
37
41
  const { files, bundle, prompter } = deps;
38
42
  const project = parseProjectName(request.name ?? (await prompter.ask("Project name", request.defaultName)));
43
+ const agent = await chooseAgent(files, prompter, request.agents);
39
44
  const values = {
40
45
  project_name: project,
41
46
  project_name_quoted: JSON.stringify(project),
42
47
  cruze_version: bundle.version,
43
48
  cruze_package: request.package ?? `@printpractical/cruze@${bundle.version}`,
49
+ review_setting: (await bundle.template(reviewSettingTemplate(agent))).trimEnd(),
44
50
  };
45
51
 
46
52
  const created: string[] = [];
@@ -53,7 +59,29 @@ export async function initProject(
53
59
  await files.writeText(entry.path, renderTemplate(await bundle.template(entry.template), values));
54
60
  created.push(entry.path);
55
61
  }
62
+ if (agent === "claude") {
63
+ if (await files.exists(CLAUDE_INSTRUCTIONS.path)) skipped.push(CLAUDE_INSTRUCTIONS.path);
64
+ else {
65
+ await files.link(CLAUDE_INSTRUCTIONS.path, CLAUDE_INSTRUCTIONS.target);
66
+ created.push(CLAUDE_INSTRUCTIONS.path);
67
+ }
68
+ }
56
69
 
57
- const skills = await installSkills({ files, bundle }, { agents: request.agents });
58
- return { project, created, skipped, skills };
70
+ const linkFor = new Set(request.agents.filter((a) => a !== "other"));
71
+ if (agent === "claude") linkFor.add("claude");
72
+ const skills = await installSkills({ files, bundle }, { agents: [...linkFor] });
73
+ return { project, agent, created, skipped, skills };
74
+ }
75
+
76
+ /** The agent named with `--agent`, or shown by the project's files, or else the answer to asking. */
77
+ async function chooseAgent(files: ProjectFiles, prompter: Prompter, requested: string[]): Promise<InitAgent> {
78
+ if (requested.includes("claude")) return "claude";
79
+ if (requested.includes("other")) return "other";
80
+ for (const marker of agentSkillHome("claude")?.markers ?? []) {
81
+ if (await files.exists(marker)) return "claude";
82
+ }
83
+ const answer = (await prompter.ask(`Coding agent (${INIT_AGENTS.join(" or ")})`, "claude")).trim().toLowerCase();
84
+ const agent = INIT_AGENTS.find((a) => a === answer);
85
+ if (agent === undefined) throw new CruzeError("unknown-agent", `Unknown agent: ${answer}. Answer ${INIT_AGENTS.join(" or ")}, or pass --agent.`);
86
+ return agent;
59
87
  }
@@ -4,6 +4,7 @@ import { AGENT_SKILL_HOMES, SHARED_SKILLS_DIR, agentSkillHome, knownAgents } fro
4
4
  import { SKILL_FILE, installedSkillName, isInstalledSkillName, skillProblems } from "../../domain/skill.ts";
5
5
  import type { Bundle, BundledSkill } from "../ports/bundle.ts";
6
6
  import type { ProjectFiles } from "../ports/project_files.ts";
7
+ import { recordVersion, refuseDowngrade, type VersionRecord } from "./record_version.ts";
7
8
 
8
9
  export interface InstallSkillsRequest {
9
10
  /** Agents to link skills for even when the project shows no sign of using them. */
@@ -15,12 +16,16 @@ export interface InstallSkillsReport {
15
16
  installed: string[];
16
17
  removed: string[];
17
18
  linked: Array<{ agent: string; dir: string }>;
19
+ /** The version recorded in .cruze/config.yaml, and the CI workflow's Cruze pin. */
20
+ recorded: VersionRecord;
18
21
  }
19
22
 
20
23
  /**
21
24
  * Replaces the project's Cruze skills with the bundled set. Skills are written to
22
25
  * the shared Agent Skills directory, and linked for every agent the project uses.
23
26
  * Skills without the Cruze prefix belong to the user and are never touched.
27
+ * The project then records this version, and its CI runs it too. An older CLI
28
+ * never replaces the skills of a project that moved to a newer one.
24
29
  */
25
30
  export async function installSkills(
26
31
  deps: { files: ProjectFiles; bundle: Bundle },
@@ -29,6 +34,7 @@ export async function installSkills(
29
34
  const { files, bundle } = deps;
30
35
  const skills = await bundle.skills();
31
36
  assertValid(skills);
37
+ await refuseDowngrade(files, bundle.version);
32
38
 
33
39
  const names = skills.map((skill) => installedSkillName(skill.folder));
34
40
  const removed = await removeStaleSkills(files, SHARED_SKILLS_DIR, names);
@@ -48,7 +54,8 @@ export async function installSkills(
48
54
  linked.push({ agent, dir: home.skillsDir });
49
55
  }
50
56
 
51
- return { version: bundle.version, installed: names, removed, linked };
57
+ const recorded = await recordVersion(files, bundle.version);
58
+ return { version: bundle.version, installed: names, removed, linked, recorded };
52
59
  }
53
60
 
54
61
  function assertValid(skills: BundledSkill[]): void {