@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.
- package/LICENSE +201 -0
- package/README.md +167 -0
- package/bin/cruze.mjs +17 -0
- package/dist/adapters/inbound/cli/cli_context.js +75 -0
- package/dist/adapters/inbound/cli/commands/code.js +30 -0
- package/dist/adapters/inbound/cli/commands/documents.js +52 -0
- package/dist/adapters/inbound/cli/commands/init.js +23 -0
- package/dist/adapters/inbound/cli/commands/install.js +15 -0
- package/dist/adapters/inbound/cli/commands/journal.js +31 -0
- package/dist/adapters/inbound/cli/commands/progress.js +51 -0
- package/dist/adapters/inbound/cli/commands/review.js +18 -0
- package/dist/adapters/inbound/cli/commands/status.js +45 -0
- package/dist/adapters/inbound/cli/commands.js +36 -0
- package/dist/adapters/inbound/cli/parse_command.js +90 -0
- package/dist/adapters/inbound/cli/run_cli.js +50 -0
- package/dist/adapters/outbound/git_repository.js +57 -0
- package/dist/adapters/outbound/node_project_files.js +101 -0
- package/dist/adapters/outbound/package_bundle.js +56 -0
- package/dist/adapters/outbound/process_agent_runner.js +26 -0
- package/dist/adapters/outbound/system_clock.js +14 -0
- package/dist/adapters/outbound/terminal_prompter.js +22 -0
- package/dist/app/ports/agent_runner.js +11 -0
- package/dist/app/ports/bundle.js +22 -0
- package/dist/app/ports/clock.js +6 -0
- package/dist/app/ports/project_files.js +20 -0
- package/dist/app/ports/prompter.js +5 -0
- package/dist/app/ports/repository.js +13 -0
- package/dist/app/project_context.js +45 -0
- package/dist/app/use_cases/abandon_work.js +42 -0
- package/dist/app/use_cases/approve_artifact.js +146 -0
- package/dist/app/use_cases/check_code.js +90 -0
- package/dist/app/use_cases/init_project.js +59 -0
- package/dist/app/use_cases/install_skills.js +93 -0
- package/dist/app/use_cases/journal_events.js +48 -0
- package/dist/app/use_cases/land_change.js +113 -0
- package/dist/app/use_cases/new_project_doc.js +40 -0
- package/dist/app/use_cases/new_work_item.js +75 -0
- package/dist/app/use_cases/project_status.js +73 -0
- package/dist/app/use_cases/record_progress.js +101 -0
- package/dist/app/use_cases/run_review.js +71 -0
- package/dist/app/use_cases/validate_project.js +22 -0
- package/dist/domain/agent_skill_homes.js +25 -0
- package/dist/domain/approvals/evaluate.js +36 -0
- package/dist/domain/approvals/fingerprint.js +52 -0
- package/dist/domain/approvals/records.js +50 -0
- package/dist/domain/architecture_sections.js +23 -0
- package/dist/domain/check/check_source.js +64 -0
- package/dist/domain/check/imports.js +81 -0
- package/dist/domain/check/rust_imports.js +84 -0
- package/dist/domain/check/type_count.js +17 -0
- package/dist/domain/config.js +128 -0
- package/dist/domain/cruze_error.js +10 -0
- package/dist/domain/edits/agents_file.js +44 -0
- package/dist/domain/edits/managed_tables.js +60 -0
- package/dist/domain/edits/progress_block.js +33 -0
- package/dist/domain/edits/status_lines.js +52 -0
- package/dist/domain/elements.js +126 -0
- package/dist/domain/glob.js +46 -0
- package/dist/domain/hashing.js +52 -0
- package/dist/domain/ids.js +37 -0
- package/dist/domain/journal.js +62 -0
- package/dist/domain/land/bookkeeping.js +54 -0
- package/dist/domain/land/merge_plan.js +132 -0
- package/dist/domain/land/text_ops.js +66 -0
- package/dist/domain/markdown.js +79 -0
- package/dist/domain/markdown_tables.js +34 -0
- package/dist/domain/project/artifact_ref.js +42 -0
- package/dist/domain/project/deltas.js +49 -0
- package/dist/domain/project/layout.js +40 -0
- package/dist/domain/project/living_model.js +54 -0
- package/dist/domain/project/new_items.js +48 -0
- package/dist/domain/project/plan_parts.js +111 -0
- package/dist/domain/project/progress.js +43 -0
- package/dist/domain/project/project_view.js +72 -0
- package/dist/domain/project/roadmap.js +43 -0
- package/dist/domain/project/work_items.js +68 -0
- package/dist/domain/project_name.js +20 -0
- package/dist/domain/scaffold.js +29 -0
- package/dist/domain/skill.js +106 -0
- package/dist/domain/skill_links.js +103 -0
- package/dist/domain/status/next_step.js +119 -0
- package/dist/domain/status/verification.js +16 -0
- package/dist/domain/status/work_status.js +101 -0
- package/dist/domain/trace.js +44 -0
- package/dist/domain/validation/delta_rules.js +79 -0
- package/dist/domain/validation/element_rules.js +99 -0
- package/dist/domain/validation/living_rules.js +97 -0
- package/dist/domain/validation/plan_rules.js +105 -0
- package/dist/domain/validation/problem.js +21 -0
- package/dist/domain/validation/project_rules.js +99 -0
- package/dist/domain/validation/validate_project.js +31 -0
- package/dist/domain/validation/work_item_rules.js +144 -0
- package/dist/main.js +32 -0
- package/package.json +40 -0
- package/scripts/build.mjs +37 -0
- package/skills/about/SKILL.md +21 -0
- package/skills/architect/SKILL.md +43 -0
- package/skills/architect/feature.md +29 -0
- package/skills/architect/project.md +58 -0
- package/skills/architect/tweak.md +22 -0
- package/skills/architect/walkthrough.md +29 -0
- package/skills/behavioural-testing/SKILL.md +63 -0
- package/skills/behavioural-testing/examples.md +102 -0
- package/skills/behavioural-testing/fakes.md +30 -0
- package/skills/build/SKILL.md +62 -0
- package/skills/dependency-approval/SKILL.md +48 -0
- package/skills/domain-language/SKILL.md +37 -0
- package/skills/envision/SKILL.md +29 -0
- package/skills/envision/feature.md +22 -0
- package/skills/envision/project.md +20 -0
- package/skills/explore/SKILL.md +39 -0
- package/skills/formats/SKILL.md +84 -0
- package/skills/formats/reference/architecture.md +80 -0
- package/skills/formats/reference/config.md +57 -0
- package/skills/formats/reference/durable-docs.md +71 -0
- package/skills/formats/reference/specs.md +65 -0
- package/skills/formats/reference/work-items.md +100 -0
- package/skills/formats/templates/adr.md +7 -0
- package/skills/formats/templates/architecture.md +100 -0
- package/skills/formats/templates/change.md +29 -0
- package/skills/formats/templates/feature.md +49 -0
- package/skills/formats/templates/glossary.md +8 -0
- package/skills/formats/templates/roadmap.md +32 -0
- package/skills/formats/templates/spec.md +11 -0
- package/skills/formats/templates/standalone-change.md +46 -0
- package/skills/formats/templates/vision.md +37 -0
- package/skills/grilling/SKILL.md +66 -0
- package/skills/hexagonal-design/SKILL.md +119 -0
- package/skills/hexagonal-design/contracts.md +68 -0
- package/skills/hexagonal-design/fundamentals.md +50 -0
- package/skills/hexagonal-design/languages/cpp.md +37 -0
- package/skills/hexagonal-design/languages/rust.md +40 -0
- package/skills/hexagonal-design/runtime.md +48 -0
- package/skills/land/SKILL.md +43 -0
- package/skills/next/SKILL.md +30 -0
- package/skills/plan/SKILL.md +52 -0
- package/skills/plan/tasks.md +26 -0
- package/skills/plan/test-plan.md +14 -0
- package/skills/research/SKILL.md +41 -0
- package/skills/research/evaluation.md +27 -0
- package/skills/rethink/SKILL.md +53 -0
- package/skills/rethink/levels.md +45 -0
- package/skills/roadmap/SKILL.md +51 -0
- package/skills/roles/SKILL.md +57 -0
- package/skills/roles/code-reviewer.md +55 -0
- package/skills/roles/design-reviewer.md +63 -0
- package/skills/roles/researcher.md +33 -0
- package/skills/roles/verifier.md +38 -0
- package/skills/triage/SKILL.md +35 -0
- package/skills/verify/SKILL.md +39 -0
- package/skills/verify/manual-test.md +23 -0
- package/src/adapters/inbound/cli/cli_context.ts +75 -0
- package/src/adapters/inbound/cli/commands/code.ts +30 -0
- package/src/adapters/inbound/cli/commands/documents.ts +52 -0
- package/src/adapters/inbound/cli/commands/init.ts +23 -0
- package/src/adapters/inbound/cli/commands/install.ts +15 -0
- package/src/adapters/inbound/cli/commands/journal.ts +31 -0
- package/src/adapters/inbound/cli/commands/progress.ts +51 -0
- package/src/adapters/inbound/cli/commands/review.ts +18 -0
- package/src/adapters/inbound/cli/commands/status.ts +45 -0
- package/src/adapters/inbound/cli/commands.ts +36 -0
- package/src/adapters/inbound/cli/parse_command.ts +90 -0
- package/src/adapters/inbound/cli/run_cli.ts +50 -0
- package/src/adapters/outbound/git_repository.ts +57 -0
- package/src/adapters/outbound/node_project_files.ts +101 -0
- package/src/adapters/outbound/package_bundle.ts +56 -0
- package/src/adapters/outbound/process_agent_runner.ts +26 -0
- package/src/adapters/outbound/system_clock.ts +14 -0
- package/src/adapters/outbound/terminal_prompter.ts +22 -0
- package/src/app/ports/agent_runner.ts +11 -0
- package/src/app/ports/bundle.ts +22 -0
- package/src/app/ports/clock.ts +6 -0
- package/src/app/ports/project_files.ts +20 -0
- package/src/app/ports/prompter.ts +5 -0
- package/src/app/ports/repository.ts +13 -0
- package/src/app/project_context.ts +45 -0
- package/src/app/use_cases/abandon_work.ts +42 -0
- package/src/app/use_cases/approve_artifact.ts +146 -0
- package/src/app/use_cases/check_code.ts +90 -0
- package/src/app/use_cases/init_project.ts +59 -0
- package/src/app/use_cases/install_skills.ts +93 -0
- package/src/app/use_cases/journal_events.ts +48 -0
- package/src/app/use_cases/land_change.ts +113 -0
- package/src/app/use_cases/new_project_doc.ts +40 -0
- package/src/app/use_cases/new_work_item.ts +75 -0
- package/src/app/use_cases/project_status.ts +73 -0
- package/src/app/use_cases/record_progress.ts +101 -0
- package/src/app/use_cases/run_review.ts +71 -0
- package/src/app/use_cases/validate_project.ts +22 -0
- package/src/domain/agent_skill_homes.ts +25 -0
- package/src/domain/approvals/evaluate.ts +36 -0
- package/src/domain/approvals/fingerprint.ts +52 -0
- package/src/domain/approvals/records.ts +50 -0
- package/src/domain/architecture_sections.ts +23 -0
- package/src/domain/check/check_source.ts +64 -0
- package/src/domain/check/imports.ts +81 -0
- package/src/domain/check/rust_imports.ts +84 -0
- package/src/domain/check/type_count.ts +17 -0
- package/src/domain/config.ts +128 -0
- package/src/domain/cruze_error.ts +10 -0
- package/src/domain/edits/agents_file.ts +44 -0
- package/src/domain/edits/managed_tables.ts +60 -0
- package/src/domain/edits/progress_block.ts +33 -0
- package/src/domain/edits/status_lines.ts +52 -0
- package/src/domain/elements.ts +126 -0
- package/src/domain/glob.ts +46 -0
- package/src/domain/hashing.ts +52 -0
- package/src/domain/ids.ts +37 -0
- package/src/domain/journal.ts +62 -0
- package/src/domain/land/bookkeeping.ts +54 -0
- package/src/domain/land/merge_plan.ts +132 -0
- package/src/domain/land/text_ops.ts +66 -0
- package/src/domain/markdown.ts +79 -0
- package/src/domain/markdown_tables.ts +34 -0
- package/src/domain/project/artifact_ref.ts +42 -0
- package/src/domain/project/deltas.ts +49 -0
- package/src/domain/project/layout.ts +40 -0
- package/src/domain/project/living_model.ts +54 -0
- package/src/domain/project/new_items.ts +48 -0
- package/src/domain/project/plan_parts.ts +111 -0
- package/src/domain/project/progress.ts +43 -0
- package/src/domain/project/project_view.ts +72 -0
- package/src/domain/project/roadmap.ts +43 -0
- package/src/domain/project/work_items.ts +68 -0
- package/src/domain/project_name.ts +20 -0
- package/src/domain/scaffold.ts +29 -0
- package/src/domain/skill.ts +106 -0
- package/src/domain/skill_links.ts +103 -0
- package/src/domain/status/next_step.ts +119 -0
- package/src/domain/status/verification.ts +16 -0
- package/src/domain/status/work_status.ts +101 -0
- package/src/domain/trace.ts +44 -0
- package/src/domain/validation/delta_rules.ts +79 -0
- package/src/domain/validation/element_rules.ts +99 -0
- package/src/domain/validation/living_rules.ts +97 -0
- package/src/domain/validation/plan_rules.ts +105 -0
- package/src/domain/validation/problem.ts +21 -0
- package/src/domain/validation/project_rules.ts +99 -0
- package/src/domain/validation/validate_project.ts +31 -0
- package/src/domain/validation/work_item_rules.ts +144 -0
- package/src/main.ts +32 -0
- package/templates/init/AGENTS.md +28 -0
- package/templates/init/CHANGELOG.md +8 -0
- package/templates/init/CLAUDE.md +1 -0
- package/templates/init/README.md +1 -0
- package/templates/init/ci.yml +23 -0
- package/templates/init/config.yaml +33 -0
- package/templates/init/gitattributes +2 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { addFutureFeature, completeTask, dropFutureFeature, pruneRoadmap, recordDeviation, reopenTask } from "../../../../app/use_cases/record_progress.js";
|
|
2
|
+
import { abandonWork } from "../../../../app/use_cases/abandon_work.js";
|
|
3
|
+
import { UsageError, requireArgs, requireOption, } from "../cli_context.js";
|
|
4
|
+
|
|
5
|
+
export async function runTask(context , args , options ) {
|
|
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 , args , options ) {
|
|
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 , args ) {
|
|
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 , args , options ) {
|
|
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.js";
|
|
2
|
+
import { UsageError, requireArgs, } from "../cli_context.js";
|
|
3
|
+
|
|
4
|
+
export async function runReviewCommand(context , args , options ) {
|
|
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.js";
|
|
2
|
+
|
|
3
|
+
import { UsageError, } from "../cli_context.js";
|
|
4
|
+
|
|
5
|
+
export async function runStatus(context , _args , options ) {
|
|
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 , status ) {
|
|
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 ) {
|
|
41
|
+
const report = await suggestNext(context);
|
|
42
|
+
const describe = (s ) => `${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
|
+
|
|
2
|
+
import { runCheck, runLand, runTrace } from "./commands/code.js";
|
|
3
|
+
import { runApprove, runNew, runValidate } from "./commands/documents.js";
|
|
4
|
+
import { runInit } from "./commands/init.js";
|
|
5
|
+
import { runInstall } from "./commands/install.js";
|
|
6
|
+
import { runFeedback, runJournal } from "./commands/journal.js";
|
|
7
|
+
import { runAbandon, runFeatures, runRoadmap, runTask } from "./commands/progress.js";
|
|
8
|
+
import { runReviewCommand } from "./commands/review.js";
|
|
9
|
+
import { runNext, runStatus } from "./commands/status.js";
|
|
10
|
+
|
|
11
|
+
export const COMMANDS = {
|
|
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, } from "./cli_context.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function parseCommand(argv ) {
|
|
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 = (key , value ) =>
|
|
57
|
+
value === undefined ? {} : ({ [key]: value } );
|
|
58
|
+
const 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.js";
|
|
2
|
+
import { UsageError } from "./cli_context.js";
|
|
3
|
+
import { COMMANDS, USAGE } from "./commands.js";
|
|
4
|
+
import { parseCommand } from "./parse_command.js";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
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 , context , terminal ) {
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/** The git repository at a directory, driven through the git command. */
|
|
8
|
+
export class GitRepository {
|
|
9
|
+
root ;
|
|
10
|
+
|
|
11
|
+
constructor(root ) {
|
|
12
|
+
this.root = root;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async currentBranch() {
|
|
16
|
+
const branch = await this.git("rev-parse", "--abbrev-ref", "HEAD");
|
|
17
|
+
return branch === null || branch === "HEAD" ? null : branch;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async headCommit() {
|
|
21
|
+
return this.git("rev-parse", "--short", "HEAD");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async userName() {
|
|
25
|
+
const name = await this.git("config", "user.name");
|
|
26
|
+
const email = await this.git("config", "user.email");
|
|
27
|
+
if (name === null) return email;
|
|
28
|
+
return email === null ? name : `${name} <${email}>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async otherBranches() {
|
|
32
|
+
const current = await this.currentBranch();
|
|
33
|
+
const listing = await this.git("for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes");
|
|
34
|
+
return (listing ?? "")
|
|
35
|
+
.split("\n")
|
|
36
|
+
.filter((ref) => ref !== "" && ref !== current && !ref.endsWith("/HEAD") && ref !== `origin/${current}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async filesOnBranch(branch , dir ) {
|
|
40
|
+
const files = new Map ();
|
|
41
|
+
const listing = await this.git("ls-tree", "-r", "--name-only", branch, "--", dir);
|
|
42
|
+
for (const path of (listing ?? "").split("\n").filter((p) => p !== "")) {
|
|
43
|
+
const text = await this.git("show", `${branch}:${path}`);
|
|
44
|
+
if (text !== null) files.set(path, text);
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async git(...args ) {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await run("git", args, { cwd: this.root, maxBuffer: 64 * 1024 * 1024 });
|
|
52
|
+
return stdout.trimEnd();
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { appendFile, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/** Project files on the local disk, confined to one root directory. */
|
|
6
|
+
export class NodeProjectFiles {
|
|
7
|
+
root ;
|
|
8
|
+
|
|
9
|
+
constructor(root ) {
|
|
10
|
+
this.root = resolve(root);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async exists(path ) {
|
|
14
|
+
try {
|
|
15
|
+
await lstat(this.resolve(path));
|
|
16
|
+
return true;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (isNotFound(error)) return false;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async readText(path ) {
|
|
24
|
+
try {
|
|
25
|
+
return await readFile(this.resolve(path), "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (isNotFound(error)) return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async writeText(path , text ) {
|
|
33
|
+
const target = this.resolve(path);
|
|
34
|
+
await mkdir(dirname(target), { recursive: true });
|
|
35
|
+
await writeFile(target, text, "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async appendText(path , text ) {
|
|
39
|
+
const target = this.resolve(path);
|
|
40
|
+
await mkdir(dirname(target), { recursive: true });
|
|
41
|
+
await appendFile(target, text, "utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async listFiles(path ) {
|
|
45
|
+
try {
|
|
46
|
+
const entries = await readdir(this.resolve(path), { withFileTypes: true, recursive: true });
|
|
47
|
+
return entries
|
|
48
|
+
.filter((entry) => entry.isFile())
|
|
49
|
+
.map((entry) => relative(this.root, join(entry.parentPath, entry.name)).split(sep).join("/"))
|
|
50
|
+
.sort();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isNotFound(error)) return [];
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async move(from , to ) {
|
|
58
|
+
const target = this.resolve(to);
|
|
59
|
+
await mkdir(dirname(target), { recursive: true });
|
|
60
|
+
await rename(this.resolve(from), target);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async listEntries(path ) {
|
|
64
|
+
try {
|
|
65
|
+
return (await readdir(this.resolve(path))).sort();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (isNotFound(error)) return [];
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async remove(path ) {
|
|
73
|
+
await rm(this.resolve(path), { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async link(path , target ) {
|
|
77
|
+
const linkPath = this.resolve(path);
|
|
78
|
+
this.assertInsideRoot(resolve(dirname(linkPath), target));
|
|
79
|
+
await mkdir(dirname(linkPath), { recursive: true });
|
|
80
|
+
await rm(linkPath, { recursive: true, force: true });
|
|
81
|
+
await symlink(target, linkPath, "dir");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolves a project-relative path, refusing anything outside the root. */
|
|
85
|
+
resolve(path ) {
|
|
86
|
+
const absolute = resolve(this.root, path);
|
|
87
|
+
this.assertInsideRoot(absolute);
|
|
88
|
+
return absolute;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assertInsideRoot(absolute ) {
|
|
92
|
+
const fromRoot = relative(this.root, absolute);
|
|
93
|
+
if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
|
|
94
|
+
throw new Error(`Path escapes the project root: ${absolute}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isNotFound(error ) {
|
|
100
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** The skills and templates shipped in the installed Cruze package. */
|
|
8
|
+
export class PackageBundle {
|
|
9
|
+
version ;
|
|
10
|
+
root ;
|
|
11
|
+
|
|
12
|
+
constructor(root , version ) {
|
|
13
|
+
this.root = root;
|
|
14
|
+
this.version = version;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Finds the package root by walking up from this module to the nearest package.json. */
|
|
18
|
+
static async locate(fromUrl = import.meta.url) {
|
|
19
|
+
let dir = dirname(fileURLToPath(fromUrl));
|
|
20
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) throw new Error("Cannot find the Cruze package root");
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
const manifest = JSON.parse(await readFile(join(dir, "package.json"), "utf8")) ;
|
|
26
|
+
return new PackageBundle(dir, manifest.version);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async skills() {
|
|
30
|
+
const skillsDir = join(this.root, "skills");
|
|
31
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
32
|
+
const folders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
33
|
+
return Promise.all(
|
|
34
|
+
folders.map(async (folder) => ({ folder, files: await readTree(join(skillsDir, folder)) })),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async template(path ) {
|
|
39
|
+
return readFile(join(this.root, "templates", path), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async formatTemplate(name ) {
|
|
43
|
+
return readFile(join(this.root, "skills", "formats", "templates", name), "utf8");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readTree(dir ) {
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
49
|
+
const files = entries.filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name));
|
|
50
|
+
return Promise.all(
|
|
51
|
+
files.sort().map(async (file) => ({
|
|
52
|
+
path: relative(dir, file).split(sep).join("/"),
|
|
53
|
+
text: await readFile(file, "utf8"),
|
|
54
|
+
})),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/** Runs an agent's non-interactive mode, such as `claude -p`, as a child process. */
|
|
5
|
+
export class ProcessAgentRunner {
|
|
6
|
+
cwd ;
|
|
7
|
+
|
|
8
|
+
constructor(cwd ) {
|
|
9
|
+
this.cwd = cwd;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
run(command , prompt ) {
|
|
13
|
+
const [program, ...args] = command;
|
|
14
|
+
if (program === undefined) return Promise.resolve({ exitCode: 127, output: "", error: "no command configured" });
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = spawn(program, args, { cwd: this.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
17
|
+
let output = "";
|
|
18
|
+
let error = "";
|
|
19
|
+
child.stdout.on("data", (chunk ) => (output += chunk.toString("utf8")));
|
|
20
|
+
child.stderr.on("data", (chunk ) => (error += chunk.toString("utf8")));
|
|
21
|
+
child.on("error", (failure) => resolve({ exitCode: 127, output, error: `${error}${failure.message}` }));
|
|
22
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, output, error }));
|
|
23
|
+
child.stdin.end(prompt);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/** The machine's clock, with dates in its local time zone. */
|
|
4
|
+
export class SystemClock {
|
|
5
|
+
now() {
|
|
6
|
+
return new Date();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
today() {
|
|
10
|
+
const date = new Date();
|
|
11
|
+
const pad = (n ) => String(n).padStart(2, "0");
|
|
12
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/** Asks on the terminal. Questions go to stderr so stdout stays machine-readable. */
|
|
5
|
+
export class TerminalPrompter {
|
|
6
|
+
async ask(question , fallback ) {
|
|
7
|
+
const terminal = createInterface({ input: process.stdin, output: process.stderr });
|
|
8
|
+
try {
|
|
9
|
+
const answer = await terminal.question(`${question} (${fallback}): `);
|
|
10
|
+
return answer.trim() === "" ? fallback : answer.trim();
|
|
11
|
+
} finally {
|
|
12
|
+
terminal.close();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Used when nobody can answer: without a terminal, or with --yes. */
|
|
18
|
+
export class DefaultAnswerPrompter {
|
|
19
|
+
async ask(_question , fallback ) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|