@printpractical/cruze 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +6 -5
  2. package/dist/adapters/inbound/cli/cli_context.js +2 -0
  3. package/dist/adapters/inbound/cli/commands/code.js +2 -1
  4. package/dist/adapters/inbound/cli/commands/journal.js +1 -1
  5. package/dist/adapters/inbound/cli/commands/show.js +32 -0
  6. package/dist/adapters/inbound/cli/commands/status.js +19 -3
  7. package/dist/adapters/inbound/cli/commands.js +3 -1
  8. package/dist/adapters/outbound/git_repository.js +40 -7
  9. package/dist/adapters/outbound/project_root.js +13 -0
  10. package/dist/app/use_cases/approve_artifact.js +19 -1
  11. package/dist/app/use_cases/check_code.js +10 -3
  12. package/dist/app/use_cases/journal_events.js +8 -5
  13. package/dist/app/use_cases/project_status.js +4 -1
  14. package/dist/app/use_cases/show_scope.js +37 -0
  15. package/dist/domain/check/budget_text.js +61 -0
  16. package/dist/domain/check/check_source.js +7 -4
  17. package/dist/domain/config.js +23 -1
  18. package/dist/domain/edits/agents_file.js +6 -1
  19. package/dist/domain/journal.js +13 -2
  20. package/dist/domain/project/scope_view.js +134 -0
  21. package/dist/domain/status/next_step.js +25 -2
  22. package/dist/domain/status/verification.js +12 -0
  23. package/dist/domain/status/work_status.js +13 -2
  24. package/dist/domain/validation/element_rules.js +5 -0
  25. package/dist/domain/validation/plan_rules.js +1 -1
  26. package/dist/domain/validation/size_rules.js +14 -5
  27. package/dist/domain/validation/work_item_rules.js +25 -3
  28. package/dist/domain/versions/rule_versions.js +1 -0
  29. package/dist/main.js +14 -4
  30. package/package.json +1 -1
  31. package/skills/architect/SKILL.md +6 -1
  32. package/skills/architect/feature.md +1 -1
  33. package/skills/architect/walkthrough.md +7 -4
  34. package/skills/build/SKILL.md +10 -8
  35. package/skills/dependency-approval/SKILL.md +8 -1
  36. package/skills/envision/SKILL.md +2 -0
  37. package/skills/envision/feature.md +1 -1
  38. package/skills/explore/SKILL.md +1 -1
  39. package/skills/formats/reference/architecture.md +1 -1
  40. package/skills/formats/reference/config.md +13 -4
  41. package/skills/formats/reference/specs.md +1 -1
  42. package/skills/formats/reference/work-items.md +1 -1
  43. package/skills/grilling/SKILL.md +19 -7
  44. package/skills/grilling/illustrating.md +37 -0
  45. package/skills/hexagonal-design/SKILL.md +2 -1
  46. package/skills/hexagonal-design/languages/rust.md +2 -1
  47. package/skills/improve/SKILL.md +3 -3
  48. package/skills/land/SKILL.md +2 -2
  49. package/skills/next/SKILL.md +1 -1
  50. package/skills/plan/SKILL.md +3 -3
  51. package/skills/plan/tasks.md +9 -5
  52. package/skills/realign/SKILL.md +2 -2
  53. package/skills/realign/notes/0.0.8.md +97 -0
  54. package/skills/realign/notes/0.0.9.md +59 -0
  55. package/skills/realign/notes.md +2 -0
  56. package/skills/research/SKILL.md +2 -1
  57. package/skills/rethink/SKILL.md +3 -3
  58. package/skills/rethink/levels.md +2 -2
  59. package/skills/roadmap/SKILL.md +2 -0
  60. package/skills/roles/SKILL.md +8 -10
  61. package/skills/roles/code-reviewer.md +10 -9
  62. package/skills/roles/design-reviewer.md +22 -12
  63. package/skills/roles/surveyor.md +1 -1
  64. package/skills/roles/verifier.md +8 -5
  65. package/skills/triage/SKILL.md +2 -0
  66. package/skills/verify/SKILL.md +6 -4
  67. package/src/adapters/inbound/cli/cli_context.ts +2 -0
  68. package/src/adapters/inbound/cli/commands/code.ts +2 -1
  69. package/src/adapters/inbound/cli/commands/journal.ts +1 -1
  70. package/src/adapters/inbound/cli/commands/show.ts +32 -0
  71. package/src/adapters/inbound/cli/commands/status.ts +19 -3
  72. package/src/adapters/inbound/cli/commands.ts +3 -1
  73. package/src/adapters/outbound/git_repository.ts +40 -7
  74. package/src/adapters/outbound/project_root.ts +13 -0
  75. package/src/app/use_cases/approve_artifact.ts +19 -1
  76. package/src/app/use_cases/check_code.ts +10 -3
  77. package/src/app/use_cases/journal_events.ts +8 -5
  78. package/src/app/use_cases/project_status.ts +4 -1
  79. package/src/app/use_cases/show_scope.ts +37 -0
  80. package/src/domain/check/budget_text.ts +61 -0
  81. package/src/domain/check/check_source.ts +7 -4
  82. package/src/domain/config.ts +23 -1
  83. package/src/domain/edits/agents_file.ts +6 -1
  84. package/src/domain/journal.ts +13 -2
  85. package/src/domain/project/scope_view.ts +134 -0
  86. package/src/domain/status/next_step.ts +25 -2
  87. package/src/domain/status/verification.ts +12 -0
  88. package/src/domain/status/work_status.ts +13 -2
  89. package/src/domain/validation/element_rules.ts +5 -0
  90. package/src/domain/validation/plan_rules.ts +1 -1
  91. package/src/domain/validation/size_rules.ts +14 -5
  92. package/src/domain/validation/work_item_rules.ts +25 -3
  93. package/src/domain/versions/rule_versions.ts +1 -0
  94. package/src/main.ts +14 -4
  95. package/templates/init/AGENTS.md +1 -1
  96. package/templates/init/config.yaml +5 -4
