@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,40 @@
1
+ /** Where Cruze documents live in a project, and the snapshot of them the domain works on. */
2
+
3
+ /** Project files by path, relative to the project root, with forward slashes. */
4
+
5
+
6
+ export const PATHS = {
7
+ vision: "docs/vision.md",
8
+ glossary: "docs/glossary.md",
9
+ architecture: "docs/architecture.md",
10
+ roadmap: "docs/roadmap.md",
11
+ adrDir: "docs/adr",
12
+ specsDir: "docs/specs",
13
+ config: ".cruze/config.yaml",
14
+ featuresDir: ".cruze/features",
15
+ changesDir: ".cruze/changes",
16
+ archiveDir: ".cruze/archive",
17
+ notesDir: ".cruze/notes",
18
+ projectApprovals: ".cruze/approvals.json",
19
+ projectJournal: ".cruze/journal.jsonl",
20
+ } ;
21
+
22
+ /** Directories whose files make up the snapshot. */
23
+ export const SNAPSHOT_ROOTS = ["docs", ".cruze"] ;
24
+
25
+ export function specPath(capability ) {
26
+ return `${PATHS.specsDir}/${capability}.md`;
27
+ }
28
+
29
+ export function filesUnder(snapshot , dir ) {
30
+ const prefix = `${dir}/`;
31
+ return [...snapshot.keys()].filter((path) => path.startsWith(prefix)).sort();
32
+ }
33
+
34
+ /** Paths directly matching `<dir>/<name><suffix>`, one level deep. */
35
+ export function childFiles(snapshot , dir , suffix ) {
36
+ return filesUnder(snapshot, dir).filter((path) => {
37
+ const rest = path.slice(dir.length + 1);
38
+ return !rest.includes("/") && rest.endsWith(suffix);
39
+ });
40
+ }
@@ -0,0 +1,54 @@
1
+ import { headingElements, rowElements, } from "../elements.js";
2
+ import { parseMarkdown, } from "../markdown.js";
3
+ import { PATHS, childFiles, } from "./layout.js";
4
+
5
+ /** An element of the living docs, with the document it lives in. */
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+ /** The living docs: vision, architecture, specs and ADRs, indexed by element ID. */
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+ export function livingDocPaths(snapshot ) {
22
+ const fixed = [PATHS.vision, PATHS.architecture].filter((path) => snapshot.has(path));
23
+ return [...fixed, ...childFiles(snapshot, PATHS.specsDir, ".md"), ...childFiles(snapshot, PATHS.adrDir, ".md")];
24
+ }
25
+
26
+ export function buildLivingModel(snapshot ) {
27
+ const docs = new Map ();
28
+ const elements = new Map ();
29
+ const places = new Map ();
30
+
31
+ for (const path of livingDocPaths(snapshot)) {
32
+ const doc = parseMarkdown(snapshot.get(path) ?? "");
33
+ docs.set(path, doc);
34
+ for (const element of elementsOf(path, doc)) {
35
+ places.set(element.id, [...(places.get(element.id) ?? []), `${path}:${element.start + 1}`]);
36
+ if (!elements.has(element.id)) elements.set(element.id, { id: element.id, path, doc, element });
37
+ }
38
+ }
39
+ const duplicates = [...places].filter(([, at]) => at.length > 1).map(([id, at]) => ({ id, places: at }));
40
+ return { docs, elements, duplicates };
41
+ }
42
+
43
+ /** The ID an ADR file defines: `ADR-<file stem>`. */
44
+ export function adrId(path ) {
45
+ return `ADR-${path.slice(path.lastIndexOf("/") + 1, -".md".length)}`;
46
+ }
47
+
48
+ function elementsOf(path , doc ) {
49
+ if (path.startsWith(`${PATHS.adrDir}/`)) {
50
+ return [{ id: adrId(path), kind: "ADR", title: doc.headings[0]?.text ?? "", form: "heading", level: 1, start: 0, end: doc.lines.length }];
51
+ }
52
+ if (path === PATHS.vision) return rowElements(doc).filter((element) => element.kind === "GOAL");
53
+ return headingElements(doc);
54
+ }
@@ -0,0 +1,48 @@
1
+ import { CruzeError } from "../cruze_error.js";
2
+ import { readChangesTable } from "./plan_parts.js";
3
+
4
+
5
+
6
+ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
+
8
+ export function requireSlug(slug ) {
9
+ if (!SLUG.test(slug)) throw new CruzeError("invalid-slug", `"${slug}" must be lowercase words joined by single hyphens`);
10
+ }
11
+
12
+ /**
13
+ * A new feature or standalone change ID: the date plus the slug, with `-2`, `-3`… added
14
+ * on a clash with any live or archived work. IDs are never reused.
15
+ */
16
+ export function allocateId(view , date , slug ) {
17
+ requireSlug(slug);
18
+ const taken = new Set(view.items.filter((item) => item.kind !== "change").map((item) => item.folderName));
19
+ const base = `${date}-${slug}`;
20
+ if (!taken.has(base)) return base;
21
+ for (let n = 2; ; n++) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
22
+ }
23
+
24
+ /**
25
+ * The change folder name for a feature's change: the row of the feature's Changes table it
26
+ * names, by full name (`01-local-serial`) or slug (`local-serial`).
27
+ */
28
+ export function changeRow(view , feature , name ) {
29
+ const rows = readChangesTable(feature.doc);
30
+ const row = rows.find((r) => r.change === name) ?? rows.find((r) => r.change.replace(/^\d\d-/, "") === name);
31
+ if (row === undefined) {
32
+ throw new CruzeError("change-not-listed", `${feature.ref} has no change "${name}" in its Changes table; add the row first`);
33
+ }
34
+ if (view.items.some((item) => item.featureRef === feature.ref && item.folderName === row.change)) {
35
+ throw new CruzeError("exists", `${feature.ref}/${row.change} already exists`);
36
+ }
37
+ return row;
38
+ }
39
+
40
+ /** Fills the template's frontmatter, title and scope lines for a new document. */
41
+ export function fillTemplate(template , values ) {
42
+ let text = template;
43
+ if (values.roadmap !== undefined) text = text.replace(/^roadmap: .*$/m, `roadmap: ${values.roadmap}`);
44
+ else text = text.replace(/^roadmap: <.*\n/m, "");
45
+ if (values.delivers !== undefined) text = text.replace(/^- Delivers: .*$/m, `- Delivers: ${values.delivers.join(", ")}`);
46
+ if (values.builds !== undefined) text = text.replace(/^- Builds: .*$/m, `- Builds: ${values.builds.join(", ")}`);
47
+ return text;
48
+ }
@@ -0,0 +1,111 @@
1
+ import { findIds } from "../ids.js";
2
+ import { findSection, } from "../markdown.js";
3
+ import { tablesIn } from "../markdown_tables.js";
4
+
5
+ /** The parts of feature.md and change.md that describe what gets built and how it is proved. */
6
+
7
+
8
+
9
+
10
+
11
+
12
+
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
+ const TASK = /^- (T\d+): `([^`]+)`[^`]*? in ((?:`[^`]+`(?:, )?)+), proves (.+)$/;
40
+
41
+ export function readScope(doc ) {
42
+ const section = findSection(doc, "Scope");
43
+ if (section === null) return null;
44
+ const scope = { delivers: [], builds: [], removes: [], line: section.heading.line };
45
+ for (let i = section.start; i < section.end; i++) {
46
+ const match = /^- (Delivers|Builds|Removes): (.*)$/.exec(doc.lines[i] ?? "");
47
+ if (match?.[1] === undefined || match[2] === undefined) continue;
48
+ const key = match[1].toLowerCase() ;
49
+ scope[key] = findIds(match[2]);
50
+ }
51
+ return scope;
52
+ }
53
+
54
+ /** Task lines, and the lines under `## Tasks` that don't parse as tasks. */
55
+ export function readTasks(doc ) {
56
+ const section = findSection(doc, "Tasks");
57
+ const tasks = [];
58
+ const malformed = [];
59
+ if (section === null) return { tasks, malformed };
60
+ for (let i = section.start; i < section.end; i++) {
61
+ const line = doc.lines[i] ?? "";
62
+ if (!line.startsWith("- ")) continue;
63
+ const match = TASK.exec(line);
64
+ if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined || match[4] === undefined) {
65
+ malformed.push(i);
66
+ continue;
67
+ }
68
+ const paths = [...match[3].matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "");
69
+ tasks.push({ number: match[1], owner: match[2], paths, proves: findIds(match[4]), line: i });
70
+ }
71
+ return { tasks, malformed };
72
+ }
73
+
74
+ /**
75
+ * Whether a change has been planned. A standalone change is designed first, with an
76
+ * empty test plan and no tasks; plan fills both.
77
+ */
78
+ export function isPlanned(doc ) {
79
+ const { tasks, malformed } = readTasks(doc);
80
+ return readTestPlan(doc).length > 0 || tasks.length > 0 || malformed.length > 0;
81
+ }
82
+
83
+ export function readTestPlan(doc ) {
84
+ const section = findSection(doc, "Test plan");
85
+ if (section === null) return [];
86
+ const table = tablesIn(doc.lines, doc.inFence, section.start, section.end)[0];
87
+ return (table?.rows ?? []).map(({ cells, line }) => ({
88
+ subject: cells[0] ?? "",
89
+ seam: cells[1] ?? "",
90
+ file: (cells[2] ?? "").replace(/`/g, ""),
91
+ kind: cells[3] ?? "",
92
+ line,
93
+ }));
94
+ }
95
+
96
+ export function readChangesTable(doc ) {
97
+ const section = findSection(doc, "Changes");
98
+ if (section === null) return [];
99
+ const table = tablesIn(doc.lines, doc.inFence, section.start, section.end)[0];
100
+ if (table === undefined) return [];
101
+ const column = (name ) => table.header.findIndex((cell) => cell.toLowerCase() === name);
102
+ const cell = (cells , name ) => cells[column(name)] ?? "";
103
+ return table.rows.map(({ cells, line }) => ({
104
+ change: cells[0] ?? "",
105
+ delivers: findIds(cell(cells, "delivers")),
106
+ builds: findIds(cell(cells, "builds")),
107
+ removes: findIds(cell(cells, "removes")),
108
+ dependsOn: cell(cells, "depends on").split(",").map((s) => s.trim()).filter((s) => s !== ""),
109
+ line,
110
+ }));
111
+ }
@@ -0,0 +1,43 @@
1
+ import { MANAGED_CLOSE, MANAGED_OPEN, } from "../markdown.js";
2
+
3
+ /** The managed `## Progress` block of a feature or change. */
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+ export function readProgress(doc ) {
15
+ const progress = { tasks: new Map(), deviations: [], changes: new Map() };
16
+ const range = progressRange(doc);
17
+ if (range === null) return progress;
18
+ for (let i = range.start; i < range.end; i++) {
19
+ const line = doc.lines[i] ?? "";
20
+ let match = /^- Branch: (.+)$/.exec(line);
21
+ if (match?.[1] !== undefined) progress.branch = match[1].trim();
22
+ match = /^- \[([ x])\] (T\d+)(?: \(([0-9a-f]+)\))?/.exec(line);
23
+ if (match?.[2] !== undefined) progress.tasks.set(match[2], match[1] === "x" ? (match[3] ?? "") : null);
24
+ match = /^- Deviation (.+)$/.exec(line);
25
+ if (match?.[1] !== undefined) progress.deviations.push(match[1]);
26
+ match = /^- Landed: (.+)$/.exec(line);
27
+ if (match?.[1] !== undefined) progress.landed = match[1];
28
+ match = /^- (\d\d-[a-z0-9-]+): (.+)$/.exec(line);
29
+ if (match?.[1] !== undefined && match[2] !== undefined) progress.changes.set(match[1], match[2]);
30
+ }
31
+ return progress;
32
+ }
33
+
34
+ /** Lines [start, end) inside the managed block that holds `## Progress`, or null. */
35
+ export function progressRange(doc ) {
36
+ const heading = doc.headings.find((h) => h.level === 2 && h.text === "Progress");
37
+ if (heading === undefined || !doc.inManaged[heading.line]) return null;
38
+ let open = heading.line;
39
+ while (open > 0 && doc.lines[open]?.trim() !== MANAGED_OPEN) open--;
40
+ let end = heading.line;
41
+ while (end < doc.lines.length && doc.lines[end]?.trim() !== MANAGED_CLOSE) end++;
42
+ return { open, start: heading.line + 1, end };
43
+ }
@@ -0,0 +1,72 @@
1
+ import { parseApprovals, } from "../approvals/records.js";
2
+ import { parseConfig, } from "../config.js";
3
+ import { journalPath, parseJournal, } from "../journal.js";
4
+ import { PATHS, } from "./layout.js";
5
+ import { buildLivingModel, } from "./living_model.js";
6
+ import { readDelta } from "./deltas.js";
7
+ import { listWorkItems, } from "./work_items.js";
8
+
9
+ /** Everything the rules need, loaded once from a snapshot. */
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+ export function buildProjectView(snapshot ) {
28
+ const items = listWorkItems(snapshot);
29
+ const approvals = new Map ();
30
+ const approvalProblems = new Map ();
31
+ for (const folder of [".cruze", ...items.map((item) => item.folder)]) {
32
+ const path = approvalsPath(folder);
33
+ const { file, problem } = parseApprovals(snapshot.get(path));
34
+ approvals.set(folder, file);
35
+ if (problem !== undefined) approvalProblems.set(path, problem);
36
+ }
37
+ const retired = new Set(
38
+ items
39
+ .filter((item) => item.archived)
40
+ .flatMap((item) => readDelta(item.doc).entries.filter((e) => e.element.op === "REMOVED").map((e) => e.element.id)),
41
+ );
42
+ const journal = [];
43
+ const journalProblems = new Map ();
44
+ for (const folder of [".cruze", ...items.map((item) => item.folder)]) {
45
+ const path = journalPath(folder);
46
+ const { entries, badLines } = parseJournal(snapshot.get(path));
47
+ journal.push(...entries);
48
+ if (badLines.length > 0) journalProblems.set(path, badLines);
49
+ }
50
+ journal.sort((a, b) => a.at.localeCompare(b.at));
51
+ const configText = snapshot.get(PATHS.config);
52
+ return {
53
+ snapshot,
54
+ living: buildLivingModel(snapshot),
55
+ items,
56
+ config: configText === undefined ? null : parseConfig(configText),
57
+ approvals,
58
+ approvalProblems,
59
+ retired,
60
+ journal,
61
+ journalProblems,
62
+ };
63
+ }
64
+
65
+ export function approvalsPath(folder ) {
66
+ return `${folder}/approvals.json`;
67
+ }
68
+
69
+ /** The folder that holds an artifact's approval: the work item's folder, or `.cruze` for project documents. */
70
+ export function approvalFolder(view , artifactPath ) {
71
+ return view.items.find((item) => item.path === artifactPath)?.folder ?? ".cruze";
72
+ }
@@ -0,0 +1,43 @@
1
+ import { readRows } from "../edits/managed_tables.js";
2
+ import { parseMarkdown } from "../markdown.js";
3
+ import { tablesIn } from "../markdown_tables.js";
4
+
5
+ /** The items of `docs/roadmap.md`, as the cruze-formats skill defines them. */
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ /** Every item in the roadmap's phases, in the order they appear. */
23
+ export function readRoadmapItems(text ) {
24
+ const doc = parseMarkdown(text);
25
+ const items = [];
26
+ for (const phase of doc.headings.filter((h) => h.level === 3 && /^Phase \d+: /.test(h.text))) {
27
+ const number = Number(/^Phase (\d+)/.exec(phase.text)?.[1] ?? 0);
28
+ const end = doc.headings.find((h) => h.line > phase.line && h.level <= 3)?.line ?? doc.lines.length;
29
+ for (const table of tablesIn(doc.lines, doc.inFence, phase.line + 1, end)) {
30
+ for (const { cells, line } of table.rows) {
31
+ const [slug = "", kind = "", goals = "", blocked = ""] = cells;
32
+ const blockedBy = blocked.split(",").map((s) => s.trim()).filter((s) => s !== "");
33
+ items.push({ slug, kind, goals, blockedBy, phase: number, line });
34
+ }
35
+ }
36
+ }
37
+ return items;
38
+ }
39
+
40
+ /** The managed `## Status` rows: each started item's folder ID and state. */
41
+ export function readRoadmapStatus(text ) {
42
+ return readRows(text, "Status").map(([slug = "", id = "", state = ""]) => ({ slug, id, state }));
43
+ }
@@ -0,0 +1,68 @@
1
+ import { parseMarkdown, } from "../markdown.js";
2
+ import { PATHS, filesUnder, } from "./layout.js";
3
+
4
+
5
+
6
+ /** A feature, a change inside a feature, or a standalone change. */
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+ const FEATURE = /^\.cruze\/(features|archive)\/([^/]+)\/feature\.md$/;
22
+ const FEATURE_CHANGE = /^\.cruze\/(features|archive)\/([^/]+)\/changes\/([^/]+)\/change\.md$/;
23
+ const STANDALONE = /^\.cruze\/(changes|archive)\/([^/]+)\/change\.md$/;
24
+
25
+ export function listWorkItems(snapshot ) {
26
+ const paths = [...filesUnder(snapshot, PATHS.featuresDir), ...filesUnder(snapshot, PATHS.changesDir), ...filesUnder(snapshot, PATHS.archiveDir)];
27
+ return paths.flatMap((path) => {
28
+ const item = classify(path);
29
+ return item === null ? [] : [{ ...item, path, doc: parseMarkdown(snapshot.get(path) ?? "") }];
30
+ });
31
+ }
32
+
33
+ export function findWorkItem(items , ref ) {
34
+ return items.find((item) => item.ref === ref || item.path === ref || item.folder === ref);
35
+ }
36
+
37
+ /** The feature a change belongs to. */
38
+ export function featureOf(items , change ) {
39
+ return change.featureRef === undefined ? undefined : items.find((item) => item.kind === "feature" && item.ref === change.featureRef);
40
+ }
41
+
42
+ export function changesOf(items , feature ) {
43
+ return items.filter((item) => item.featureRef === feature.ref).sort((a, b) => a.ref.localeCompare(b.ref));
44
+ }
45
+
46
+ function classify(path ) {
47
+ const folder = path.slice(0, path.lastIndexOf("/"));
48
+ let match = FEATURE.exec(path);
49
+ if (match?.[2] !== undefined) {
50
+ return { kind: "feature", ref: match[2], folderName: match[2], folder, archived: match[1] === "archive" };
51
+ }
52
+ match = FEATURE_CHANGE.exec(path);
53
+ if (match?.[2] !== undefined && match[3] !== undefined) {
54
+ return {
55
+ kind: "change",
56
+ ref: `${match[2]}/${match[3]}`,
57
+ folderName: match[3],
58
+ folder,
59
+ archived: match[1] === "archive",
60
+ featureRef: match[2],
61
+ };
62
+ }
63
+ match = STANDALONE.exec(path);
64
+ if (match?.[2] !== undefined) {
65
+ return { kind: "standalone", ref: match[2], folderName: match[2], folder, archived: match[1] === "archive" };
66
+ }
67
+ return null;
68
+ }
@@ -0,0 +1,20 @@
1
+ import { CruzeError } from "./cruze_error.js";
2
+
3
+
4
+
5
+ const MAX_LENGTH = 100;
6
+
7
+ /** A project name is a single line of human-readable text, used as the README title. */
8
+ export function parseProjectName(raw ) {
9
+ const name = raw.trim();
10
+ if (name.length === 0) {
11
+ throw new CruzeError("invalid-project-name", "The project name is empty.");
12
+ }
13
+ if (/[\r\n]/.test(name)) {
14
+ throw new CruzeError("invalid-project-name", "The project name must be a single line.");
15
+ }
16
+ if (name.length > MAX_LENGTH) {
17
+ throw new CruzeError("invalid-project-name", `The project name is longer than ${MAX_LENGTH} characters.`);
18
+ }
19
+ return name ;
20
+ }
@@ -0,0 +1,29 @@
1
+ /** The files `cruze init` gives a repository, and how their templates are filled. */
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+ export const INIT_SCAFFOLD = [
11
+ { path: "README.md", template: "init/README.md" },
12
+ { path: "CHANGELOG.md", template: "init/CHANGELOG.md" },
13
+ { path: "AGENTS.md", template: "init/AGENTS.md" },
14
+ { path: "CLAUDE.md", template: "init/CLAUDE.md" },
15
+ { path: ".cruze/config.yaml", template: "init/config.yaml" },
16
+ { path: ".github/workflows/ci.yml", template: "init/ci.yml" },
17
+ { path: ".gitattributes", template: "init/gitattributes" },
18
+ ];
19
+
20
+ /** Replaces every `{{key}}` with its value; an unknown key is a template bug. */
21
+ export function renderTemplate(template , values ) {
22
+ return template.replace(/\{\{([a-z_]+)\}\}/g, (placeholder, key ) => {
23
+ const value = values[key];
24
+ if (value === undefined) {
25
+ throw new Error(`Template placeholder ${placeholder} has no value`);
26
+ }
27
+ return value;
28
+ });
29
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Rules for bundled skills in the Agent Skills format: a folder holding a SKILL.md
3
+ * whose frontmatter carries `name` and `description`, installed as `cruze-<folder>`.
4
+ */
5
+
6
+ export const SKILL_FILE = "SKILL.md";
7
+ export const SKILL_PREFIX = "cruze-";
8
+ export const MAX_BODY_LINES = 150;
9
+
10
+ const MAX_NAME_LENGTH = 64;
11
+ const MAX_DESCRIPTION_LENGTH = 1024;
12
+ const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
13
+
14
+
15
+
16
+
17
+
18
+
19
+ /** The directory name a bundled skill folder gets once installed into a project. */
20
+ export function installedSkillName(folder ) {
21
+ return `${SKILL_PREFIX}${folder}`;
22
+ }
23
+
24
+ export function isInstalledSkillName(name ) {
25
+ return name.startsWith(SKILL_PREFIX);
26
+ }
27
+
28
+ /**
29
+ * Splits a SKILL.md into frontmatter and body. Frontmatter is read as flat
30
+ * `key: value` lines, which is all Cruze's own skills use; returns null when
31
+ * the file has no frontmatter block.
32
+ */
33
+ export function parseSkillDocument(text ) {
34
+ const lines = text.split(/\r?\n/);
35
+ if (lines[0] !== "---") return null;
36
+ const end = lines.indexOf("---", 1);
37
+ if (end === -1) return null;
38
+
39
+ const frontmatter = {};
40
+ for (const line of lines.slice(1, end)) {
41
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
42
+ if (match?.[1] !== undefined && match[2] !== undefined) {
43
+ frontmatter[match[1]] = unquote(match[2].trim());
44
+ }
45
+ }
46
+ return { frontmatter, body: lines.slice(end + 1).join("\n") };
47
+ }
48
+
49
+ /** Every rule a bundled skill breaks; empty when the skill is valid. */
50
+ export function skillProblems(folder , skillText ) {
51
+ const doc = parseSkillDocument(skillText);
52
+ if (doc === null) return [`${folder}: ${SKILL_FILE} has no frontmatter block`];
53
+
54
+ const problems = [];
55
+ const expectedName = installedSkillName(folder);
56
+ const name = doc.frontmatter["name"] ?? "";
57
+ const description = doc.frontmatter["description"] ?? "";
58
+
59
+ if (name !== expectedName) {
60
+ problems.push(`${folder}: name is "${name}", expected "${expectedName}"`);
61
+ }
62
+ if (name.length > MAX_NAME_LENGTH || !NAME_PATTERN.test(name)) {
63
+ problems.push(`${folder}: name must be lowercase letters, digits and single hyphens, at most ${MAX_NAME_LENGTH} characters`);
64
+ }
65
+ if (description.length === 0) {
66
+ problems.push(`${folder}: description is missing`);
67
+ }
68
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
69
+ problems.push(`${folder}: description is longer than ${MAX_DESCRIPTION_LENGTH} characters`);
70
+ }
71
+ const bodyLines = doc.body.trimEnd().split("\n").length;
72
+ if (bodyLines > MAX_BODY_LINES) {
73
+ problems.push(`${folder}: body is ${bodyLines} lines, the limit is ${MAX_BODY_LINES}; move reference material to sibling files`);
74
+ }
75
+ return problems;
76
+ }
77
+
78
+ function unquote(value ) {
79
+ const quoted = /^"(.*)"$/.exec(value) ?? /^'(.*)'$/.exec(value);
80
+ return quoted?.[1] ?? value;
81
+ }
82
+
83
+ /**
84
+ * Agent-specific tool names a skill must not use, because skills run under any agent. Plain
85
+ * verbs such as "Read" or "Edit" are fine; these patterns catch tool-shaped uses only.
86
+ */
87
+ const AGENT_TOOL_PATTERNS = [
88
+ { pattern: /\b(TodoWrite|WebFetch|WebSearch|AskUserQuestion|NotebookEdit|MultiEdit|ExitPlanMode|SlashCommand|subagent_type)\b/, label: "a Claude Code tool name" },
89
+ { pattern: /`(Read|Write|Edit|Bash|Grep|Glob|Task|Agent|Skill|LS)`/, label: "a tool name in backticks" },
90
+ { pattern: /\b(Read|Write|Edit|Bash|Grep|Glob|Task|Agent|Skill)\s+tool\b/, label: "a named tool" },
91
+ { pattern: /\b(Bash|Read|Write|Edit|WebFetch)\([^)]*\)/, label: "a tool permission pattern" },
92
+ ];
93
+
94
+ /**
95
+ * Lines of a skill file that name an agent-specific tool. Fenced code blocks are skipped: they
96
+ * hold data such as a config example, which may be written for one agent.
97
+ */
98
+ export function agentToolProblems(where , text ) {
99
+ let inFence = false;
100
+ return text.split("\n").flatMap((line, i) => {
101
+ if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
102
+ if (inFence) return [];
103
+ const hit = AGENT_TOOL_PATTERNS.find(({ pattern }) => pattern.test(line));
104
+ return hit === undefined ? [] : [`${where}:${i + 1}: names ${hit.label}; describe the action instead`];
105
+ });
106
+ }