@printpractical/cruze 0.0.1

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 (248) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +167 -0
  3. package/bin/cruze.mjs +17 -0
  4. package/dist/adapters/inbound/cli/cli_context.js +75 -0
  5. package/dist/adapters/inbound/cli/commands/code.js +30 -0
  6. package/dist/adapters/inbound/cli/commands/documents.js +52 -0
  7. package/dist/adapters/inbound/cli/commands/init.js +23 -0
  8. package/dist/adapters/inbound/cli/commands/install.js +15 -0
  9. package/dist/adapters/inbound/cli/commands/journal.js +31 -0
  10. package/dist/adapters/inbound/cli/commands/progress.js +51 -0
  11. package/dist/adapters/inbound/cli/commands/review.js +18 -0
  12. package/dist/adapters/inbound/cli/commands/status.js +45 -0
  13. package/dist/adapters/inbound/cli/commands.js +36 -0
  14. package/dist/adapters/inbound/cli/parse_command.js +90 -0
  15. package/dist/adapters/inbound/cli/run_cli.js +50 -0
  16. package/dist/adapters/outbound/git_repository.js +57 -0
  17. package/dist/adapters/outbound/node_project_files.js +101 -0
  18. package/dist/adapters/outbound/package_bundle.js +56 -0
  19. package/dist/adapters/outbound/process_agent_runner.js +26 -0
  20. package/dist/adapters/outbound/system_clock.js +14 -0
  21. package/dist/adapters/outbound/terminal_prompter.js +22 -0
  22. package/dist/app/ports/agent_runner.js +11 -0
  23. package/dist/app/ports/bundle.js +22 -0
  24. package/dist/app/ports/clock.js +6 -0
  25. package/dist/app/ports/project_files.js +20 -0
  26. package/dist/app/ports/prompter.js +5 -0
  27. package/dist/app/ports/repository.js +13 -0
  28. package/dist/app/project_context.js +45 -0
  29. package/dist/app/use_cases/abandon_work.js +42 -0
  30. package/dist/app/use_cases/approve_artifact.js +146 -0
  31. package/dist/app/use_cases/check_code.js +90 -0
  32. package/dist/app/use_cases/init_project.js +59 -0
  33. package/dist/app/use_cases/install_skills.js +93 -0
  34. package/dist/app/use_cases/journal_events.js +48 -0
  35. package/dist/app/use_cases/land_change.js +113 -0
  36. package/dist/app/use_cases/new_project_doc.js +40 -0
  37. package/dist/app/use_cases/new_work_item.js +75 -0
  38. package/dist/app/use_cases/project_status.js +73 -0
  39. package/dist/app/use_cases/record_progress.js +101 -0
  40. package/dist/app/use_cases/run_review.js +71 -0
  41. package/dist/app/use_cases/validate_project.js +22 -0
  42. package/dist/domain/agent_skill_homes.js +25 -0
  43. package/dist/domain/approvals/evaluate.js +36 -0
  44. package/dist/domain/approvals/fingerprint.js +52 -0
  45. package/dist/domain/approvals/records.js +50 -0
  46. package/dist/domain/architecture_sections.js +23 -0
  47. package/dist/domain/check/check_source.js +64 -0
  48. package/dist/domain/check/imports.js +81 -0
  49. package/dist/domain/check/rust_imports.js +84 -0
  50. package/dist/domain/check/type_count.js +17 -0
  51. package/dist/domain/config.js +128 -0
  52. package/dist/domain/cruze_error.js +10 -0
  53. package/dist/domain/edits/agents_file.js +44 -0
  54. package/dist/domain/edits/managed_tables.js +60 -0
  55. package/dist/domain/edits/progress_block.js +33 -0
  56. package/dist/domain/edits/status_lines.js +52 -0
  57. package/dist/domain/elements.js +126 -0
  58. package/dist/domain/glob.js +46 -0
  59. package/dist/domain/hashing.js +52 -0
  60. package/dist/domain/ids.js +37 -0
  61. package/dist/domain/journal.js +62 -0
  62. package/dist/domain/land/bookkeeping.js +54 -0
  63. package/dist/domain/land/merge_plan.js +132 -0
  64. package/dist/domain/land/text_ops.js +66 -0
  65. package/dist/domain/markdown.js +79 -0
  66. package/dist/domain/markdown_tables.js +34 -0
  67. package/dist/domain/project/artifact_ref.js +42 -0
  68. package/dist/domain/project/deltas.js +49 -0
  69. package/dist/domain/project/layout.js +40 -0
  70. package/dist/domain/project/living_model.js +54 -0
  71. package/dist/domain/project/new_items.js +48 -0
  72. package/dist/domain/project/plan_parts.js +111 -0
  73. package/dist/domain/project/progress.js +43 -0
  74. package/dist/domain/project/project_view.js +72 -0
  75. package/dist/domain/project/roadmap.js +43 -0
  76. package/dist/domain/project/work_items.js +68 -0
  77. package/dist/domain/project_name.js +20 -0
  78. package/dist/domain/scaffold.js +29 -0
  79. package/dist/domain/skill.js +106 -0
  80. package/dist/domain/skill_links.js +103 -0
  81. package/dist/domain/status/next_step.js +119 -0
  82. package/dist/domain/status/verification.js +16 -0
  83. package/dist/domain/status/work_status.js +101 -0
  84. package/dist/domain/trace.js +44 -0
  85. package/dist/domain/validation/delta_rules.js +79 -0
  86. package/dist/domain/validation/element_rules.js +99 -0
  87. package/dist/domain/validation/living_rules.js +97 -0
  88. package/dist/domain/validation/plan_rules.js +105 -0
  89. package/dist/domain/validation/problem.js +21 -0
  90. package/dist/domain/validation/project_rules.js +99 -0
  91. package/dist/domain/validation/validate_project.js +31 -0
  92. package/dist/domain/validation/work_item_rules.js +144 -0
  93. package/dist/main.js +32 -0
  94. package/package.json +40 -0
  95. package/scripts/build.mjs +37 -0
  96. package/skills/about/SKILL.md +21 -0
  97. package/skills/architect/SKILL.md +43 -0
  98. package/skills/architect/feature.md +29 -0
  99. package/skills/architect/project.md +58 -0
  100. package/skills/architect/tweak.md +22 -0
  101. package/skills/architect/walkthrough.md +29 -0
  102. package/skills/behavioural-testing/SKILL.md +63 -0
  103. package/skills/behavioural-testing/examples.md +102 -0
  104. package/skills/behavioural-testing/fakes.md +30 -0
  105. package/skills/build/SKILL.md +62 -0
  106. package/skills/dependency-approval/SKILL.md +48 -0
  107. package/skills/domain-language/SKILL.md +37 -0
  108. package/skills/envision/SKILL.md +29 -0
  109. package/skills/envision/feature.md +22 -0
  110. package/skills/envision/project.md +20 -0
  111. package/skills/explore/SKILL.md +39 -0
  112. package/skills/formats/SKILL.md +84 -0
  113. package/skills/formats/reference/architecture.md +80 -0
  114. package/skills/formats/reference/config.md +57 -0
  115. package/skills/formats/reference/durable-docs.md +71 -0
  116. package/skills/formats/reference/specs.md +65 -0
  117. package/skills/formats/reference/work-items.md +100 -0
  118. package/skills/formats/templates/adr.md +7 -0
  119. package/skills/formats/templates/architecture.md +100 -0
  120. package/skills/formats/templates/change.md +29 -0
  121. package/skills/formats/templates/feature.md +49 -0
  122. package/skills/formats/templates/glossary.md +8 -0
  123. package/skills/formats/templates/roadmap.md +32 -0
  124. package/skills/formats/templates/spec.md +11 -0
  125. package/skills/formats/templates/standalone-change.md +46 -0
  126. package/skills/formats/templates/vision.md +37 -0
  127. package/skills/grilling/SKILL.md +66 -0
  128. package/skills/hexagonal-design/SKILL.md +119 -0
  129. package/skills/hexagonal-design/contracts.md +68 -0
  130. package/skills/hexagonal-design/fundamentals.md +50 -0
  131. package/skills/hexagonal-design/languages/cpp.md +37 -0
  132. package/skills/hexagonal-design/languages/rust.md +40 -0
  133. package/skills/hexagonal-design/runtime.md +48 -0
  134. package/skills/land/SKILL.md +43 -0
  135. package/skills/next/SKILL.md +30 -0
  136. package/skills/plan/SKILL.md +52 -0
  137. package/skills/plan/tasks.md +26 -0
  138. package/skills/plan/test-plan.md +14 -0
  139. package/skills/research/SKILL.md +41 -0
  140. package/skills/research/evaluation.md +27 -0
  141. package/skills/rethink/SKILL.md +53 -0
  142. package/skills/rethink/levels.md +45 -0
  143. package/skills/roadmap/SKILL.md +51 -0
  144. package/skills/roles/SKILL.md +57 -0
  145. package/skills/roles/code-reviewer.md +55 -0
  146. package/skills/roles/design-reviewer.md +63 -0
  147. package/skills/roles/researcher.md +33 -0
  148. package/skills/roles/verifier.md +38 -0
  149. package/skills/triage/SKILL.md +35 -0
  150. package/skills/verify/SKILL.md +39 -0
  151. package/skills/verify/manual-test.md +23 -0
  152. package/src/adapters/inbound/cli/cli_context.ts +75 -0
  153. package/src/adapters/inbound/cli/commands/code.ts +30 -0
  154. package/src/adapters/inbound/cli/commands/documents.ts +52 -0
  155. package/src/adapters/inbound/cli/commands/init.ts +23 -0
  156. package/src/adapters/inbound/cli/commands/install.ts +15 -0
  157. package/src/adapters/inbound/cli/commands/journal.ts +31 -0
  158. package/src/adapters/inbound/cli/commands/progress.ts +51 -0
  159. package/src/adapters/inbound/cli/commands/review.ts +18 -0
  160. package/src/adapters/inbound/cli/commands/status.ts +45 -0
  161. package/src/adapters/inbound/cli/commands.ts +36 -0
  162. package/src/adapters/inbound/cli/parse_command.ts +90 -0
  163. package/src/adapters/inbound/cli/run_cli.ts +50 -0
  164. package/src/adapters/outbound/git_repository.ts +57 -0
  165. package/src/adapters/outbound/node_project_files.ts +101 -0
  166. package/src/adapters/outbound/package_bundle.ts +56 -0
  167. package/src/adapters/outbound/process_agent_runner.ts +26 -0
  168. package/src/adapters/outbound/system_clock.ts +14 -0
  169. package/src/adapters/outbound/terminal_prompter.ts +22 -0
  170. package/src/app/ports/agent_runner.ts +11 -0
  171. package/src/app/ports/bundle.ts +22 -0
  172. package/src/app/ports/clock.ts +6 -0
  173. package/src/app/ports/project_files.ts +20 -0
  174. package/src/app/ports/prompter.ts +5 -0
  175. package/src/app/ports/repository.ts +13 -0
  176. package/src/app/project_context.ts +45 -0
  177. package/src/app/use_cases/abandon_work.ts +42 -0
  178. package/src/app/use_cases/approve_artifact.ts +146 -0
  179. package/src/app/use_cases/check_code.ts +90 -0
  180. package/src/app/use_cases/init_project.ts +59 -0
  181. package/src/app/use_cases/install_skills.ts +93 -0
  182. package/src/app/use_cases/journal_events.ts +48 -0
  183. package/src/app/use_cases/land_change.ts +113 -0
  184. package/src/app/use_cases/new_project_doc.ts +40 -0
  185. package/src/app/use_cases/new_work_item.ts +75 -0
  186. package/src/app/use_cases/project_status.ts +73 -0
  187. package/src/app/use_cases/record_progress.ts +101 -0
  188. package/src/app/use_cases/run_review.ts +71 -0
  189. package/src/app/use_cases/validate_project.ts +22 -0
  190. package/src/domain/agent_skill_homes.ts +25 -0
  191. package/src/domain/approvals/evaluate.ts +36 -0
  192. package/src/domain/approvals/fingerprint.ts +52 -0
  193. package/src/domain/approvals/records.ts +50 -0
  194. package/src/domain/architecture_sections.ts +23 -0
  195. package/src/domain/check/check_source.ts +64 -0
  196. package/src/domain/check/imports.ts +81 -0
  197. package/src/domain/check/rust_imports.ts +84 -0
  198. package/src/domain/check/type_count.ts +17 -0
  199. package/src/domain/config.ts +128 -0
  200. package/src/domain/cruze_error.ts +10 -0
  201. package/src/domain/edits/agents_file.ts +44 -0
  202. package/src/domain/edits/managed_tables.ts +60 -0
  203. package/src/domain/edits/progress_block.ts +33 -0
  204. package/src/domain/edits/status_lines.ts +52 -0
  205. package/src/domain/elements.ts +126 -0
  206. package/src/domain/glob.ts +46 -0
  207. package/src/domain/hashing.ts +52 -0
  208. package/src/domain/ids.ts +37 -0
  209. package/src/domain/journal.ts +62 -0
  210. package/src/domain/land/bookkeeping.ts +54 -0
  211. package/src/domain/land/merge_plan.ts +132 -0
  212. package/src/domain/land/text_ops.ts +66 -0
  213. package/src/domain/markdown.ts +79 -0
  214. package/src/domain/markdown_tables.ts +34 -0
  215. package/src/domain/project/artifact_ref.ts +42 -0
  216. package/src/domain/project/deltas.ts +49 -0
  217. package/src/domain/project/layout.ts +40 -0
  218. package/src/domain/project/living_model.ts +54 -0
  219. package/src/domain/project/new_items.ts +48 -0
  220. package/src/domain/project/plan_parts.ts +111 -0
  221. package/src/domain/project/progress.ts +43 -0
  222. package/src/domain/project/project_view.ts +72 -0
  223. package/src/domain/project/roadmap.ts +43 -0
  224. package/src/domain/project/work_items.ts +68 -0
  225. package/src/domain/project_name.ts +20 -0
  226. package/src/domain/scaffold.ts +29 -0
  227. package/src/domain/skill.ts +106 -0
  228. package/src/domain/skill_links.ts +103 -0
  229. package/src/domain/status/next_step.ts +119 -0
  230. package/src/domain/status/verification.ts +16 -0
  231. package/src/domain/status/work_status.ts +101 -0
  232. package/src/domain/trace.ts +44 -0
  233. package/src/domain/validation/delta_rules.ts +79 -0
  234. package/src/domain/validation/element_rules.ts +99 -0
  235. package/src/domain/validation/living_rules.ts +97 -0
  236. package/src/domain/validation/plan_rules.ts +105 -0
  237. package/src/domain/validation/problem.ts +21 -0
  238. package/src/domain/validation/project_rules.ts +99 -0
  239. package/src/domain/validation/validate_project.ts +31 -0
  240. package/src/domain/validation/work_item_rules.ts +144 -0
  241. package/src/main.ts +32 -0
  242. package/templates/init/AGENTS.md +28 -0
  243. package/templates/init/CHANGELOG.md +8 -0
  244. package/templates/init/CLAUDE.md +1 -0
  245. package/templates/init/README.md +1 -0
  246. package/templates/init/ci.yml +23 -0
  247. package/templates/init/config.yaml +33 -0
  248. package/templates/init/gitattributes +2 -0