@@ -0,0 +1,134 @@
1
+ import { elementLines, } from "../elements.js";
2
+ import { findIds } from "../ids.js";
3
+ import { findSection, } from "../markdown.js";
4
+ import { readDelta } from "./deltas.js";
5
+ import { readScope } from "./plan_parts.js";
6
+
7
+ import { featureOf, } from "./work_items.js";
8
+
9
+ /**
10
+ * The design a piece of work builds, cut out of the documents that hold it, so a step reads
11
+ * the elements in its scope instead of the whole architecture and the whole feature.
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
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+ export function scopeView(view , item ) {
54
+ const feature = featureOf(view.items, item);
55
+ const deltaItem = feature ?? item;
56
+ const locate = locator(view, deltaItem);
57
+ const wanted = item.kind === "change" || item.kind === "standalone" ? changeIds(item) : featureIds(deltaItem);
58
+ const missing = [];
59
+ const show = (ids ) =>
60
+ ids.flatMap((id) => {
61
+ const found = locate(id);
62
+ if (found === undefined) {
63
+ missing.push(id);
64
+ return [];
65
+ }
66
+ const shown = { id, title: found.element.title, source: `${found.path}:${found.element.start + 1}`, body: elementLines(found.doc, found.element) };
67
+ if (found.element.op !== undefined) shown.op = found.element.op;
68
+ return [shown];
69
+ });
70
+ const elements = show(wanted.elements);
71
+ const scenarios = withoutRepeatedScenarios(show(wanted.scenarios));
72
+ const shownIds = new Set([...elements, ...scenarios].map((e) => e.id));
73
+ const cited = [...new Set([...elements, ...scenarios].flatMap((e) => findIds(e.body.join("\n"))))]
74
+ .filter((id) => !shownIds.has(id))
75
+ .flatMap((id) => {
76
+ const found = locate(id);
77
+ return found === undefined ? [] : [{ id, title: found.element.title, source: `${found.path}:${found.element.start + 1}` }];
78
+ });
79
+ const decisions = decisionLines((feature ?? item).doc);
80
+ const changeDecisions = feature === undefined ? [] : decisionLines(item.doc);
81
+ const result = { ref: item.ref, kind: item.kind, elements, scenarios, cited, decisions, changeDecisions, missing };
82
+ if (feature !== undefined) result.feature = feature.ref;
83
+ return result;
84
+ }
85
+
86
+ /**
87
+ * A requirement shown beside its own scenarios keeps only its own text: its body would repeat
88
+ * each of them, which made up a third of one change's output.
89
+ */
90
+ function withoutRepeatedScenarios(shown ) {
91
+ const ids = new Set(shown.map((e) => e.id));
92
+ return shown.map((element) => {
93
+ if (!element.id.startsWith("REQ-")) return element;
94
+ const first = element.body.findIndex((line, i) => i > 0 && /^#{2,6} /.test(line) && findIds(line).some((id) => ids.has(id)));
95
+ return first === -1 ? element : { ...element, body: element.body.slice(0, first) };
96
+ });
97
+ }
98
+
99
+ /** A change shows what its scope builds, removes and delivers. */
100
+ function changeIds(change ) {
101
+ const scope = readScope(change.doc);
102
+ return { elements: [...(scope?.builds ?? []), ...(scope?.removes ?? [])], scenarios: scope?.delivers ?? [] };
103
+ }
104
+
105
+ /** A feature shows its whole delta: every operation, with a requirement's scenarios inside its body. */
106
+ function featureIds(feature ) {
107
+ const entries = readDelta(feature.doc).entries;
108
+ return {
109
+ elements: entries.filter((e) => e.section === "architecture").map((e) => e.element.id),
110
+ scenarios: entries.filter((e) => e.section === "spec").map((e) => e.element.id),
111
+ };
112
+ }
113
+
114
+ /** Finds an element's body in the work's own delta first, since that is the text the work builds, then in the living docs. */
115
+ function locator(view , deltaItem ) {
116
+ const delta = readDelta(deltaItem.doc);
117
+ const inDelta = new Map ();
118
+ for (const entry of delta.entries) {
119
+ inDelta.set(entry.element.id, entry.element);
120
+ for (const scenario of entry.scenarios) inDelta.set(scenario.id, scenario);
121
+ }
122
+ return (id) => {
123
+ const own = inDelta.get(id);
124
+ if (own !== undefined) return { doc: deltaItem.doc, path: deltaItem.path, element: own };
125
+ const living = view.living.elements.get(id);
126
+ return living === undefined ? undefined : { doc: living.doc, path: living.path, element: living.element };
127
+ };
128
+ }
129
+
130
+ function decisionLines(doc ) {
131
+ const section = findSection(doc, "Settled decisions");
132
+ if (section === null) return [];
133
+ return doc.lines.slice(section.start, section.end).filter((line) => line.startsWith("- "));
134
+ }
@@ -7,7 +7,7 @@ import { readRoadmapItems, readRoadmapStatus } from "../project/roadmap.js";
7
7
  import { changesOf, featureOf, } from "../project/work_items.js";
8
8
  import { compareVersions } from "../versions/cruze_version.js";
9
9
  import { FIRST_RELEASE } from "../versions/rule_versions.js";
10
- import { isVerified } from "./verification.js";
10
+ import { awaitsRethink, isVerified } from "./verification.js";
11
11
  import { activeChange } from "./work_status.js";
12
12
 
13
13
  /** The Cruze step to run next, computed from the project's state and the current branch. */
@@ -31,7 +31,7 @@ export function nextSteps(view , branch )
31
31
  const project = projectStep(view);
32
32
  if (project !== null) return { next: project, also: [] };
33
33
  const active = activeChange(view, branch);
34
- const inFlight = view.items.filter((item) => !item.archived && item.kind !== "change").flatMap((item) => itemSteps(view, item, branch));
34
+ const inFlight = inRoadmapOrder(view, view.items.filter((item) => !item.archived && item.kind !== "change")).flatMap((item) => itemSteps(view, item, branch));
35
35
  const roadmap = roadmapStep(view);
36
36
  const realign = realignStep(view);
37
37
  const candidates = [
@@ -45,6 +45,28 @@ export function nextSteps(view , branch )
45
45
  return { next: next ?? { step: "roadmap", reason: "every item on the roadmap has landed; close the release and plan the next" }, also };
46
46
  }
47
47
 
48
+ /**
49
+ * Work in flight in the roadmap's order, with items whose roadmap blockers haven't all landed
50
+ * last: a feature designed early still waits for the prefactors the user put before it.
51
+ */
52
+ function inRoadmapOrder(view , items ) {
53
+ const text = view.snapshot.get(PATHS.roadmap);
54
+ if (text === undefined) return items;
55
+ const rows = readRoadmapStatus(text);
56
+ const state = new Map(rows.map((row) => [row.slug, row.state]));
57
+ const slugOf = new Map(rows.map((row) => [row.id, row.slug]));
58
+ const roadmap = readRoadmapItems(text);
59
+ const rank = (item ) => {
60
+ const at = roadmap.findIndex((r) => r.slug === slugOf.get(item.ref));
61
+ const blocked = at !== -1 && (roadmap[at]?.blockedBy ?? []).some((b) => state.get(b) !== "landed");
62
+ return [blocked ? 1 : 0, at === -1 ? roadmap.length : at];
63
+ };
64
+ return items
65
+ .map((item, i) => ({ item, key: [...rank(item), i] }))
66
+ .sort((a, b) => (a.key[0] ?? 0) - (b.key[0] ?? 0) || (a.key[1] ?? 0) - (b.key[1] ?? 0) || (a.key[2] ?? 0) - (b.key[2] ?? 0))
67
+ .map(({ item }) => item);
68
+ }
69
+
48
70
  /** Realign comes when nothing else is due: the installed skills expect more than the code meets. */
49
71
  function realignStep(view ) {
50
72
  const config = view.config?.config;
@@ -117,6 +139,7 @@ function changeStep(view , change , branch )
117
139
  if (!isPlanned(change.doc)) return { step: "plan", target: change.ref, reason: "the change is designed but has no test plan or tasks yet" };
118
140
  if (state.state === "unapproved") return { step: "plan", target: change.ref, reason: "review the plan and approve it on the branch that will build it" };
119
141
  if (state.state !== "approved") return { step: "rethink", target: change.ref, reason: `the change or something it cites changed since its approval${state.changed.length > 0 ? ` (${state.changed.join(", ")})` : ""}` };
142
+ if (awaitsRethink(view.journal, change, feature)) return { step: "rethink", target: feature?.ref ?? change.ref, reason: `verify sent ${change.ref} back for a design change` };
120
143
  if (bound !== undefined && bound !== branch) return { step: "switch-branch", target: bound, reason: `${change.ref} is built on branch ${bound}` };
121
144
  const progress = readProgress(change.doc);
122
145
  if ([...progress.tasks.values()].some((commit) => commit === null)) return { step: "build", target: change.ref, reason: "the change has open tasks" };
@@ -15,3 +15,15 @@ export function isVerified(journal , change ) {
15
15
  const verifiedHash = approvals.filter((e) => e.at < verification.at).at(-1)?.["hash"];
16
16
  return approvals.filter((e) => e.at > verification.at).every((e) => e["hash"] === verifiedHash);
17
17
  }
18
+
19
+ /**
20
+ * Whether verify sent the change back for a rethink that hasn't been recorded yet. Tasks the
21
+ * agent reopens at verify would otherwise make the change look ready to build again.
22
+ */
23
+ export function awaitsRethink(journal , change , feature ) {
24
+ const verification = journal.filter((e) => e.event === "verification" && e["item"] === change.ref).sort((a, b) => a.at.localeCompare(b.at)).at(-1);
25
+ if (verification?.["result"] !== "sent-back" || verification["back_to"] !== "rethink") return false;
26
+ // An architecture-level rethink records no item, and counts too.
27
+ const refs = new Set([change.ref, feature?.ref, undefined]);
28
+ return !journal.some((e) => e.event === "rethink" && e.at > verification.at && refs.has(e["item"] ));
29
+ }
@@ -1,7 +1,7 @@
1
1
  import { evaluateApproval, } from "../approvals/evaluate.js";
2
2
  import { PATHS } from "../project/layout.js";
3
3
  import { readDelta } from "../project/deltas.js";
4
- import { readScope } from "../project/plan_parts.js";
4
+ import { readChangesTable, readScope } from "../project/plan_parts.js";
5
5
  import { readProgress } from "../project/progress.js";
6
6
 
7
7
  import { changesOf, featureOf, } from "../project/work_items.js";
@@ -80,12 +80,23 @@ export function buildGateReasons(view , branch )
80
80
  const feature = featureOf(view.items, change);
81
81
  if (feature !== undefined) required.push(feature.path);
82
82
  if (view.snapshot.has(PATHS.architecture)) required.push(PATHS.architecture);
83
- return required.flatMap((path) => {
83
+ const approvals = required.flatMap((path) => {
84
84
  const status = evaluateApproval(view, path);
85
85
  if (status.state === "approved") return [];
86
86
  const detail = status.changed.length > 0 ? `: ${status.changed.join(", ")} changed` : status.unjournaled === true ? ": the approval has no journal entry" : "";
87
87
  return [`${path} is ${status.state}${detail}`];
88
88
  });
89
+ return [...approvals, ...unlandedDependencies(view, change, feature)];
90
+ }
91
+
92
+ /** The changes this change's row in its feature's Changes table depends on that haven't landed on this branch. */
93
+ function unlandedDependencies(view , change , feature ) {
94
+ if (feature === undefined) return [];
95
+ const row = readChangesTable(feature.doc).find((r) => r.change === change.folderName);
96
+ const landed = new Set(changesOf(view.items, feature).filter((c) => readProgress(c.doc).landed !== undefined).map((c) => c.folderName));
97
+ return (row?.dependsOn ?? [])
98
+ .filter((dependency) => !landed.has(dependency))
99
+ .map((dependency) => `${change.ref} depends on ${dependency}, which hasn't landed here; land it, then merge main into this branch`);
89
100
  }
90
101
 
91
102
  /** The elements a change touches: its scope and its delta, not the elements it only cites, such as a test seam. */
@@ -41,6 +41,11 @@ export function elementProblems(path , doc , element
41
41
  problems.push(error(path, element.start, "fact-value", `${element.id} ${key} must be one of ${rule.values.join(", ")}`));
42
42
  }
43
43
  }
44
+ if (element.kind === "MOD") {
45
+ for (const glob of (facts.get("Path") ?? []).flatMap((value) => [...value.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "")).filter((p) => /[*?[{]/.test(p))) {
46
+ problems.push(error(path, element.start, "module-path-glob", `${element.id} Path \`${glob}\` is a glob, which matches no file; name a directory ending in / or a single file, one per Path value`));
47
+ }
48
+ }
44
49
  const module = facts.get("Module")?.[0];
45
50
  if (module !== undefined && !findIds(module).some((id) => id.startsWith("MOD-"))) {
46
51
  problems.push(error(path, element.start, "fact-value", `${element.id} Module must name a MOD element`));
@@ -46,7 +46,7 @@ export function changePlanProblems(
46
46
  if (kindOf(id) === "MOD") {
47
47
  const modulePaths = modulePathsOf(view, delta.doc, delta.delta, id);
48
48
  if (!taskPaths.some((p) => modulePaths.some((m) => p.startsWith(m)))) {
49
- problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}`));
49
+ problems.push(error(path, scope.line, "unproved-scope", `${id} is in scope but no task writes inside ${modulePaths.join(" or ") || "its path"}; a module that owns other files, such as a build script, lists each under its Path`));
50
50
  }
51
51
  } else if (kindOf(id) === "REQ") {
52
52
  const scenarios = delta.delta.entries.find((e) => e.element.id === id)?.scenarios.map((s) => s.id) ?? [];
@@ -12,17 +12,25 @@ export function changeLimits(view ) {
12
12
  return view.config?.config?.changes ?? DEFAULT_CHANGE_LIMITS;
13
13
  }
14
14
 
15
+ /** The rules a size warning comes from. An item stays outside the band only with the user's recorded acceptance. */
16
+ export const SIZE_RULES = ["feature-size", "change-too-small", "change-too-large", "plan-too-large"];
17
+
18
+ /** Whether the user accepted this feature or change outside the band, under `changes.exceptions`. */
19
+ export function sizeAccepted(view , ref ) {
20
+ return changeLimits(view).exceptions.some((exception) => exception.item === ref);
21
+ }
22
+
15
23
  /** A feature's changes not yet landed that fall outside the size band. A feature of one change has nothing to merge into. */
16
- export function changeRowSizeProblems(view , path , rows , landed ) {
24
+ export function changeRowSizeProblems(view , feature , rows , landed ) {
17
25
  const limits = changeLimits(view);
18
26
  const problems = [];
19
- for (const row of rows.filter((r) => !landed.has(r.change))) {
27
+ for (const row of rows.filter((r) => !landed.has(r.change) && !sizeAccepted(view, `${feature.ref}/${r.change}`))) {
20
28
  if (rows.length > 1 && row.builds.length < limits.minBuilds) {
21
- problems.push(warning(path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
29
+ problems.push(warning(feature.path, row.line, "change-too-small", `${row.change} builds ${row.builds.length} element(s), under ${limits.minBuilds}; merge it into the change it continues, since each change pays for its own plan, review and verify`));
22
30
  }
23
31
  const scenarios = row.delivers.filter((id) => kindOf(id) === "SCN").length;
24
32
  if (scenarios > limits.maxScenarios) {
25
- problems.push(warning(path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
33
+ problems.push(warning(feature.path, row.line, "change-too-large", `${row.change} delivers ${scenarios} scenarios, over ${limits.maxScenarios}; split it, since one verifier run has to go through them all`));
26
34
  }
27
35
  }
28
36
  return problems;
@@ -31,6 +39,7 @@ export function changeRowSizeProblems(view , path , rows
31
39
  /** A plan with more tasks than one code review and one verify can hold. */
32
40
  export function planSizeProblems(view , path , tasks ) {
33
41
  const { maxTasks } = changeLimits(view);
34
- if (tasks <= maxTasks) return [];
42
+ const ref = view.items.find((item) => item.path === path)?.ref ?? path;
43
+ if (tasks <= maxTasks || sizeAccepted(view, ref)) return [];
35
44
  return [warning(path, undefined, "plan-too-large", `the plan has ${tasks} tasks, over ${maxTasks}; split the change through rethink at feature level`)];
36
45
  }
@@ -11,7 +11,7 @@ import { deltaProblems } from "./delta_rules.js";
11
11
  import { malformedIdHeadings, lineOf } from "./living_rules.js";
12
12
  import { changePlanProblems } from "./plan_rules.js";
13
13
  import { error, warning, } from "./problem.js";
14
- import { changeRowSizeProblems } from "./size_rules.js";
14
+ import { changeRowSizeProblems, sizeAccepted } from "./size_rules.js";
15
15
 
16
16
  const REQUIRED_SECTIONS = {
17
17
  feature: ["Intent", "Spec delta", "Architecture delta", "Changes"],
@@ -45,6 +45,10 @@ function itemProblems(view , item ) {
45
45
  for (const cited of findIds(text).filter((cited) => !available.has(cited))) {
46
46
  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`));
47
47
  }
48
+ const dropped = droppedScenarios(view, delta);
49
+ for (const cited of findIds(text).filter((cited) => dropped.has(cited))) {
50
+ problems.push(error(path, lineOf(text, cited), "cites-dropped", `${cited} is cited, but ${dropped.get(cited)} in this work's delta leaves it out, so it won't exist once the work lands; describe it in words instead`));
51
+ }
48
52
  if (item.kind !== "change") problems.push(...deltaProblems(view, item));
49
53
  if (item.kind === "feature") problems.push(...featureScopeProblems(view, item, delta));
50
54
  // A landed change is history: its scope named what it built, which is now built.
@@ -69,10 +73,10 @@ function featureScopeProblems(view , feature , delta
69
73
  );
70
74
  const names = rows.map((row) => row.change);
71
75
  const extra = rows[MAX_CHANGES];
72
- if (extra !== undefined) {
76
+ if (extra !== undefined && !sizeAccepted(view, feature.ref)) {
73
77
  problems.push(warning(feature.path, extra.line, "feature-size", `the feature has ${rows.length} changes, over ${MAX_CHANGES}; split it into features a single design review can hold`));
74
78
  }
75
- problems.push(...changeRowSizeProblems(view, feature.path, rows, landedChanges));
79
+ problems.push(...changeRowSizeProblems(view, feature, rows, landedChanges));
76
80
  for (const row of rows) {
77
81
  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`));
78
82
  if (names.indexOf(row.change) !== names.lastIndexOf(row.change)) problems.push(error(feature.path, row.line, "change-name", `change ${row.change} is listed twice`));
@@ -151,3 +155,21 @@ function scopeIds(item ) {
151
155
  const scope = readScope(item.doc);
152
156
  return scope === null ? [] : [...scope.delivers, ...scope.builds, ...scope.removes];
153
157
  }
158
+
159
+ /**
160
+ * Living scenarios that a MODIFIED requirement in the delta leaves out, each with that requirement.
161
+ * Land drops them, so a citation of one passes validation now and breaks only at land.
162
+ */
163
+ function droppedScenarios(view , delta ) {
164
+ const dropped = new Map ();
165
+ for (const entry of delta.entries.filter((e) => e.element.op === "MODIFIED" && kindOf(e.element.id) === "REQ")) {
166
+ const living = view.living.elements.get(entry.element.id);
167
+ if (living === undefined) continue;
168
+ const kept = new Set(entry.scenarios.map((s) => s.id));
169
+ for (const scenario of view.living.elements.values()) {
170
+ const nested = scenario.doc === living.doc && scenario.element.start > living.element.start && scenario.element.start < living.element.end;
171
+ if (nested && kindOf(scenario.id) === "SCN" && !kept.has(scenario.id) && !delta.ids.has(scenario.id)) dropped.set(scenario.id, entry.element.id);
172
+ }
173
+ }
174
+ return dropped;
175
+ }
@@ -16,6 +16,7 @@ export const RULE_SINCE = {
16
16
  "validate:change-too-small": "0.0.5",
17
17
  "validate:change-too-large": "0.0.5",
18
18
  "validate:plan-too-large": "0.0.5",
19
+ "validate:module-path-glob": "0.0.8",
19
20
  };
20
21
 
21
22
  /** Whether a rule arrived after the version the project's code meets, so it waits for `realign`. */
package/dist/main.js CHANGED
@@ -1,27 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  // Composition root: the only place that constructs adapters.
3
- import { basename } from "node:path";
3
+ import { basename, relative } from "node:path";
4
4
  import { runCli } from "./adapters/inbound/cli/run_cli.js";
5
5
  import { GitRepository } from "./adapters/outbound/git_repository.js";
6
6
  import { NodeProjectFiles } from "./adapters/outbound/node_project_files.js";
7
7
  import { PackageBundle } from "./adapters/outbound/package_bundle.js";
8
+ import { findProjectRoot } from "./adapters/outbound/project_root.js";
8
9
  import { ProcessAgentRunner } from "./adapters/outbound/process_agent_runner.js";
9
10
  import { SystemClock } from "./adapters/outbound/system_clock.js";
10
11
  import { DefaultAnswerPrompter, TerminalPrompter } from "./adapters/outbound/terminal_prompter.js";
11
12
 
13
+ // A reader that stops early, such as `cruze journal list | head`, closes the pipe; that is not an error worth a stack trace.
14
+ process.stdout.on("error", (error ) => {
15
+ if (error.code === "EPIPE") process.exit(0);
16
+ throw error;
17
+ });
18
+
12
19
  const cwd = process.cwd();
20
+ // Every command but init works on the project that holds the working directory, so it runs from any subdirectory.
21
+ const root = process.argv[2] === "init" ? cwd : (findProjectRoot(cwd) ?? cwd);
13
22
  const exitCode = await runCli(
14
23
  process.argv.slice(2),
15
24
  {
16
- files: new NodeProjectFiles(cwd),
25
+ files: new NodeProjectFiles(root),
17
26
  bundle: await PackageBundle.locate(),
18
27
  clock: new SystemClock(),
19
- repository: new GitRepository(cwd),
28
+ repository: new GitRepository(root),
20
29
  interactivePrompter: new TerminalPrompter(),
21
30
  unattendedPrompter: new DefaultAnswerPrompter(),
22
- runner: new ProcessAgentRunner(cwd),
31
+ runner: new ProcessAgentRunner(root),
23
32
  interactive: process.stdin.isTTY === true,
24
33
  directoryName: basename(cwd),
34
+ workingDirectory: relative(root, cwd).split("\\").join("/"),
25
35
  },
26
36
  {
27
37
  stdout: (text) => process.stdout.write(`${text}\n`),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@printpractical/cruze",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Spec-driven development for coding agents: one living model of the system, small deltas, convergent review.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Chris Buchter",
@@ -32,7 +32,8 @@ Architect decides the how and the how big. It designs the domain, the ports and
32
32
 
33
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
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`.
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. Never split an owner across files to keep them short.
36
+ - **Comply with the references.** When the project must follow an external document, such as a protocol specification, it is listed under `references:` in `.cruze/config.yaml`; at project scope, ask the user whether there is one, and list it with what it governs. Read the parts each element relies on, and cite the section in the element's body, such as `RFC 4254 §5.2`, so plan, build and the reviewers can check it.
36
37
  - **Ask, challenge and defer** as the grilling skill describes. Contexts, ports, public contracts, dependency direction and dependencies are always the user's decisions.
37
38
  - **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
 
@@ -41,3 +42,7 @@ Architect decides the how and the how big. It designs the domain, the ports and
41
42
  - Project scope: `plan` for the walking-skeleton change.
42
43
  - Feature scope: `plan` for the feature's first change.
43
44
  - Tweak scope: `plan` for the standalone change.
45
+
46
+ Plan branches from main, so get the approved design onto main first, as a pull request or a merge, together with the vision or feature map it builds on. A plan branched from an unmerged design branch carries the design into the change's pull request.
47
+
48
+ Start it in a new session, so its context starts empty.
@@ -33,6 +33,6 @@ The result is a `feature.md` with a complete architecture delta and an ordered l
33
33
 
34
34
  The thresholds are the defaults under `changes:` in `.cruze/config.yaml`; a project can tune them.
35
35
 
36
- Done when `cruze validate` reports no `change-too-small`, `change-too-large` or `feature-size` warning, or the user has accepted one and a `D<n>` line under the feature's `## Decisions` records why. After approval, only a rethink can split a change.
36
+ Done when `cruze validate` reports no `change-too-small`, `change-too-large` or `feature-size` warning. When the user accepts one, record it twice: a `D<n>` line under the feature's `## Settled decisions` saying why, and the item under `changes.exceptions` in `.cruze/config.yaml` with the same reason, which is what stops the warning. `cruze approve` refuses an item with a warning still standing (`size-unaccepted`). After approval, only a rethink can split a change.
37
37
 
38
38
  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.
@@ -6,12 +6,15 @@ At feature scope, cover only what the feature adds, changes or removes. At proje
6
6
 
7
7
  ## The overview
8
8
 
9
- Write it in the glossary's terms, as what callers can do and what happens when they do, not as a list of elements. Keep it to what the user can read in a minute.
9
+ Write it in the glossary's terms, as what callers can do and what happens when they do, not as a list of elements. Keep the prose to what the user can read in a minute, and open it with the line that says where things stand, from `.agents/skills/cruze-grilling/illustrating.md`.
10
10
 
11
11
  1. **What it does.** The intent in two sentences. Then one line per use case, or at feature scope per requirement: who asks for what, what they get, and the main way it fails.
12
12
  2. **What it touches.** Each new external system, and each new dependency named with its purpose and the approval behind it. Also each component you chose to build yourself, with the reason.
13
- 3. **The slices.** The changes in order, one line each, saying what each delivers and why the first is first. At project scope, the roadmap's phases and the walking skeleton instead.
14
- 4. **What is yours to decide.** The review findings the roles skill brings to the user, any `cruze validate` warning still standing, such as a change over its size limit, any point where a fresh implementer could still build something different, and any rule you challenged that the user overrode.
13
+ 3. **What it looks like.** Illustrated as `.agents/skills/cruze-grilling/illustrating.md` says:
14
+ - A plain-text diagram of the main scenario crossing the design: the inbound adapter, the use case, the domain elements and the ports, marking what is new.
15
+ - A code sketch, in the project's language, of what the new and changed elements look like: the port operations and use case signatures with their errors, the main entity, and the call an inbound adapter makes.
16
+ 4. **The slices.** The changes in order, one line each, saying what each delivers and why the first is first. At project scope, the roadmap's phases and the walking skeleton instead.
17
+ 5. **What is yours to decide.** The review findings the roles skill brings to the user, any `cruze validate` warning still standing, such as a change over its size limit, any point where a fresh implementer could still build something different, and any rule you challenged that the user overrode.
15
18
 
16
19
  ## The documents
17
20
 
@@ -20,7 +23,7 @@ List each file the user should read in full, with the sections that changed:
20
23
  - Feature scope: the feature's `feature.md`, and the ADRs it adds.
21
24
  - Project scope: `docs/architecture.md`, the `layers:` in `.cruze/config.yaml`, `docs/roadmap.md`, the walking-skeleton `change.md`, and the ADRs in `docs/adr/`.
22
25
 
23
- The diagrams are Mermaid inside those files. Say where the user can render them, such as an editor preview or GitHub, rather than pasting them.
26
+ The documents' diagrams are Mermaid. Say where the user can render them, such as an editor preview or GitHub, rather than pasting them; the overview's plain-text sketches are for reading in the conversation.
24
27
 
25
28
  End the message with: "Read these in full, then tell me what to change, or approve. Do you approve this design?"
26
29
 
@@ -16,11 +16,11 @@ Build turns an approved plan into code, one task at a time. Each task starts wit
16
16
  - `.agents/skills/cruze-behavioural-testing/SKILL.md`
17
17
  - `.agents/skills/cruze-hexagonal-design/SKILL.md`, and its language file when one exists for the project's language
18
18
  - `.agents/skills/cruze-dependency-approval/SKILL.md`
19
- 3. **Read the plan and the design.** Read the change's `change.md` and its feature's `feature.md`. Read the full body of every element the change builds, and every scenario it delivers. Read `commands:` in `.cruze/config.yaml`.
19
+ 3. **Read the plan and the design.** Read the change's `change.md`. Run `cruze show <ref>` and read its output: the full body of every element the change builds, every scenario it delivers, and the settled decisions. Read an element it lists as cited only when the task calls it, from the place it names, never `feature.md` or `docs/architecture.md` whole. Read a reference it lists at the sections the change's elements cite. Read `commands:` in `.cruze/config.yaml`.
20
20
  4. **Build each open task in order,** following the task loop below. Skip the tasks already ticked in `## Progress`.
21
21
  5. **Finish.** When every task is ticked:
22
22
  - Run the format, lint and build commands and the full suite, as `.agents/skills/cruze-formats/reference/config.md` defines it under `commands`.
23
- - Run `cruze check` and `cruze trace`, and quote their results.
23
+ - Run `cruze check --ci` and `cruze trace`, and quote their results.
24
24
  - When all of them pass, record the commit they ran at with `cruze journal add checks --set commit=<commit> --item <ref>`, where the commit is the output of `git rev-parse --short HEAD`. Verify quotes this instead of running the suite again.
25
25
  - Commit the progress record with a message such as `chore: record progress on <change>`.
26
26
  - Done when all of them pass and the checks are recorded.
@@ -30,28 +30,30 @@ Build turns an approved plan into code, one task at a time. Each task starts wit
30
30
  For task `T<n>`:
31
31
 
32
32
  1. **Read it.** Note its owner, its files, the IDs it proves, and the test plan rows for those IDs.
33
- 2. **Red.** Write the tests for the IDs it proves, at the seams the test plan names, carrying the IDs. Run them, and see them fail for the reason you expect.
33
+ 2. **Red.** Write the tests for the IDs it proves, at the seams the test plan names, carrying the IDs. Run them, and see them fail for the reason you expect. When the task names a break, use it, or any break that compiles and turns the same assertion red.
34
34
  3. **Green.** Write the least code that makes them pass, in the task's files. Code that belongs to another task, or another change, waits for it, even when the next change will need it.
35
- 4. **Check.** Run `cruze check <the task's files>`, the formatter and the linter, and fix what they report. Run the tests the task touched. When `commands:` has a `test_full`, run `test` too, since it is the fast tier. Otherwise run the rest of the suite only when it is quick.
35
+ 4. **Check.** Run `cruze check --ci <the task's files>`, the formatter and the linter, and fix what they report. Meet a budget as the budget rule in `.agents/skills/cruze-hexagonal-design/SKILL.md` says, never by bending the code. Run the tests the task touched. When `commands:` has a `test_full`, run `test` too, since it is the fast tier. Otherwise run the rest of the suite only when it is quick.
36
36
  5. **Commit** with a Conventional Commit message naming the task, such as `feat(access): console session state machine (T2)`.
37
37
  6. **Tick it.** Run `cruze task done T<n>`, which records the commit. Its progress edit rides along in the next commit.
38
38
 
39
39
  A task with no behaviour of its own, such as a package manifest or a plain data type, has no red step. Build it, check it, commit it and tick it, and the first task whose test uses it covers it.
40
40
 
41
- Done for a task when its tests pass, `cruze check` passes for its files, the commit exists and the task is ticked.
41
+ Done for a task when its tests pass, `cruze check --ci` passes for its files, the commit exists and the task is ticked.
42
42
 
43
43
  ## Deviations
44
44
 
45
45
  The plan is design. Within a task you decide only what is local and reversible.
46
46
 
47
- - **Record it and carry on** for a task-level choice: a private helper in another file of the same module, a private type, a fixture or harness added to the test support module that the task's tests need, or two tasks' commits swapped for a reason. The test support module is in every test-writing task's scope, so extend it rather than writing a private helper in a test file. Run `cruze task deviation T<n> "<what you did and why>"`.
47
+ - **Record it and carry on** for a task-level choice: a private helper in another file of the same module, a new file of the owner's module that splits off a responsibility, a `check.exceptions` entry with its reason, a private type, a fixture or harness added to the test support module that the task's tests need, or two tasks' commits swapped for a reason. The test support module is in every test-writing task's scope, so extend it rather than writing a private helper in a test file. Run `cruze task deviation T<n> "<what you did and why>"`.
48
48
  - **Stop and offer `rethink`** when the code needs any of these:
49
49
  - a new element, port operation or scenario;
50
50
  - a file outside the owner's module, other than the test support module;
51
51
  - a change to an approved scenario's behaviour, or to its test;
52
- - a dependency the design doesn't name.
52
+ - a dependency, or a feature of an approved dependency, that the design doesn't name. Ask for it as `.agents/skills/cruze-dependency-approval/SKILL.md` says, and never write around it with hand-written code or a shell command;
53
+ - a limit, timeout, buffer size, message or error case the design doesn't give. Picking one in code hides a gap in the design: the user decides it, at feature level. Two projects settled 35 of them as deviations.
53
54
 
54
55
  Explain what you found, with the evidence, and don't work around it. Completed tasks keep their commits.
56
+ - **Read a deviation back before you record it.** One that says the design gives no value, text, error or outcome and names the one you chose, or that the code can't keep a promise a port or use case makes, is a rethink, not a deviation. In one change, two deviations worded that way came back as code-review blockers and sent it back at verify.
55
57
  - Never edit `feature.md`, `docs/architecture.md` or `docs/specs/` during build. Design changes go through `rethink`.
56
58
 
57
59
  ## Protecting the check
@@ -60,4 +62,4 @@ Never weaken, skip or delete a behaviour, contract or smoke test for an approved
60
62
 
61
63
  ## Next step
62
64
 
63
- `verify`, once every task is ticked and the finishing checks pass.
65
+ `verify`, once every task is ticked and the finishing checks pass. Start it in a new session, so its context starts empty: one session that ran plan, build, verify and land reached 745k tokens.
@@ -9,12 +9,19 @@ Every new dependency needs the user's explicit approval before it is declared or
9
9
 
10
10
  ## Asking
11
11
 
12
- 1. Establish the need: the capability, and why the standard library and the project's already-approved dependencies don't provide it.
12
+ 1. Establish the need: the capability, and what the design looks like with the dependency and without it, using the standard library and the project's already-approved dependencies. Show both as short code sketches, following `.agents/skills/cruze-grilling/illustrating.md`.
13
13
  2. Choose a recommendation, following `.agents/skills/cruze-research/SKILL.md`, which also says where versions come from.
14
14
  3. Say where it will sit: the module that owns it, and the port that keeps it out of the core when it is infrastructure.
15
15
  4. Name the realistic alternatives, including writing it yourself and what owning that code would cost. Never offer a hand-written alternative for cryptography, TLS or a security protocol.
16
16
  5. Ask, then wait. Nothing is declared, installed or written against it until the user answers.
17
17
 
18
+ ## Weighing
19
+
20
+ - Recommend the option that gives the cleanest design: the one an experienced developer in the language would write, with the least code to own. A dependency, a new crate or package, or a feature of an approved dependency is not a cost in itself. Weigh its concrete costs: maintenance and release health, security record, licence, and build time or binary size where a stated constraint makes them matter.
21
+ - "No new dependency" is never the reason for a choice. Name what the chosen option does better.
22
+ - A structure the language requires for an idiom, such as a separate crate for a derive macro, is part of that idiom, not an extra cost.
23
+ - When the clean design needs a dependency or feature nobody approved yet, ask for it. Hand-written code, a shell command or a less idiomatic construct that exists only to avoid asking is a design flaw.
24
+
18
25
  ```markdown
19
26
  **Dependency: `russh` for SSH hops**
20
27
  - Why not std or existing: the standard library has no SSH client, and no approved dependency provides one.
@@ -27,3 +27,5 @@ Envision decides the what: the problem, who has it, and the behaviour that solve
27
27
 
28
28
  - After project scope: run `architect` at project scope.
29
29
  - After feature scope: run `architect "<feature>"`.
30
+
31
+ Start it in a new session, so its context starts empty.
@@ -18,6 +18,6 @@ The result is a feature folder whose `feature.md` holds the intent, the settled
18
18
  - Name every ID with glossary terms. An ID is never reused.
19
19
  - Done when every requirement has a scenario for its main case and one for each failure a user can meet.
20
20
  8. **Leave the design to architect.** Set `## Architecture delta` to `None.` and leave the Changes table with its header row only. Remove the remaining template guides.
21
- 9. **Confirm.** Summarize the intent and each requirement with its scenarios, in plain words, and ask the user to confirm them. Apply any corrections.
21
+ 9. **Confirm.** Summarize the intent and each requirement with its scenarios, in plain words, showing each requirement's main scenario as the user would see it, and ask the user to confirm them. Apply any corrections.
22
22
 
23
23
  Done when the user has confirmed, `cruze validate` reports no errors for the feature, only its `not-designed` warning, and the feature is committed.
@@ -12,7 +12,7 @@ Explore is the optional thinking partner that can come before any other Cruze st
12
12
  1. **Frame the problem.** Ask what prompted the idea, who it affects, and what outcome would make it worthwhile. If the user starts from a solution, restate the problem underneath it and confirm it. Done when you can state the problem in one sentence the user agrees with.
13
13
  2. **Read what exists.** Read whichever of these exist: `docs/vision.md`, `docs/glossary.md`, `docs/architecture.md`, `docs/roadmap.md`, and the code the idea touches. Run `cruze status` to see work in flight. Use the glossary's words from here on, following `.agents/skills/cruze-domain-language/SKILL.md`.
14
14
  3. **Separate facts from guesses.** List what is known evidence, what is a hypothesis, and what is a preference. Look up any fact you can rather than asking the user for it.
15
- 4. **Diverge.** Lay out two to four genuinely different approaches, including leaving things as they are when that is credible. For each, give what it changes, what it leaves alone, its benefits, its costs and failure modes, the assumptions it rests on, and what evidence would tell it apart from the others. Sketch an interaction as a Mermaid `sequenceDiagram` or `flowchart` when that makes a difference clearer.
15
+ 4. **Diverge.** Lay out two to four genuinely different approaches, including leaving things as they are when that is credible. For each, give what it changes, what it leaves alone, its benefits, its costs and failure modes, the assumptions it rests on, and what evidence would tell it apart from the others. Show what each approach looks like, following `.agents/skills/cruze-grilling/illustrating.md`.
16
16
  5. **Converge.** Compare the strongest options against the user's outcome, the vision's goals and constraints, and the architecture. Challenge assumptions and accidental scope growth, following the challenge rules in `.agents/skills/cruze-grilling/SKILL.md`. Recommend one option only when its trade-offs are understood. Otherwise recommend the smallest step that would settle the question, such as a spike, a measurement or a research question.
17
17
  6. **Offer to capture.** Summarize the problem, the options, the trade-offs, your recommendation and the open questions, then ask whether to capture them. When the user declines, stop here: the discussion itself was the result.
18
18
 
@@ -35,7 +35,7 @@ Facts are `- Key: value` items. Keys marked required must be present; the others
35
35
  | `PORT` | `Direction` (`driven` or `driving`), `Operations`, `Module`, `File` | `Implemented by`, `Future` |
36
36
  | `ADP` | `Implements` (a driven port) or `Drives` (use cases), `Technology`, `Module`, `File` | `Adopts` (library and adopt-or-build reason), `Format` (the stored or wire format it owns, or where its schema lives) |
37
37
  | `FLOW` | `Elements`, a Mermaid `sequenceDiagram` | `Serves` (SCN IDs, once they exist), `Failure paths` |
38
- | `MOD` | `Path`, `Layer` (`core`, `domain`, `application`, `adapter` or `composition`) | |
38
+ | `MOD` | `Path` (a directory ending in `/`, or one file; list several in backticks, never a glob, including every file the module owns outside its directory, such as a build script), `Layer` (`core`, `domain`, `application`, `adapter` or `composition`) | |
39
39
  | `RULE` | one or two sentences of rule | `Enforced by` (config layer names) |
40
40
  | `XC` | prose | |
41
41