@printpractical/cruze 0.0.1 → 0.0.2

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