@@ -0,0 +1,105 @@
1
+ import { factsOf, isUnbuilt } from "../elements.js";
2
+ import { findIds, kindOf } from "../ids.js";
3
+
4
+
5
+ import { isPlanned, readScope, readTasks, readTestPlan, } from "../project/plan_parts.js";
6
+
7
+ import { error, warning, } from "./problem.js";
8
+
9
+ const TEST_KINDS = ["behaviour", "contract", "domain", "smoke"];
10
+
11
+ /**
12
+ * Rules for one buildable change: its scope names only things that can be built. Once it is
13
+ * planned, every scope ID is proved by a task, every delivered scenario has a behaviour test,
14
+ * and every port a built adapter implements has a contract test.
15
+ */
16
+ export function changePlanProblems(
17
+ view ,
18
+ path ,
19
+ doc ,
20
+ delta ,
21
+ available ,
22
+ ) {
23
+ const scope = readScope(doc);
24
+ if (scope === null) return [error(path, undefined, "missing-section", 'the change needs a "## Scope" section')];
25
+ const problems = [...scopeTargetProblems(view, path, scope, delta.delta)];
26
+ if (!isPlanned(doc)) return [...problems, warning(path, undefined, "not-planned", "the change has no test plan or tasks yet; plan fills them")];
27
+ const { tasks, malformed } = readTasks(doc);
28
+ for (const line of malformed) {
29
+ problems.push(error(path, line, "task-format", "task lines read: - T<n>: `<owner>` in `<path>`[, `<path>`], proves <ID>[, <ID>]"));
30
+ }
31
+ if (tasks.length === 0 && malformed.length === 0) problems.push(error(path, undefined, "missing-tasks", "the change has no tasks"));
32
+
33
+ const proved = new Set(tasks.flatMap((task) => task.proves));
34
+ const taskPaths = tasks.flatMap((task) => task.paths);
35
+ for (const task of tasks) {
36
+ for (const id of task.proves.filter((id) => !available.has(id))) {
37
+ problems.push(error(path, task.line, "unknown-id", `${task.number} proves ${id}, which is not defined`));
38
+ }
39
+ }
40
+ for (const id of [...scope.delivers, ...scope.builds]) {
41
+ if (kindOf(id) === "MOD") {
42
+ const modulePaths = modulePathsOf(view, delta.doc, delta.delta, id);
43
+ if (!taskPaths.some((p) => modulePaths.some((m) => p.startsWith(m)))) {
44
+ problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}`));
45
+ }
46
+ } else if (kindOf(id) === "REQ") {
47
+ const scenarios = delta.delta.entries.find((e) => e.element.id === id)?.scenarios.map((s) => s.id) ?? [];
48
+ if (!proved.has(id) && !scenarios.some((s) => proved.has(s))) problems.push(error(path, scope.line, "unproved-scope", `no task proves ${id} or its scenarios`));
49
+ } else if (!proved.has(id)) {
50
+ problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task proves it`));
51
+ }
52
+ }
53
+
54
+ const rows = readTestPlan(doc);
55
+ for (const row of rows) {
56
+ if (!TEST_KINDS.includes(row.kind)) problems.push(error(path, row.line, "test-kind", `test kind must be one of ${TEST_KINDS.join(", ")}`));
57
+ const subject = findIds(row.subject)[0];
58
+ if (subject === undefined || !available.has(subject)) problems.push(error(path, row.line, "unknown-id", `test subject "${row.subject}" is not a defined ID`));
59
+ if (row.file === "") problems.push(error(path, row.line, "test-file", "every test plan row names its test file"));
60
+ }
61
+ for (const id of scope.delivers.filter((id) => kindOf(id) === "SCN")) {
62
+ if (!rows.some((row) => findIds(row.subject)[0] === id && row.kind === "behaviour")) {
63
+ problems.push(error(path, scope.line, "missing-behaviour-test", `${id} is delivered but has no behaviour row in the test plan`));
64
+ }
65
+ }
66
+ for (const adapter of scope.builds.filter((id) => kindOf(id) === "ADP")) {
67
+ const ports = (factsOfScoped(view, delta.doc, delta.delta, adapter).get("Implements") ?? []).flatMap(findIds).filter((id) => kindOf(id) === "PORT");
68
+ for (const port of ports.filter((p) => !rows.some((row) => findIds(row.subject)[0] === p && row.kind === "contract"))) {
69
+ problems.push(error(path, scope.line, "missing-contract-test", `${adapter} implements ${port} but the test plan has no contract row for ${port}`));
70
+ }
71
+ }
72
+ return problems;
73
+ }
74
+
75
+ /** Scope IDs must come from the item's deltas or be planned in the living docs. */
76
+ function scopeTargetProblems(view , path , scope , delta ) {
77
+ const problems = [];
78
+ for (const id of [...scope.delivers, ...scope.builds]) {
79
+ if (delta.ids.has(id)) continue;
80
+ const living = view.living.elements.get(id);
81
+ if (living === undefined) problems.push(error(path, scope.line, "unknown-id", `${id} is in scope but defined nowhere`));
82
+ else if (!isUnbuilt(living.doc, living.element)) {
83
+ problems.push(error(path, scope.line, "scope-not-planned", `${id} is already built; change it through a MODIFIED operation in the delta`));
84
+ }
85
+ }
86
+ for (const id of scope.removes) {
87
+ const removal = delta.entries.find((e) => e.element.id === id && e.element.op === "REMOVED");
88
+ if (removal === undefined) problems.push(error(path, scope.line, "scope-removes", `${id} is in Removes but the delta has no REMOVED operation for it`));
89
+ }
90
+ if (scope.delivers.length + scope.builds.length + scope.removes.length === 0) {
91
+ problems.push(warning(path, scope.line, "empty-scope", "the scope delivers and builds nothing"));
92
+ }
93
+ return problems;
94
+ }
95
+
96
+ /** An element's facts as this item will build it: from its delta when it has one, else from the living docs. */
97
+ function factsOfScoped(view , doc , delta , id ) {
98
+ const fromDelta = delta.entries.find((e) => e.element.id === id);
99
+ const living = view.living.elements.get(id);
100
+ return fromDelta !== undefined ? factsOf(doc, fromDelta.element) : living !== undefined ? factsOf(living.doc, living.element) : new Map ();
101
+ }
102
+
103
+ function modulePathsOf(view , doc , delta , id ) {
104
+ return (factsOfScoped(view, doc, delta, id).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => p !== "");
105
+ }
@@ -0,0 +1,21 @@
1
+ /** One way a document breaks the cruze-formats rules. */
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+ export function error(path , line , rule , message ) {
12
+ return line === undefined ? { path, rule, message, severity: "error" } : { path, line: line + 1, rule, message, severity: "error" };
13
+ }
14
+
15
+ export function warning(path , line , rule , message ) {
16
+ return { ...error(path, line, rule, message), severity: "warning" };
17
+ }
18
+
19
+ export function sortProblems(problems ) {
20
+ return [...problems].sort((a, b) => a.path.localeCompare(b.path) || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule));
21
+ }
@@ -0,0 +1,99 @@
1
+ import { factsOf } from "../elements.js";
2
+ import { matchesAny } from "../glob.js";
3
+ import { findIds } from "../ids.js";
4
+ import { parseMarkdown, } from "../markdown.js";
5
+ import { PATHS } from "../project/layout.js";
6
+ import { readRoadmapItems } from "../project/roadmap.js";
7
+
8
+ import { error, warning, } from "./problem.js";
9
+
10
+ /** Rules that span the project: leftover template guides, the roadmap and the config. */
11
+
12
+ const GUIDE = /(?<!`)<(?!!--|\/|https?:|br\b)[A-Za-z][^<>\n]*>(?!`)/;
13
+ const PLACEHOLDER = /\{\{[a-z_]+\}\}/;
14
+
15
+ /**
16
+ * Unfilled `<guides>` and `{{placeholders}}` outside code, in living docs and active work.
17
+ * Managed blocks are skipped: the CLI writes them, and they may quote text such as `<COMMAND>`.
18
+ */
19
+ export function templateLeftovers(view ) {
20
+ const paths = [...view.living.docs.keys(), PATHS.roadmap, PATHS.glossary, ...view.items.filter((i) => !i.archived).map((i) => i.path)];
21
+ return paths.flatMap((path) => {
22
+ const text = view.snapshot.get(path);
23
+ if (text === undefined) return [];
24
+ const doc = parseMarkdown(text);
25
+ return doc.lines.flatMap((line, i) => {
26
+ if (doc.inFence[i] || doc.inManaged[i]) return [];
27
+ const withoutCode = line.replace(/`[^`]*`/g, "");
28
+ if (GUIDE.test(withoutCode) || PLACEHOLDER.test(withoutCode)) {
29
+ return [error(path, i, "template-leftover", "replace the template guide or placeholder on this line")];
30
+ }
31
+ return [];
32
+ });
33
+ });
34
+ }
35
+
36
+ export function roadmapProblems(view ) {
37
+ const text = view.snapshot.get(PATHS.roadmap);
38
+ if (text === undefined) return [];
39
+ const doc = parseMarkdown(text);
40
+ const problems = [];
41
+ const items = new Map ();
42
+ for (const { slug: item, kind, blockedBy, line } of readRoadmapItems(text)) {
43
+ if (items.has(item)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} appears twice`));
44
+ if (!["feature", "change"].includes(kind)) problems.push(error(PATHS.roadmap, line, "roadmap-item", `item ${item} kind must be feature or change`));
45
+ items.set(item, { blockedBy, line });
46
+ }
47
+ for (const [item, { blockedBy, line }] of items) {
48
+ for (const blocker of blockedBy.filter((b) => !items.has(b))) problems.push(error(PATHS.roadmap, line, "roadmap-blocker", `${item} is blocked by unknown item ${blocker}`));
49
+ }
50
+ const cycle = findCycle(items);
51
+ if (cycle !== null) problems.push(error(PATHS.roadmap, undefined, "roadmap-cycle", `blocking cycle: ${cycle.join(" -> ")}`));
52
+ const coverage = doc.lines.join("\n").split(/^## Coverage$/m)[1] ?? "";
53
+ for (const goal of [...view.living.elements.keys()].filter((id) => id.startsWith("GOAL-"))) {
54
+ if (!findIds(coverage).includes(goal)) problems.push(warning(PATHS.roadmap, undefined, "goal-uncovered", `${goal} has no items in the coverage table`));
55
+ }
56
+ return problems;
57
+ }
58
+
59
+ function findCycle(items ) {
60
+ const state = new Map ();
61
+ const visit = (item , path ) => {
62
+ if (state.get(item) === "done") return null;
63
+ if (state.get(item) === "visiting") return [...path.slice(path.indexOf(item)), item];
64
+ state.set(item, "visiting");
65
+ for (const next of items.get(item)?.blockedBy ?? []) {
66
+ const found = visit(next, [...path, item]);
67
+ if (found !== null) return found;
68
+ }
69
+ state.set(item, "done");
70
+ return null;
71
+ };
72
+ for (const item of items.keys()) {
73
+ const found = visit(item, []);
74
+ if (found !== null) return found;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export function configProblems(view ) {
80
+ if (view.config === null) return [error(PATHS.config, undefined, "config-missing", "the project has no .cruze/config.yaml; run cruze init")];
81
+ const problems = view.config.problems.map((message) => error(PATHS.config, undefined, "config", message));
82
+ const config = view.config.config;
83
+ const architecture = view.living.docs.get(PATHS.architecture);
84
+ if (config === null || architecture === undefined || config.layers.length === 0) return problems;
85
+ for (const { element } of [...view.living.elements.values()].filter((e) => e.id.startsWith("MOD-"))) {
86
+ for (const path of modulePaths(architecture, element)) {
87
+ const probe = path.endsWith("/") ? `${path}probe.file` : path;
88
+ const layers = config.layers.filter((layer) => matchesAny(probe, layer.paths));
89
+ if (layers.length !== 1) {
90
+ problems.push(warning(PATHS.config, undefined, "module-layer", `${element.id} path ${path} falls in ${layers.length} layers; it should fall in exactly one`));
91
+ }
92
+ }
93
+ }
94
+ return problems;
95
+ }
96
+
97
+ function modulePaths(doc , element ) {
98
+ return (factsOf(doc, element).get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? ""));
99
+ }
@@ -0,0 +1,31 @@
1
+
2
+ import { livingProblems } from "./living_rules.js";
3
+ import { error, sortProblems, } from "./problem.js";
4
+ import { configProblems, roadmapProblems, templateLeftovers } from "./project_rules.js";
5
+ import { workItemProblems } from "./work_item_rules.js";
6
+
7
+ /** Every rule in the cruze-formats skill, applied to the whole project. */
8
+ export function validateProject(view ) {
9
+ const approvalProblems = [...view.approvalProblems].map(([path, message]) => error(path, undefined, "approvals-file", message));
10
+ const journalProblems = [...view.journalProblems].flatMap(([path, lines]) =>
11
+ lines.map((line) => error(path, line - 1, "journal-line", "this journal line is not a valid entry")),
12
+ );
13
+ return sortProblems([
14
+ ...configProblems(view),
15
+ ...livingProblems(view),
16
+ ...roadmapProblems(view),
17
+ ...workItemProblems(view),
18
+ ...templateLeftovers(view),
19
+ ...approvalProblems,
20
+ ...journalProblems,
21
+ ]);
22
+ }
23
+
24
+ /** Problems that concern one document. */
25
+ export function problemsFor(problems , path ) {
26
+ return problems.filter((problem) => problem.path === path);
27
+ }
28
+
29
+ export function hasErrors(problems ) {
30
+ return problems.some((problem) => problem.severity === "error");
31
+ }
@@ -0,0 +1,144 @@
1
+ import { isUnbuilt } from "../elements.js";
2
+ import { findIds, kindOf } from "../ids.js";
3
+ import { elementHash } from "../hashing.js";
4
+ import { findSection, } from "../markdown.js";
5
+ import { readDelta, } from "../project/deltas.js";
6
+ import { readChangesTable, readScope } from "../project/plan_parts.js";
7
+ import { readProgress } from "../project/progress.js";
8
+
9
+ import { changesOf, featureOf, } from "../project/work_items.js";
10
+ import { deltaProblems } from "./delta_rules.js";
11
+ import { malformedIdHeadings, lineOf } from "./living_rules.js";
12
+ import { changePlanProblems } from "./plan_rules.js";
13
+ import { error, warning, } from "./problem.js";
14
+
15
+ const REQUIRED_SECTIONS = {
16
+ feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
17
+ change: ["Scope", "Test plan", "Tasks"],
18
+ standalone: ["Intent", "Spec delta", "Architecture delta", "Scope", "Test plan", "Tasks"],
19
+ };
20
+
21
+ /** Rules for every active feature and change. Archived work is history and is not checked. */
22
+ export function workItemProblems(view ) {
23
+ return view.items.filter((item) => !item.archived).flatMap((item) => itemProblems(view, item));
24
+ }
25
+
26
+ function itemProblems(view , item ) {
27
+ const { path, doc } = item;
28
+ const problems = [...malformedIdHeadings(path, doc)];
29
+ const id = doc.frontmatter?.["id"];
30
+ if (id !== item.folderName) problems.push(error(path, 0, "frontmatter", `frontmatter id must be "${item.folderName}", the folder name`));
31
+ const title = doc.frontmatter?.["title"];
32
+ if (typeof title !== "string" || title.trim() === "") problems.push(error(path, 0, "frontmatter", "frontmatter needs a title"));
33
+ for (const section of REQUIRED_SECTIONS[item.kind]) {
34
+ if (findSection(doc, section) === null) problems.push(error(path, undefined, "missing-section", `missing "## ${section}"`));
35
+ }
36
+
37
+ const feature = item.kind === "change" ? featureOf(view.items, item) : undefined;
38
+ if (item.kind === "change" && feature === undefined) problems.push(error(path, undefined, "orphan-change", "the change's feature.md is missing"));
39
+ const deltaSource = item.kind === "change" ? feature : item;
40
+ const delta = deltaSource === undefined ? readDelta(doc) : readDelta(deltaSource.doc);
41
+ const available = new Set([...view.living.elements.keys(), ...delta.ids]);
42
+
43
+ const text = doc.lines.join("\n");
44
+ for (const cited of findIds(text).filter((cited) => !available.has(cited))) {
45
+ problems.push(error(path, lineOf(text, cited), "unknown-id", `${cited} is cited but defined neither in the living docs nor in this work's deltas`));
46
+ }
47
+ if (item.kind !== "change") problems.push(...deltaProblems(view, item));
48
+ if (item.kind === "feature") problems.push(...featureScopeProblems(view, item, delta));
49
+ // A landed change is history: its scope named what it built, which is now built.
50
+ const landed = item.kind !== "feature" && readProgress(doc).landed !== undefined;
51
+ if (item.kind !== "feature" && deltaSource !== undefined && !landed) {
52
+ problems.push(...changePlanProblems(view, path, doc, { delta, doc: deltaSource.doc }, available));
53
+ }
54
+ if (item.kind === "standalone") problems.push(...coverageProblems(view, path, delta, doc, [{ name: "this change", ids: scopeIds(item) }]));
55
+ return problems;
56
+ }
57
+
58
+ /** A feature's Changes table covers its deltas, and each change folder agrees with its row. */
59
+ function featureScopeProblems(view , feature , delta ) {
60
+ const rows = readChangesTable(feature.doc);
61
+ if (rows.length === 0) return [warning(feature.path, undefined, "not-designed", "the feature has no changes yet; architect designs and splits it")];
62
+ const problems = [];
63
+ const landedChanges = new Set(
64
+ [...readProgress(feature.doc).changes].filter(([, state]) => state === "landed").map(([change]) => change),
65
+ );
66
+ const names = rows.map((row) => row.change);
67
+ for (const row of rows) {
68
+ if (!/^\d\d-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.change)) problems.push(error(feature.path, row.line, "change-name", `change "${row.change}" must be named NN-slug`));
69
+ if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
70
+ for (const dependency of row.dependsOn) {
71
+ const at = names.indexOf(dependency);
72
+ if (at === -1 || at >= names.indexOf(row.change)) problems.push(error(feature.path, row.line, "change-order", `${row.change} depends on ${dependency}, which must be an earlier change`));
73
+ }
74
+ for (const id of landedChanges.has(row.change) ? [] : [...row.delivers, ...row.builds]) {
75
+ const living = view.living.elements.get(id);
76
+ const planned = living !== undefined && isUnbuilt(living.doc, living.element);
77
+ if (!delta.ids.has(id) && !planned) problems.push(error(feature.path, row.line, "scope-not-planned", `${id} must come from this feature's deltas or be planned in the living docs`));
78
+ }
79
+ }
80
+ problems.push(...coverageProblems(view, feature.path, delta, feature.doc, rows.map((row) => ({ name: row.change, ids: [...row.delivers, ...row.builds, ...row.removes], landed: landedChanges.has(row.change) }))));
81
+
82
+ for (const change of changesOf(view.items, feature)) {
83
+ const row = rows.find((r) => r.change === change.folderName);
84
+ if (row === undefined) {
85
+ problems.push(error(change.path, undefined, "change-not-listed", `${change.folderName} is not in the feature's Changes table`));
86
+ continue;
87
+ }
88
+ const expected = new Set([...row.delivers, ...row.builds, ...row.removes]);
89
+ const actual = new Set(scopeIds(change));
90
+ const differ = [...expected].filter((id) => !actual.has(id)).concat([...actual].filter((id) => !expected.has(id)));
91
+ if (differ.length > 0) problems.push(error(change.path, readScope(change.doc)?.line, "scope-mismatch", `Scope differs from the feature's Changes row: ${differ.join(", ")}`));
92
+ }
93
+ return problems;
94
+ }
95
+
96
+ /**
97
+ * Every delta ID lands exactly once. A requirement is covered by its own ID or one of its
98
+ * scenarios. A scenario must be covered when it is new or its text changed; an unchanged
99
+ * scenario kept by a MODIFIED requirement may be left out.
100
+ */
101
+ function coverageProblems(
102
+ view ,
103
+ path ,
104
+ delta ,
105
+ deltaDoc ,
106
+ groups ,
107
+ ) {
108
+ const coveredBy = new Map ();
109
+ const pendingBy = new Map ();
110
+ for (const group of groups) {
111
+ for (const id of group.ids) {
112
+ coveredBy.set(id, [...(coveredBy.get(id) ?? []), group.name]);
113
+ if (group.landed !== true) pendingBy.set(id, [...(pendingBy.get(id) ?? []), group.name]);
114
+ }
115
+ }
116
+ const problems = [];
117
+ // A change that already landed may be followed by one that rebuilds the same element after a rethink.
118
+ const once = (id , line , required ) => {
119
+ const pending = pendingBy.get(id) ?? [];
120
+ if (required && (coveredBy.get(id) ?? []).length === 0) problems.push(error(path, line, "uncovered", `${id} is not in any change's scope`));
121
+ if (pending.length > 1) problems.push(error(path, line, "covered-twice", `${id} is in the scope of ${pending.join(" and ")}; it must land exactly once`));
122
+ };
123
+ for (const { element, scenarios } of delta.entries) {
124
+ if (kindOf(element.id) !== "REQ") {
125
+ once(element.id, element.start, true);
126
+ continue;
127
+ }
128
+ once(element.id, element.start, false);
129
+ if (![element.id, ...scenarios.map((s) => s.id)].some((id) => coveredBy.has(id))) {
130
+ problems.push(error(path, element.start, "uncovered", `${element.id} is not delivered by any change`));
131
+ }
132
+ for (const scenario of scenarios) {
133
+ const living = view.living.elements.get(scenario.id);
134
+ const changed = living === undefined || elementHash(living.doc, living.element) !== elementHash(deltaDoc, scenario);
135
+ once(scenario.id, scenario.start, element.op !== "REMOVED" && changed);
136
+ }
137
+ }
138
+ return problems;
139
+ }
140
+
141
+ function scopeIds(item ) {
142
+ const scope = readScope(item.doc);
143
+ return scope === null ? [] : [...scope.delivers, ...scope.builds, ...scope.removes];
144
+ }
package/dist/main.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Composition root: the only place that constructs adapters.
3
+ import { basename } from "node:path";
4
+ import { runCli } from "./adapters/inbound/cli/run_cli.js";
5
+ import { GitRepository } from "./adapters/outbound/git_repository.js";
6
+ import { NodeProjectFiles } from "./adapters/outbound/node_project_files.js";
7
+ import { PackageBundle } from "./adapters/outbound/package_bundle.js";
8
+ import { ProcessAgentRunner } from "./adapters/outbound/process_agent_runner.js";
9
+ import { SystemClock } from "./adapters/outbound/system_clock.js";
10
+ import { DefaultAnswerPrompter, TerminalPrompter } from "./adapters/outbound/terminal_prompter.js";
11
+
12
+ const cwd = process.cwd();
13
+ const exitCode = await runCli(
14
+ process.argv.slice(2),
15
+ {
16
+ files: new NodeProjectFiles(cwd),
17
+ bundle: await PackageBundle.locate(),
18
+ clock: new SystemClock(),
19
+ repository: new GitRepository(cwd),
20
+ interactivePrompter: new TerminalPrompter(),
21
+ unattendedPrompter: new DefaultAnswerPrompter(),
22
+ runner: new ProcessAgentRunner(cwd),
23
+ interactive: process.stdin.isTTY === true,
24
+ directoryName: basename(cwd),
25
+ },
26
+ {
27
+ stdout: (text) => process.stdout.write(`${text}\n`),
28
+ stderr: (text) => process.stderr.write(`${text}\n`),
29
+ stdoutIsTerminal: process.stdout.isTTY === true,
30
+ },
31
+ );
32
+ process.exitCode = exitCode;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@printpractical/cruze",
3
+ "version": "0.0.1",
4
+ "description": "Spec-driven development for coding agents: one living model of the system, small deltas, convergent review.",
5
+ "license": "Apache-2.0",
6
+ "author": "Chris Buchter",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/PrintPractical/cruze.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "cruze": "bin/cruze.mjs"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "dist",
18
+ "scripts",
19
+ "src",
20
+ "skills",
21
+ "templates"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22.18"
25
+ },
26
+ "scripts": {
27
+ "build": "node --no-warnings scripts/build.mjs",
28
+ "typecheck": "tsc -p tsconfig.json",
29
+ "test": "node --test \"test/**/*.test.ts\"",
30
+ "check": "npm run typecheck && npm test && npm run build",
31
+ "prepack": "npm run build"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.20.4",
35
+ "typescript": "^7.0.2"
36
+ },
37
+ "dependencies": {
38
+ "yaml": "^2.9.1"
39
+ }
40
+ }
@@ -0,0 +1,37 @@
1
+ // Builds dist/ from src/ by erasing the TypeScript types with Node's own type stripper.
2
+ // It needs no dependencies, so it also runs on a user's machine: bin/cruze.mjs calls it on
3
+ // first run when dist/ is missing, as it is after a git install. The sources use erasable
4
+ // syntax only (tsconfig's erasableSyntaxOnly), so erasing the types is the whole build.
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { stripTypeScriptTypes } from "node:module";
7
+ import { dirname, join, relative } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const root = fileURLToPath(new URL("..", import.meta.url));
11
+ const src = join(root, "src");
12
+ const dist = join(root, "dist");
13
+ const RELATIVE_TS_IMPORT = /((?:from|import)\s*\(?\s*)(["'])(\.{1,2}\/[^"']+)\.ts\2/g;
14
+
15
+ const sources = (dir) =>
16
+ readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
17
+ entry.isDirectory() ? sources(join(dir, entry.name)) : entry.name.endsWith(".ts") ? [join(dir, entry.name)] : [],
18
+ );
19
+
20
+ // Build beside dist/ and swap it in, so a concurrent first run never sees half a build.
21
+ const staging = `${dist}.${process.pid}`;
22
+ rmSync(staging, { recursive: true, force: true });
23
+ for (const file of sources(src)) {
24
+ const code = stripTypeScriptTypes(readFileSync(file, "utf8"), { mode: "strip" }).replace(RELATIVE_TS_IMPORT, "$1$2$3.js$2");
25
+ const target = join(staging, relative(src, file)).replace(/\.ts$/, ".js");
26
+ mkdirSync(dirname(target), { recursive: true });
27
+ writeFileSync(target, code);
28
+ }
29
+ chmodSync(join(staging, "main.js"), 0o755);
30
+ rmSync(dist, { recursive: true, force: true });
31
+ try {
32
+ renameSync(staging, dist);
33
+ } catch (error) {
34
+ // Another process finished its build first; use that one.
35
+ rmSync(staging, { recursive: true, force: true });
36
+ if (!existsSync(join(dist, "main.js"))) throw error;
37
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: cruze-about
3
+ description: Explains what Cruze is and what it can do in this repository. Use when the user asks about Cruze, its commands, or how to start working with it.
4
+ ---
5
+
6
+ # About Cruze
7
+
8
+ Cruze is a spec-driven development framework. It keeps one living model of the system (vision, glossary, architecture and behaviour specs in `docs/`) and delivers work as small changes against that model, each merged back into it when it lands.
9
+
10
+ This installation is an early preview. It has:
11
+
12
+ - The `cruze` CLI: `cruze init` sets up a repository, `cruze install` updates the skills after an upgrade, and `cruze help` lists the commands that validate, approve, check, trace and land work.
13
+ - `cruze-formats`, the format of every Cruze document.
14
+ - Knowledge skills holding the standards to design and code to: `cruze-hexagonal-design`, `cruze-behavioural-testing`, `cruze-grilling`, `cruze-domain-language`, `cruze-dependency-approval` and `cruze-research`.
15
+ - The workflow: `explore` (optional), `envision` for the what, `architect` for the how, `roadmap` for the order of work, then `plan`, `build`, `verify` and `land` for each change. `triage` handles bugs, `rethink` steps back from anywhere, and `next` says what to do now.
16
+
17
+ When the user asks what Cruze can do:
18
+
19
+ 1. Read `.cruze/config.yaml` and report the project name and Cruze version.
20
+ 2. Run `cruze --version`, and say whether it differs from the version in `.cruze/config.yaml`. When `cruze` isn't found, tell the user to install it globally as https://github.com/PrintPractical/cruze#quick-start describes; every Cruze skill needs it.
21
+ 3. Run `cruze next` and explain the step it names, in the context of the whole workflow.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: cruze-architect
3
+ description: Decides how to build and how big, at project scope (domain model, contexts, ports, adapters, module map, layer rules, ADRs and the roadmap with its walking skeleton), feature scope (the architecture delta and its split into changes) or tweak scope (a standalone change), ending with a design review and a walkthrough for your approval.
4
+ ---
5
+
6
+ # Architect
7
+
8
+ Architect decides the how and the how big. It designs the domain, the ports and adapters, and where every element lives in the source tree, before any code exists. It ends with the one heavy human gate, the design walkthrough, where the user sees the entities, their interactions, the ports and the files, and approves the design.
9
+
10
+ ## Steps
11
+
12
+ 1. **Pick the scope.**
13
+ - **Project scope:** `docs/architecture.md` doesn't exist yet. Reworking an approved architecture goes through `rethink` instead.
14
+ - **Feature scope:** the user names a feature, or a feature folder exists without changes.
15
+ - **Tweak scope:** the user asks for a small change to existing behaviour. When the design shows it needs any architecture change, it becomes a feature: say so and continue at feature scope.
16
+ - Done when you can say which, and why.
17
+ 2. **Check what it builds on.** Run `cruze status`. `docs/vision.md` must be approved and current. At feature and tweak scope, `docs/architecture.md` must be approved and current as well. When a document isn't, stop and name the step that fixes it: `envision` for the vision, and `rethink` for an architecture that changed since its approval.
18
+ 3. **Read the living model.** Read `docs/vision.md`, `docs/glossary.md`, `docs/architecture.md`, the ADRs in `docs/adr/`, `docs/roadmap.md`, `.cruze/config.yaml`, and the specs and code the work touches. Read any note in `.cruze/notes/` the user points to.
19
+ 4. **Load the standards.** Read these, and follow them for the rest of this step:
20
+ - `.agents/skills/cruze-hexagonal-design/SKILL.md`
21
+ - `.agents/skills/cruze-grilling/SKILL.md`
22
+ - `.agents/skills/cruze-domain-language/SKILL.md`
23
+ - `.agents/skills/cruze-research/SKILL.md`
24
+ - `.agents/skills/cruze-dependency-approval/SKILL.md`
25
+ - `.agents/skills/cruze-formats/reference/architecture.md`
26
+ 5. **Design.** Follow the scope's procedure: [project.md](project.md), [feature.md](feature.md) or [tweak.md](tweak.md). Tweak scope ends there and hands off to `plan`.
27
+ 6. **Review the design.** Run the design review in `.agents/skills/cruze-roles/SKILL.md`, with the design reviewer role. Done when every finding has a journaled disposition and no blocker is open without the user's decision.
28
+ 7. **Walk the user through it.** Follow [walkthrough.md](walkthrough.md). Apply what the user changes, run `cruze validate` again, and repeat the parts of the walkthrough those changes affect.
29
+ 8. **Record the approval.** Only after the user says they approve, run the scope's `cruze approve` command and quote its result, then commit. At project scope, the roadmap follows as its own approval, described in [project.md](project.md).
30
+
31
+ ## Rules
32
+
33
+ - **Design for the roadmap, build for the phase.** Read the whole feature map and roadmap. When a later feature will need a seam, name it now in a `Future` fact, such as "transport will gain MQTT in phase 3, so inbound adapters stay behind `CommandSource`". Build only what the current work needs.
34
+ - **Adopt before you build.** Before designing any component that isn't domain logic, run the researcher role from `.agents/skills/cruze-roles/SKILL.md` on it. Record every decision in `## Adopt or build`, or at project scope in the adapter's `Adopts` fact. Every new dependency goes to the user through the dependency-approval skill.
35
+ - **Every element with code has a home.** Each `ENT`, `UC`, `PORT`, `ADP` and `FLOW` names its `Module` and `File`. Each file has one responsibility and fits the budget in `.cruze/config.yaml`.
36
+ - **Ask, challenge and defer** as the grilling skill describes. Contexts, ports, public contracts, dependency direction and dependencies are always the user's decisions.
37
+ - **Stepping back is normal.** When the design shows that the vision or an earlier decision is wrong, stop and say so. The vision changes through `envision`, and an approved architecture through `rethink`.
38
+
39
+ ## Next step
40
+
41
+ - Project scope: `plan` for the walking-skeleton change.
42
+ - Feature scope: `plan` for the feature's first change.
43
+ - Tweak scope: `plan` for the standalone change.
@@ -0,0 +1,29 @@
1
+ # Architect at feature scope
2
+
3
+ The result is a `feature.md` with a complete architecture delta and an ordered list of changes, approved at the walkthrough. Formats: `.agents/skills/cruze-formats/reference/work-items.md` and `.agents/skills/cruze-formats/reference/architecture.md`.
4
+
5
+ ## Settle what it does
6
+
7
+ 1. **Find the feature folder.** When `envision "<feature>"` created one, read its intent, settled decisions and spec delta.
8
+ 2. **Or settle it now.** When there is no folder, follow `.agents/skills/cruze-envision/feature.md` from its step 2 up to, but not including, its step 8. That puts the feature on the roadmap, checks its blockers, creates the folder and settles the intent and spec delta.
9
+
10
+ ## Design the delta
11
+
12
+ 3. **Trace each scenario through the model.** For every scenario in the spec delta, name the inbound adapter it enters through, the use case that runs it, the domain elements that decide it, and the ports it crosses. Done when every scenario has a path, and each missing piece is a named gap.
13
+ 4. **Place each gap,** following "Placing an owner" in the hexagonal-design skill. Prefer extending an existing element when it is the right owner. Each change to the living model becomes an operation under `## Architecture delta`:
14
+ - `ADDED` for a new element, with its full body.
15
+ - `MODIFIED` for an existing element, with its full new body. Copy the current body from `docs/architecture.md` and edit it, so nothing is lost by accident.
16
+ - `REMOVED` with a `Reason`, for an element the feature retires.
17
+ 5. **Link behaviour to design.** Each use case that runs a requirement lists it under `Serves`. Add or modify a `FLOW` for each key scenario, with `Serves` naming its scenarios and the failure paths they need.
18
+ 6. **Research and decide.** For each new component that isn't domain logic, run the researcher role and record a row in `## Adopt or build`. Take every new dependency to the user.
19
+ 7. **Keep later seams open.** When the feature map shows a later feature that will extend this area, record the seam in a `Future` fact, and don't build it.
20
+ 8. Done when `cruze validate` reports no errors in the delta itself, and every cited ID exists in the living docs or this delta.
21
+
22
+ ## Split it into changes
23
+
24
+ 9. **Slice vertically.** Each change delivers one or more scenarios end to end, through every layer they need, and can land on its own. The first change is the thinnest slice that proves the design, a tracer bullet. Later changes widen it.
25
+ 10. **Fill the Changes table.** Give each row a number and a slug, such as `01-local-serial`. List the scenario and requirement IDs it `Delivers`, the architecture IDs it `Builds`, any IDs it `Removes`, and the changes it `Depends on`.
26
+ 11. **Cover everything exactly once.** Every delta ID appears in exactly one change. A module the delta adds, or one still `planned` in the living docs, is built by the first change that writes a file in it. A module already `built` isn't listed. Done when `cruze validate` reports no errors for the feature, and no `not-designed` warning.
27
+ 12. **Size each change.** A change that would need more than about 12 tasks, or touches more than one context's domain, splits in two.
28
+
29
+ Then continue with the review and walkthrough steps of the skill. When the user approves, run `cruze approve <feature>` and quote its result. The command takes the feature's full ID, such as `2026-09-25-open-console`, or its slug when no other active item shares it.