@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,35 @@
1
+ ---
2
+ name: cruze-triage
3
+ description: Handles a bug report by reproducing it, finding its root cause against the living docs, and fixing a code defect with a failing test first, or sending a spec gap to architect and a design flaw to rethink.
4
+ ---
5
+
6
+ # Triage
7
+
8
+ Triage starts from a symptom and ends with a fix, or with the right step to fix it. A bug has three possible causes, and each goes somewhere different. When the code doesn't do what the specs say, triage fixes it. When the specs never covered the case, it goes to `architect` as a tweak. When the design can't deliver the specs, it goes to `rethink`.
9
+
10
+ ## Steps
11
+
12
+ 1. **Capture the symptom.** Get the exact steps, what happened (output, error, exit code), what the user expected, and the environment. Done when you can write the steps as commands.
13
+ 2. **Reproduce it.** Run the steps against the real system. Done when you can make it fail on demand and have quoted the output. When you can't reproduce it, ask the user for what differs in their environment, and stop until you can.
14
+ 3. **Place it in the living docs.** Find the requirement and scenarios in `docs/specs/` that cover this behaviour, and the flow in `docs/architecture.md` that runs it. Then classify it:
15
+ - **Code defect:** a scenario says what should happen, and the code does something else.
16
+ - **Spec gap:** no scenario covers this case.
17
+ - **Design flaw:** the design as written can't deliver what the scenario says.
18
+ 4. **Confirm the diagnosis.** Tell the user the classification, the scenario or its absence, and your evidence. They confirm it before anything changes. For a spec gap, stop and hand off to `architect "<tweak>"`. For a design flaw, stop and hand off to `rethink`.
19
+ 5. **Find the root cause.** Trace the failing path through the code until you can name the line that is wrong and why. The cause is the first wrong decision, not the place the error surfaces.
20
+ 6. **Write the failing test first.** On a branch such as `fix/<slug>`, add a test at the seam of the violated scenario, following `.agents/skills/cruze-behavioural-testing/SKILL.md`. It carries the scenario's ID and reproduces the bug. Run it, and see it fail for this bug's reason.
21
+ 7. **Fix it.** Make the smallest change in the module that owns the wrong decision, following `.agents/skills/cruze-hexagonal-design/SKILL.md`. Run `cruze check` on the files you changed, then the full test suite. Done when the new test passes and nothing else broke.
22
+ 8. **Commit** with a message such as `fix(<scope>): <what the user saw> (SCN-<id>)`.
23
+ 9. **Record it** with `cruze journal add bug --set summary="<what the user saw>" --set cause="<the root cause>" --set scenario=<SCN id> --set escaped="<the step that should have caught it>"`. `escaped` names the Cruze step that let the bug through, such as `verify`, or `none` when no step could have.
24
+
25
+ ## Rules
26
+
27
+ - A missing test is part of the cause. When the violated scenario had a test that passed anyway, say why it missed the bug, and strengthen it in the same fix.
28
+ - Fix the cause only. Clean-ups you notice nearby go to the user as suggestions.
29
+ - Never change a scenario to match the code. That is a spec change, and it goes through `architect`.
30
+
31
+ ## Next step
32
+
33
+ - Code defect fixed: `land`, which lands bug fixes too.
34
+ - Spec gap: `architect "<tweak>"`.
35
+ - Design flaw: `rethink`.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: cruze-verify
3
+ description: Verifies a built change before it lands, with the mechanical checks, a fresh-context verifier that runs the real system, a two-lane code review and a manual test script, ending with your decision to accept it or send it back.
4
+ ---
5
+
6
+ # Verify
7
+
8
+ Verify answers one question: does this change work when someone uses it? Green tests are evidence, not proof. A verifier in a fresh context runs the real system through each scenario, reviewers check the code against the spec and the architecture, and the user gets a script to try it themselves. The user decides whether it lands.
9
+
10
+ ## Steps
11
+
12
+ 1. **Check it is ready.** Run `cruze next`. It must name `verify` for this change, on this branch. Find the base commit the change branched from with `git merge-base main HEAD`.
13
+ 2. **Run the mechanical checks.** Run the project's format, lint, build and test commands from `commands:` in `.cruze/config.yaml`, then `cruze validate`, `cruze check --ci` and `cruze trace --change <ref>`. Quote each result. When one fails, fix the code in a commit and run them again. When a fix needs design, stop and offer `rethink`.
14
+ 3. **Check the dependencies.** Follow "Checking" in `.agents/skills/cruze-dependency-approval/SKILL.md` against the base commit. An unrecorded dependency is a blocker.
15
+ 4. **Run the verifier.** Run the verifier role from `.agents/skills/cruze-roles/SKILL.md` for the change. It must be able to build and run the system. When it runs through `cruze review`, check that `review.command` in `.cruze/config.yaml` allows the project's build and run commands, and add them with the user's agreement. Done when you have its report, with a result for every delivered scenario.
16
+ 5. **Review the code.** Run the review in the roles skill with the code reviewer, `review=code`, `--item <ref>` and the base commit. Fix each finding the user marks `fixed` in a commit, then check the fixes in round 2.
17
+ 6. **Write the manual test script** as `manual-test.md` in the change folder, following [manual-test.md](manual-test.md). Commit it.
18
+ 7. **Report.** Show the user four things:
19
+ - The mechanical checks, one line each.
20
+ - The verifier's result for every scenario.
21
+ - The review's outcome, with each finding's disposition.
22
+ - The path to `manual-test.md`, with a recommendation to run it before accepting.
23
+ 8. **Ask for the decision:** "Do you accept this change, or send it back?"
24
+ 9. **Record it** with `cruze journal add verification --set result=accepted --set summary="<one line>" --item <ref>`, or `result=sent-back`. On sent-back, say what goes back:
25
+ - A defect in code a task already built: `cruze task reopen T<n> "<reason>"`, then `build`.
26
+ - A design that can't deliver the scenario: `rethink`.
27
+
28
+ Done when the decision is recorded.
29
+
30
+ ## Rules
31
+
32
+ - A scenario the verifier couldn't run isn't passed. Mark it clearly in the report and in `manual-test.md`.
33
+ - Findings from the review are dispositions in the journal, never edits to `change.md`.
34
+ - Fixes made during verify are code commits that stay within the tasks' files. When a fix needs a new file or owner, it is a deviation or a rethink, exactly as in build.
35
+
36
+ ## Next step
37
+
38
+ - Accepted: `land`.
39
+ - Sent back: `build` for reopened tasks, or `rethink` for design.
@@ -0,0 +1,23 @@
1
+ # The manual test script
2
+
3
+ `manual-test.md` in the change folder is what a person runs to see the change work with their own eyes. Write it for someone who knows the product but not the code.
4
+
5
+ ```markdown
6
+ # Manual test: <change title>
7
+
8
+ Setup, once:
9
+ 1. <build or install command, from the project's commands>
10
+ 2. <any configuration file to create, with its full content>
11
+
12
+ ## SCN-<id>: <scenario title>
13
+ 1. <exact command or action>
14
+ 2. <next action>
15
+ Expected: <what they see, quoted from the scenario's THEN>
16
+ Verifier: pass | fail | could not run (<what it needs>)
17
+ ```
18
+
19
+ - One section per scenario the change delivers, in the order of the spec delta.
20
+ - Every step is a command they can paste or an action they can take, with real values.
21
+ - Expected results are quoted from the scenario.
22
+ - Scenarios the verifier couldn't run come first, since they carry the most risk. Say what each needs, such as a serial device or a jump host.
23
+ - Finish with how to clean up anything the setup created.
@@ -0,0 +1,75 @@
1
+ import type { ProjectDeps } from "../../../app/project_context.ts";
2
+ import type { AgentRunner } from "../../../app/ports/agent_runner.ts";
3
+ import type { Prompter } from "../../../app/ports/prompter.ts";
4
+
5
+ /** Everything a command handler needs, assembled by the composition root. */
6
+ export interface CliContext extends ProjectDeps {
7
+ /** Asks a person; used only when the terminal is interactive and --yes is absent. */
8
+ interactivePrompter: Prompter;
9
+ /** Accepts every default. */
10
+ unattendedPrompter: Prompter;
11
+ interactive: boolean;
12
+ /** Starts an agent in a fresh context for `cruze review`. */
13
+ runner: AgentRunner;
14
+ /** Name of the working directory, the default project name. */
15
+ directoryName: string;
16
+ }
17
+
18
+ /** What a command produces: a JSON result for agents, a summary for people, and whether it passed. */
19
+ export interface CommandResult {
20
+ json: unknown;
21
+ human: string;
22
+ /** Exit code 1 without an error: a check ran and found problems. */
23
+ failed?: boolean;
24
+ }
25
+
26
+ /** Parsed options, shared by every command; each handler reads the ones it accepts. */
27
+ export interface Options {
28
+ name?: string;
29
+ yes: boolean;
30
+ agent: string[];
31
+ json: boolean;
32
+ title?: string;
33
+ feature?: string;
34
+ roadmap?: string;
35
+ change?: string;
36
+ commit?: string;
37
+ summary?: string;
38
+ goals?: string;
39
+ reason?: string;
40
+ set: string[];
41
+ item?: string;
42
+ event?: string;
43
+ out?: string;
44
+ redact: boolean;
45
+ all: boolean;
46
+ file: string[];
47
+ ci: boolean;
48
+ gate?: string;
49
+ override?: string;
50
+ overlap: boolean;
51
+ replan: string[];
52
+ rebase: string[];
53
+ round?: string;
54
+ package?: string;
55
+ base?: string;
56
+ blockers?: string;
57
+ }
58
+
59
+ export type Handler = (context: CliContext, args: string[], options: Options) => Promise<CommandResult>;
60
+
61
+ export class UsageError extends Error {
62
+ constructor(message: string) {
63
+ super(message);
64
+ this.name = "UsageError";
65
+ }
66
+ }
67
+
68
+ export function requireArgs(args: string[], names: string[]): void {
69
+ if (args.length < names.length) throw new UsageError(`missing ${names.slice(args.length).map((n) => `<${n}>`).join(" ")}`);
70
+ }
71
+
72
+ export function requireOption<T>(value: T | undefined, flag: string): T {
73
+ if (value === undefined) throw new UsageError(`${flag} is required`);
74
+ return value;
75
+ }
@@ -0,0 +1,30 @@
1
+ import { checkCode, traceTests } from "../../../../app/use_cases/check_code.ts";
2
+ import { landChange } from "../../../../app/use_cases/land_change.ts";
3
+ import type { CliContext, CommandResult, Options } from "../cli_context.ts";
4
+
5
+ export async function runCheck(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
6
+ const files = [...options.file, ...args];
7
+ const report = await checkCode(context, { ci: options.ci, ...(files.length === 0 ? {} : { files }) });
8
+ const lines = report.findings.map((f) => `${f.severity} ${f.path}${f.line === undefined ? "" : `:${f.line}`} ${f.message} [${f.rule}]`);
9
+ lines.push(`Checked ${report.checked} file(s): ${report.passed ? "passed" : "failed"}.`);
10
+ return { json: report, human: lines.join("\n"), failed: !report.passed };
11
+ }
12
+
13
+ export async function runTrace(context: CliContext, _args: string[], options: Options): Promise<CommandResult> {
14
+ const report = await traceTests(context, { all: options.all, ...(options.change === undefined ? {} : { change: options.change }) });
15
+ const lines = report.missing.map((id) => `missing test for ${id}`);
16
+ lines.push(...report.unknown.map((u) => `${u.path}:${u.line} cites ${u.id}, which no spec or delta defines`));
17
+ lines.push(`Traced ${report.scenarios.length} scenario(s) for ${report.scope}: ${report.passed ? "passed" : "failed"}.`);
18
+ return { json: report, human: lines.join("\n"), failed: !report.passed };
19
+ }
20
+
21
+ export async function runLand(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
22
+ const report = await landChange(context, args[0], options.override === undefined ? {} : { override: options.override });
23
+ const lines = [`Landed ${report.change}.`];
24
+ if (report.merged.length > 0) lines.push(`Merged into the living docs: ${report.merged.join(", ")}`);
25
+ if (report.built.length > 0) lines.push(`Now built: ${report.built.join(", ")}`);
26
+ if (report.removed.length > 0) lines.push(`Removed: ${report.removed.join(", ")}`);
27
+ if (report.restamped.length > 0) lines.push(`Re-stamped approvals: ${report.restamped.join(", ")}`);
28
+ if (report.archivedTo !== undefined) lines.push(`Finished; archived to ${report.archivedTo}.`);
29
+ return { json: report, human: lines.join("\n") };
30
+ }
@@ -0,0 +1,52 @@
1
+ import { approveArtifact } from "../../../../app/use_cases/approve_artifact.ts";
2
+ import { PROJECT_DOC_KINDS, createNote, createProjectDoc, type ProjectDocKind } from "../../../../app/use_cases/new_project_doc.ts";
3
+ import { createWorkItem, type NewKind } from "../../../../app/use_cases/new_work_item.ts";
4
+ import { validate } from "../../../../app/use_cases/validate_project.ts";
5
+ import type { Problem } from "../../../../domain/validation/problem.ts";
6
+ import { UsageError, requireArgs, requireOption, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
7
+
8
+ export async function runValidate(context: CliContext): Promise<CommandResult> {
9
+ const report = await validate(context);
10
+ const summary = report.problems.length === 0 ? "No problems." : `${report.errors} error(s), ${report.warnings} warning(s).`;
11
+ return { json: report, human: [...report.problems.map(formatProblem), summary].join("\n"), failed: !report.valid };
12
+ }
13
+
14
+ export async function runApprove(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
15
+ requireArgs(args, ["document"]);
16
+ const report = await approveArtifact(context, args[0] ?? "", { replan: options.replan, rebase: options.rebase });
17
+ const lines = [`Approved ${report.artifact} (${report.hash.slice(0, 19)}), pinned to ${report.upstream.length} upstream element(s).`];
18
+ if (report.stamped.length > 0) lines.push(`Marked ${report.stamped.length} element(s) planned.`);
19
+ if (report.bound !== undefined) lines.push(`Bound the change to branch ${report.bound}.`);
20
+ if (report.replanned.length > 0) lines.push(`Set back to planned: ${report.replanned.join(", ")}.`);
21
+ if (report.rebased.length > 0) lines.push(`Accepted the current living text of: ${report.rebased.join(", ")}.`);
22
+ return { json: report, human: lines.join("\n") };
23
+ }
24
+
25
+ export async function runNew(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
26
+ requireArgs(args, ["kind"]);
27
+ if (PROJECT_DOC_KINDS.includes(args[0] as ProjectDocKind)) {
28
+ const report = await createProjectDoc(context, args[0] as ProjectDocKind);
29
+ return { json: report, human: `Created ${report.path}` };
30
+ }
31
+ requireArgs(args, ["kind", "slug"]);
32
+ if (args[0] === "note") {
33
+ const report = await createNote(context, args[1] ?? "", requireOption(options.title, "--title"));
34
+ return { json: report, human: `Created ${report.path}` };
35
+ }
36
+ const kind = args[0] as NewKind;
37
+ if (!["feature", "change", "adr"].includes(kind)) {
38
+ throw new UsageError(`cruze new takes ${PROJECT_DOC_KINDS.join(", ")}, feature, change, adr or note, not "${args[0]}"`);
39
+ }
40
+ const report = await createWorkItem(context, {
41
+ kind,
42
+ slug: args[1] ?? "",
43
+ title: requireOption(options.title, "--title"),
44
+ ...(options.feature === undefined ? {} : { feature: options.feature }),
45
+ ...(options.roadmap === undefined ? {} : { roadmap: options.roadmap }),
46
+ });
47
+ return { json: report, human: `Created ${report.kind} ${report.ref} at ${report.path}` };
48
+ }
49
+
50
+ export function formatProblem(problem: Problem): string {
51
+ return `${problem.severity === "error" ? "error" : "warning"} ${problem.path}${problem.line === undefined ? "" : `:${problem.line}`} ${problem.message} [${problem.rule}]`;
52
+ }
@@ -0,0 +1,23 @@
1
+ import { initProject } from "../../../../app/use_cases/init_project.ts";
2
+ import type { CliContext, CommandResult, Options } from "../cli_context.ts";
3
+ import { describeSkillInstall } from "./install.ts";
4
+
5
+ export async function runInit(context: CliContext, _args: string[], options: Options): Promise<CommandResult> {
6
+ const prompter = options.yes || !context.interactive ? context.unattendedPrompter : context.interactivePrompter;
7
+ const report = await initProject(
8
+ { files: context.files, bundle: context.bundle, prompter },
9
+ {
10
+ defaultName: context.directoryName,
11
+ agents: options.agent,
12
+ ...(options.name === undefined ? {} : { name: options.name }),
13
+ ...(options.package === undefined ? {} : { package: options.package }),
14
+ },
15
+ );
16
+
17
+ const lines = [`Initialized ${report.project} with Cruze ${report.skills.version}.`];
18
+ if (report.created.length > 0) lines.push(`Created: ${report.created.join(", ")}`);
19
+ if (report.skipped.length > 0) lines.push(`Left untouched (already present): ${report.skipped.join(", ")}`);
20
+ lines.push(...describeSkillInstall(report.skills));
21
+ lines.push("Next: open your coding agent in this repository and ask it what Cruze can do.");
22
+ return { json: report, human: lines.join("\n") };
23
+ }
@@ -0,0 +1,15 @@
1
+ import { installSkills, type InstallSkillsReport } from "../../../../app/use_cases/install_skills.ts";
2
+ import type { CliContext, CommandResult, Options } from "../cli_context.ts";
3
+
4
+ export async function runInstall(context: CliContext, _args: string[], options: Options): Promise<CommandResult> {
5
+ const report = await installSkills({ files: context.files, bundle: context.bundle }, { agents: options.agent });
6
+ return { json: report, human: describeSkillInstall(report).join("\n") };
7
+ }
8
+
9
+ export function describeSkillInstall(report: InstallSkillsReport): string[] {
10
+ const count = report.installed.length;
11
+ const lines = [`Installed ${count} Cruze skill${count === 1 ? "" : "s"} into .agents/skills/: ${report.installed.join(", ")}`];
12
+ if (report.removed.length > 0) lines.push(`Removed skills no longer shipped: ${report.removed.join(", ")}`);
13
+ for (const link of report.linked) lines.push(`Linked them for ${link.agent} in ${link.dir}/`);
14
+ return lines;
15
+ }
@@ -0,0 +1,31 @@
1
+ import { exportFeedback, listEvents, recordEvent } from "../../../../app/use_cases/journal_events.ts";
2
+ import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
3
+
4
+ export async function runJournal(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
5
+ requireArgs(args, ["add|list"]);
6
+ if (args[0] === "add") {
7
+ requireArgs(args, ["add", "event"]);
8
+ const fields = Object.fromEntries(options.set.map((pair) => {
9
+ const at = pair.indexOf("=");
10
+ if (at < 1) throw new UsageError(`--set takes key=value, not "${pair}"`);
11
+ return [pair.slice(0, at), pair.slice(at + 1)];
12
+ }));
13
+ const entry = await recordEvent(context, args[1] ?? "", fields, options.item);
14
+ return { json: entry, human: `Recorded ${entry.event}.` };
15
+ }
16
+ if (args[0] === "list") {
17
+ const entries = await listEvents(context, options.event);
18
+ return { json: entries, human: entries.map((e) => `${e.at} ${e.event} ${JSON.stringify({ ...e, at: undefined, event: undefined, by: undefined })}`).join("\n") || "No entries." };
19
+ }
20
+ throw new UsageError(`cruze journal takes add or list, not "${args[0]}"`);
21
+ }
22
+
23
+ export async function runFeedback(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
24
+ if (args[0] !== "export") throw new UsageError("usage: cruze feedback export [--out <file>] [--no-redact]");
25
+ const bundle = await exportFeedback(context, options.redact);
26
+ if (options.out !== undefined) {
27
+ await context.files.writeText(options.out, `${JSON.stringify(bundle, null, 2)}\n`);
28
+ return { json: { out: options.out, entries: bundle.entries.length }, human: `Wrote ${bundle.entries.length} entries to ${options.out}.` };
29
+ }
30
+ return { json: bundle, human: `${bundle.entries.length} journal entries${bundle.redacted ? ", redacted" : ""}.` };
31
+ }
@@ -0,0 +1,51 @@
1
+ import { addFutureFeature, completeTask, dropFutureFeature, pruneRoadmap, recordDeviation, reopenTask } from "../../../../app/use_cases/record_progress.ts";
2
+ import { abandonWork } from "../../../../app/use_cases/abandon_work.ts";
3
+ import { UsageError, requireArgs, requireOption, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
4
+
5
+ export async function runTask(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
6
+ requireArgs(args, ["done|deviation|reopen", "task"]);
7
+ const change = options.change === undefined ? {} : { change: options.change };
8
+ if (args[0] === "done") {
9
+ const report = await completeTask(context, args[1] ?? "", { ...change, ...(options.commit === undefined ? {} : { commit: options.commit }) });
10
+ return { json: report, human: `${report.task} done in ${report.change} (${report.done}/${report.total}).` };
11
+ }
12
+ if (args[0] === "deviation") {
13
+ requireArgs(args, ["deviation", "task", "text"]);
14
+ const report = await recordDeviation(context, args[1] ?? "", args.slice(2).join(" "), change);
15
+ return { json: report, human: `Recorded a deviation on ${report.task} in ${report.change}.` };
16
+ }
17
+ if (args[0] === "reopen") {
18
+ requireArgs(args, ["reopen", "task", "reason"]);
19
+ const report = await reopenTask(context, args[1] ?? "", args.slice(2).join(" "), change);
20
+ return { json: report, human: `Reopened ${report.task} in ${report.change}.` };
21
+ }
22
+ throw new UsageError(`cruze task takes done, deviation or reopen, not "${args[0]}"`);
23
+ }
24
+
25
+ export async function runFeatures(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
26
+ requireArgs(args, ["add|drop", "slug"]);
27
+ if (args[0] === "add") {
28
+ const goals = (options.goals ?? "").split(",").map((g) => g.trim()).filter((g) => g !== "");
29
+ const report = await addFutureFeature(context, args[1] ?? "", requireOption(options.summary, "--summary"), goals);
30
+ return { json: report, human: `Added ${report.feature} to the future list.` };
31
+ }
32
+ if (args[0] === "drop") {
33
+ const report = await dropFutureFeature(context, args[1] ?? "", requireOption(options.reason, "--reason"));
34
+ return { json: report, human: `Dropped ${report.feature} from the future list.` };
35
+ }
36
+ throw new UsageError(`cruze features takes add or drop, not "${args[0]}"`);
37
+ }
38
+
39
+ export async function runRoadmap(context: CliContext, args: string[]): Promise<CommandResult> {
40
+ requireArgs(args, ["prune"]);
41
+ if (args[0] !== "prune") throw new UsageError(`cruze roadmap takes prune, not "${args[0]}"`);
42
+ const report = await pruneRoadmap(context);
43
+ return { json: report, human: report.removed.length === 0 ? "No landed items to clear." : `Cleared ${report.removed.join(", ")} from the roadmap status.` };
44
+ }
45
+
46
+ export async function runAbandon(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
47
+ requireArgs(args, ["ref"]);
48
+ const report = await abandonWork(context, args[0] ?? "", requireOption(options.reason, "--reason"));
49
+ const back = report.futureList === undefined ? "" : ` ${report.futureList} is back on the future list.`;
50
+ return { json: report, human: `Abandoned ${report.ref}; archived to ${report.archivedTo}.${back}` };
51
+ }
@@ -0,0 +1,18 @@
1
+ import { runReview } from "../../../../app/use_cases/run_review.ts";
2
+ import { UsageError, requireArgs, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
3
+
4
+ export async function runReviewCommand(context: CliContext, args: string[], options: Options): Promise<CommandResult> {
5
+ requireArgs(args, ["role"]);
6
+ const round = options.round === undefined ? undefined : Number(options.round);
7
+ if (round !== undefined && round !== 1 && round !== 2) throw new UsageError("--round is 1 or 2");
8
+ const blockers = options.blockers === undefined ? undefined : await context.files.readText(options.blockers);
9
+ if (options.blockers !== undefined && blockers === undefined) throw new UsageError(`cannot read ${options.blockers}`);
10
+ const report = await runReview(context, {
11
+ role: args[0] ?? "",
12
+ ...(options.item === undefined ? {} : { item: options.item }),
13
+ ...(round === undefined ? {} : { round }),
14
+ ...(blockers === undefined ? {} : { blockers }),
15
+ ...(options.base === undefined ? {} : { base: options.base }),
16
+ });
17
+ return { json: report, human: report.report };
18
+ }
@@ -0,0 +1,45 @@
1
+ import { checkBuildGate, findOverlaps, showStatus, suggestNext } from "../../../../app/use_cases/project_status.ts";
2
+ import type { ApprovalStatus } from "../../../../domain/approvals/evaluate.ts";
3
+ import { UsageError, type CliContext, type CommandResult, type Options } from "../cli_context.ts";
4
+
5
+ export async function runStatus(context: CliContext, _args: string[], options: Options): Promise<CommandResult> {
6
+ if (options.gate !== undefined) {
7
+ if (options.gate !== "build") throw new UsageError(`unknown gate "${options.gate}"; the only gate is build`);
8
+ const report = await checkBuildGate(context, options.override);
9
+ const human = report.passed
10
+ ? report.overridden ? `Build gate overridden and journaled:\n ${report.reasons.join("\n ")}` : "Build gate passed."
11
+ : `Build gate blocked:\n ${report.reasons.join("\n ")}`;
12
+ return { json: report, human, failed: !report.passed };
13
+ }
14
+ if (options.overlap) {
15
+ const report = await findOverlaps(context, options.change);
16
+ const human = report.overlaps.length === 0
17
+ ? `No other branch touches what ${report.change} touches.`
18
+ : report.overlaps.map((o) => `${o.branch}: ${o.item} also touches ${o.shared.join(", ")}`).join("\n");
19
+ return { json: report, human };
20
+ }
21
+ const report = await showStatus(context);
22
+ const lines = [`Branch: ${report.branch ?? "(none)"}${report.active === undefined ? "" : `, building ${report.active}`}`];
23
+ for (const doc of report.documents) lines.push(describe(doc.artifact, doc));
24
+ for (const feature of report.features) {
25
+ lines.push(describe(`feature ${feature.ref}`, feature.approval));
26
+ for (const change of feature.changes) {
27
+ const name = change.ref.split("/")[1] ?? change.ref;
28
+ lines.push(change.landed !== undefined ? ` ${name}: landed ${change.landed}` : ` ${describe(name, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
29
+ }
30
+ }
31
+ for (const change of report.standalone) lines.push(`${describe(`change ${change.ref}`, change.approval)}, tasks ${change.tasksDone}/${change.tasksTotal}`);
32
+ return { json: report, human: lines.join("\n") };
33
+ }
34
+
35
+ function describe(label: string, status: ApprovalStatus): string {
36
+ const detail = status.changed.length > 0 ? ` (${status.changed.join(", ")})` : status.unjournaled === true ? " (no journal entry)" : "";
37
+ return `${label}: ${status.state}${detail}`;
38
+ }
39
+
40
+ export async function runNext(context: CliContext): Promise<CommandResult> {
41
+ const report = await suggestNext(context);
42
+ const describe = (s: { step: string; target?: string; reason: string }): string => `${s.step}${s.target === undefined ? "" : ` ${s.target}`}: ${s.reason}`;
43
+ const lines = [`Next: ${describe(report.next)}`, ...report.also.map((s) => `Also: ${describe(s)}`)];
44
+ return { json: report, human: lines.join("\n") };
45
+ }
@@ -0,0 +1,36 @@
1
+ import type { Handler } from "./cli_context.ts";
2
+ import { runCheck, runLand, runTrace } from "./commands/code.ts";
3
+ import { runApprove, runNew, runValidate } from "./commands/documents.ts";
4
+ import { runInit } from "./commands/init.ts";
5
+ import { runInstall } from "./commands/install.ts";
6
+ import { runFeedback, runJournal } from "./commands/journal.ts";
7
+ import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.ts";
8
+ import { runReviewCommand } from "./commands/review.ts";
9
+ import { runNext, runStatus } from "./commands/status.ts";
10
+
11
+ export const COMMANDS: Record<string, { handler: Handler; usage: string }> = {
12
+ init: { handler: runInit, usage: "init [--name <name>] [--yes] [--agent <agent>] [--package <npm spec>] set up this repository and install the skills" },
13
+ install: { handler: runInstall, usage: "install [--agent <agent>] install or update the Cruze skills" },
14
+ validate: { handler: runValidate, usage: "validate check every document against the formats" },
15
+ status: { handler: runStatus, usage: "status [--gate build [--override <reason>]] [--overlap [--change <ref>]]" },
16
+ next: { handler: runNext, usage: "next the step to run next on this branch, and why" },
17
+ approve: { handler: runApprove, usage: "approve <vision|architecture|roadmap|ref|path> [--replan <ID>...] [--rebase <ID>...] record your approval of a document" },
18
+ new: { handler: runNew, usage: "new <vision|glossary|architecture|roadmap> | new <feature|change|adr|note> <slug> --title <title> [--feature <ref>] [--roadmap <slug>]" },
19
+ task: { handler: runTask, usage: "task <done|deviation|reopen> <T#> [text] [--change <ref>] [--commit <sha>]" },
20
+ features: { handler: runFeatures, usage: "features <add|drop> <slug> [--summary <text> --goals <ids>] [--reason <text>]" },
21
+ roadmap: { handler: runRoadmap, usage: "roadmap prune clear landed items from the roadmap status when a release closes" },
22
+ journal: { handler: runJournal, usage: "journal <add <event> --set key=value... [--item <ref>]|list [--event <event>]>" },
23
+ review: { handler: runReviewCommand, usage: "review <role> [--item <ref>] [--round 1|2 --blockers <file>] [--base <commit>] run a role in a fresh agent context" },
24
+ feedback: { handler: runFeedback, usage: "feedback export [--out <file>] [--no-redact]" },
25
+ trace: { handler: runTrace, usage: "trace [--all] [--change <ref>] link scenarios to the tests that prove them" },
26
+ check: { handler: runCheck, usage: "check [<file>...] [--ci] enforce layer rules and file budgets" },
27
+ abandon: { handler: runAbandon, usage: "abandon <ref> --reason <text> archive a feature or standalone change that stops for good" },
28
+ land: { handler: runLand, usage: "land [<ref>] [--override <reason>] merge a verified change into the living docs" },
29
+ };
30
+
31
+ export const USAGE = `Usage: cruze <command> [options]
32
+
33
+ Commands:
34
+ ${Object.values(COMMANDS).map((c) => ` ${c.usage}`).join("\n")}
35
+
36
+ Every command prints JSON on stdout when it isn't a terminal, or with --json.`;
@@ -0,0 +1,90 @@
1
+ import { parseArgs } from "node:util";
2
+ import { UsageError, type Options } from "./cli_context.ts";
3
+
4
+ export interface ParsedCommand {
5
+ /** The command and, for grouped commands, its subcommand, such as `new feature`. */
6
+ words: string[];
7
+ options: Options;
8
+ help: boolean;
9
+ version: boolean;
10
+ }
11
+
12
+ export function parseCommand(argv: string[]): ParsedCommand {
13
+ let parsed;
14
+ try {
15
+ parsed = parseArgs({
16
+ args: argv,
17
+ allowPositionals: true,
18
+ options: {
19
+ name: { type: "string" },
20
+ yes: { type: "boolean", short: "y", default: false },
21
+ agent: { type: "string", multiple: true, default: [] },
22
+ json: { type: "boolean", default: false },
23
+ version: { type: "boolean", short: "v", default: false },
24
+ help: { type: "boolean", short: "h", default: false },
25
+ title: { type: "string" },
26
+ feature: { type: "string" },
27
+ roadmap: { type: "string" },
28
+ change: { type: "string" },
29
+ commit: { type: "string" },
30
+ summary: { type: "string" },
31
+ goals: { type: "string" },
32
+ reason: { type: "string" },
33
+ set: { type: "string", multiple: true, default: [] },
34
+ item: { type: "string" },
35
+ event: { type: "string" },
36
+ out: { type: "string" },
37
+ "no-redact": { type: "boolean", default: false },
38
+ all: { type: "boolean", default: false },
39
+ file: { type: "string", multiple: true, default: [] },
40
+ ci: { type: "boolean", default: false },
41
+ gate: { type: "string" },
42
+ override: { type: "string" },
43
+ overlap: { type: "boolean", default: false },
44
+ round: { type: "string" },
45
+ package: { type: "string" },
46
+ replan: { type: "string", multiple: true, default: [] },
47
+ rebase: { type: "string", multiple: true, default: [] },
48
+ base: { type: "string" },
49
+ blockers: { type: "string" },
50
+ },
51
+ });
52
+ } catch (error) {
53
+ throw new UsageError(error instanceof Error ? error.message : String(error));
54
+ }
55
+ const { values, positionals } = parsed;
56
+ const optional = <K extends string>(key: K, value: string | undefined): Partial<Record<K, string>> =>
57
+ value === undefined ? {} : ({ [key]: value } as Record<K, string>);
58
+ const options: Options = {
59
+ yes: values.yes,
60
+ agent: values.agent,
61
+ json: values.json,
62
+ set: values.set,
63
+ redact: !values["no-redact"],
64
+ all: values.all,
65
+ file: values.file,
66
+ ci: values.ci,
67
+ overlap: values.overlap,
68
+ replan: values.replan,
69
+ rebase: values.rebase,
70
+ ...optional("name", values.name),
71
+ ...optional("title", values.title),
72
+ ...optional("feature", values.feature),
73
+ ...optional("roadmap", values.roadmap),
74
+ ...optional("change", values.change),
75
+ ...optional("commit", values.commit),
76
+ ...optional("summary", values.summary),
77
+ ...optional("goals", values.goals),
78
+ ...optional("reason", values.reason),
79
+ ...optional("item", values.item),
80
+ ...optional("event", values.event),
81
+ ...optional("out", values.out),
82
+ ...optional("gate", values.gate),
83
+ ...optional("override", values.override),
84
+ ...optional("round", values.round),
85
+ ...optional("package", values.package),
86
+ ...optional("base", values.base),
87
+ ...optional("blockers", values.blockers),
88
+ };
89
+ return { words: positionals, options, help: values.help, version: values.version };
90
+ }
@@ -0,0 +1,50 @@
1
+ import { CruzeError } from "../../../domain/cruze_error.ts";
2
+ import { type CliContext, UsageError } from "./cli_context.ts";
3
+ import { COMMANDS, USAGE } from "./commands.ts";
4
+ import { parseCommand } from "./parse_command.ts";
5
+
6
+ export interface Terminal {
7
+ stdout(text: string): void;
8
+ stderr(text: string): void;
9
+ /** True when stdout is a terminal, so a person rather than an agent is reading it. */
10
+ stdoutIsTerminal: boolean;
11
+ }
12
+
13
+ /**
14
+ * Runs one CLI invocation and returns the exit code: 0 on success, 1 when a check fails or
15
+ * an expected error occurs, 2 for usage errors. Agents get JSON on stdout; people get a summary on stderr.
16
+ */
17
+ export async function runCli(argv: string[], context: CliContext, terminal: Terminal): Promise<number> {
18
+ let json = !terminal.stdoutIsTerminal || argv.includes("--json");
19
+ try {
20
+ const parsed = parseCommand(argv);
21
+ json ||= parsed.options.json;
22
+ if (parsed.version) {
23
+ terminal.stdout(context.bundle.version);
24
+ return 0;
25
+ }
26
+ const [name, ...args] = parsed.words;
27
+ if (parsed.help || name === undefined || name === "help") {
28
+ terminal.stderr(USAGE);
29
+ return 0;
30
+ }
31
+ const command = COMMANDS[name];
32
+ if (command === undefined) throw new UsageError(`Unknown command: ${name}`);
33
+ const result = await command.handler(context, args, parsed.options);
34
+ terminal.stderr(result.human);
35
+ if (json) terminal.stdout(JSON.stringify(result.json, null, 2));
36
+ return result.failed === true ? 1 : 0;
37
+ } catch (error) {
38
+ if (error instanceof UsageError) {
39
+ terminal.stderr(`${error.message}\n\n${USAGE}`);
40
+ if (json) terminal.stdout(JSON.stringify({ error: { code: "usage", message: error.message } }));
41
+ return 2;
42
+ }
43
+ if (error instanceof CruzeError) {
44
+ terminal.stderr(`cruze: ${error.message}`);
45
+ if (json) terminal.stdout(JSON.stringify({ error: { code: error.code, message: error.message } }));
46
+ return 1;
47
+ }
48
+ throw error;
49
+ }
50
+ }