karajan-code 2.7.4 → 2.8.0
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/README.md +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +85 -31
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas → flat task string adapter.
|
|
3
|
+
*
|
|
4
|
+
* Renders a validated Canvas object as a single markdown blob the
|
|
5
|
+
* existing planner can consume verbatim. Preserves every section
|
|
6
|
+
* with its original heading so downstream steps (planner prompt,
|
|
7
|
+
* coder prompt, plan JSON `task` field) see the same shape.
|
|
8
|
+
*
|
|
9
|
+
* The point: Canvas users get planner-time validation (catching
|
|
10
|
+
* broken acceptance tests before they're persisted) AND benefit from
|
|
11
|
+
* every existing planner / coder pipeline without those pipelines
|
|
12
|
+
* needing Canvas awareness. Norms + Safeguards explicitly bracketed
|
|
13
|
+
* so prompts that scan for them can find them with a regex.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} canvas — output of validateCanvas (.canvas)
|
|
18
|
+
* @returns {string} markdown task ready for the planner
|
|
19
|
+
*/
|
|
20
|
+
export function canvasToTask(canvas) {
|
|
21
|
+
const lines = [`# ${canvas.title}`, ""];
|
|
22
|
+
|
|
23
|
+
// Requirements
|
|
24
|
+
lines.push("## Requirements", canvas.requirements.problem);
|
|
25
|
+
if (canvas.requirements.done && canvas.requirements.done.length > 0) {
|
|
26
|
+
lines.push("", "Definition of done:");
|
|
27
|
+
for (const d of canvas.requirements.done) lines.push(`- ${d}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push("");
|
|
30
|
+
|
|
31
|
+
// Entities (optional)
|
|
32
|
+
if (canvas.entities && canvas.entities.length > 0) {
|
|
33
|
+
lines.push("## Entities");
|
|
34
|
+
for (const e of canvas.entities) {
|
|
35
|
+
const fields = e.fields && e.fields.length > 0 ? ` — fields: ${e.fields.join(", ")}` : "";
|
|
36
|
+
lines.push(`- ${e.name}${fields}${e.notes ? `. ${e.notes}` : ""}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Approach
|
|
42
|
+
lines.push("## Approach", canvas.approach, "");
|
|
43
|
+
|
|
44
|
+
// Structure (optional content)
|
|
45
|
+
lines.push("## Structure");
|
|
46
|
+
if (canvas.structure?.touches?.length > 0) lines.push(`- touches: ${canvas.structure.touches.join(", ")}`);
|
|
47
|
+
if (canvas.structure?.depends_on?.length > 0) lines.push(`- depends_on: ${canvas.structure.depends_on.join(", ")}`);
|
|
48
|
+
if (canvas.structure?.contracts?.length > 0) lines.push(`- contracts: ${canvas.structure.contracts.join(", ")}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
// Operations — each becomes one HU. Acceptance tests inline so the
|
|
52
|
+
// planner sees the contract from turn 1.
|
|
53
|
+
lines.push("## Operations");
|
|
54
|
+
canvas.operations.forEach((op, i) => {
|
|
55
|
+
lines.push(`${i + 1}. ${op.title}`);
|
|
56
|
+
if (op.file_hint) lines.push(` file: ${op.file_hint}`);
|
|
57
|
+
if (op.blocked_by && op.blocked_by.length > 0) lines.push(` depends on: ${op.blocked_by.join(", ")}`);
|
|
58
|
+
lines.push(" Acceptance:");
|
|
59
|
+
for (const t of op.acceptance) {
|
|
60
|
+
// Render shell as one-liner, gherkin as fenced block so newlines survive.
|
|
61
|
+
if (t.type === "gherkin") {
|
|
62
|
+
lines.push(" - gherkin:", " ```gherkin", ...t.content.split("\n").map((l) => ` ${l}`), " ```");
|
|
63
|
+
} else {
|
|
64
|
+
lines.push(` - shell: \`${t.content}\``);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
lines.push("");
|
|
69
|
+
|
|
70
|
+
// Norms (optional) — bracketed so prompts can find them.
|
|
71
|
+
if (canvas.norms && canvas.norms.length > 0) {
|
|
72
|
+
lines.push("## Norms (per-task engineering standards — coder MUST follow)");
|
|
73
|
+
for (const n of canvas.norms) lines.push(`- ${n}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Safeguards (optional) — bracketed similarly.
|
|
78
|
+
if (canvas.safeguards && canvas.safeguards.length > 0) {
|
|
79
|
+
lines.push("## Safeguards (non-negotiable invariants — code MUST hold these)");
|
|
80
|
+
for (const s of canvas.safeguards) lines.push(`- ${s}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REASONS Canvas validator.
|
|
3
|
+
*
|
|
4
|
+
* Schema validation + planner-time sanity checks for acceptance tests.
|
|
5
|
+
* The schema is enforced via Valibot in canvas/schema.js. This module
|
|
6
|
+
* adds the bug-class fix: parse-check every shell acceptance test
|
|
7
|
+
* BEFORE it gets persisted into a plan and BEFORE any LLM is called.
|
|
8
|
+
*
|
|
9
|
+
* Catches the 2026-04-29 incident class:
|
|
10
|
+
* `length as $n | .filesOver300LOC == $n` → exits non-zero with a
|
|
11
|
+
* descriptive error pointing at the offending operation + test
|
|
12
|
+
* index, BEFORE the user has paid for any agent run.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { safeParseCanvas } from "./schema.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a single shell acceptance test through `bash -n` (parse-only,
|
|
20
|
+
* doesn't execute) to catch shell-level syntax errors. Then, if the
|
|
21
|
+
* test invokes `jq`, also probe the jq query through `jq -n <query>`
|
|
22
|
+
* with empty input — jq errors there iff the query has bad syntax.
|
|
23
|
+
*
|
|
24
|
+
* Trade-off accepted: bash -n won't catch every command-substitution
|
|
25
|
+
* pitfall (it parses but doesn't expand), and jq -n with empty input
|
|
26
|
+
* may pass queries that fail at runtime against real input. The point
|
|
27
|
+
* is to catch the OBVIOUS bugs cheaply, not to be airtight.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} cmd
|
|
30
|
+
* @returns {{ ok: true } | { ok: false, kind: string, error: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function validateShellTest(cmd) {
|
|
33
|
+
if (typeof cmd !== "string" || cmd.trim().length === 0) {
|
|
34
|
+
return { ok: false, kind: "empty", error: "shell test is empty" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Layer 1: bash -n parse-check on the whole command.
|
|
38
|
+
const bash = spawnSync("bash", ["-n", "-c", cmd], { encoding: "utf8", timeout: 5000 });
|
|
39
|
+
if (bash.status !== 0) {
|
|
40
|
+
const err = (bash.stderr || "").trim() || "bash -n rejected the command";
|
|
41
|
+
return { ok: false, kind: "shell-syntax", error: err };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Layer 2: every `jq '...'` invocation gets its query parse-checked.
|
|
45
|
+
// Permissive regex: catches `jq -e 'X'`, `jq "X"`, `| jq 'X'`. False
|
|
46
|
+
// negatives are fine — better to skip a real jq than misinterpret a
|
|
47
|
+
// shell-quoting edge case.
|
|
48
|
+
//
|
|
49
|
+
// We distinguish COMPILE errors (the bug we want to catch — invalid
|
|
50
|
+
// syntax, e.g. `length as $n | .field` over an array) from RUNTIME
|
|
51
|
+
// errors (the query is well-formed but fails on null input from
|
|
52
|
+
// `-n`, like `.foo[]` on null). Only compile errors fail validation;
|
|
53
|
+
// runtime errors against synthetic input are expected because real
|
|
54
|
+
// input isn't available at plan-time.
|
|
55
|
+
const jqRe = /\bjq\b\s+(?:-[a-zA-Z]+\s+)*(?:'([^']+)'|"([^"]+)")/g;
|
|
56
|
+
for (const m of cmd.matchAll(jqRe)) {
|
|
57
|
+
const query = m[1] || m[2];
|
|
58
|
+
if (!query) continue;
|
|
59
|
+
|
|
60
|
+
// Static pattern detector for the 2026-04-29 as-binding misuse:
|
|
61
|
+
// `[expr] | length as $X | .field` and similar. The `as $X`
|
|
62
|
+
// binds the value to a name but does NOT change the value
|
|
63
|
+
// flowing through the pipe. The next `.field` is therefore
|
|
64
|
+
// applied to the array tail (or whatever the previous stage
|
|
65
|
+
// returned), not to the original root. Always wrong.
|
|
66
|
+
//
|
|
67
|
+
// Detection: a pipeline whose tail is `length as $X | .X` (or
|
|
68
|
+
// any `<terminal-operator> as $X | .field`) is the smell.
|
|
69
|
+
const asBindingMisuse = /\b(?:length|keys|values|min|max|add|first|last)\s+as\s+\$[A-Za-z_][\w]*\s*\|\s*\.[A-Za-z_]/;
|
|
70
|
+
if (asBindingMisuse.test(query)) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
kind: "jq-syntax",
|
|
74
|
+
error: "Likely as-binding misuse: `length as $n | .field` does not change the value flowing through; `.field` is applied to the previous stage's output, not the root. Use `. as $r | <transform> | $r.field` instead.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const jq = spawnSync("jq", ["-n", query], { encoding: "utf8", timeout: 5000 });
|
|
79
|
+
if (jq.status !== 0) {
|
|
80
|
+
const err = (jq.stderr || "").trim() || "jq rejected the query";
|
|
81
|
+
// jq distinguishes compile errors with these tokens. Anything
|
|
82
|
+
// else (e.g. "Cannot index null with string") is a runtime error
|
|
83
|
+
// against the empty input, NOT a syntax bug.
|
|
84
|
+
const isCompileError = /\b(compile error|syntax error|unexpected|jq: error \(at <stdin>:\d+\): ?$)/i.test(err);
|
|
85
|
+
if (isCompileError) {
|
|
86
|
+
return { ok: false, kind: "jq-syntax", error: err };
|
|
87
|
+
}
|
|
88
|
+
// Special case: `length as $n | .field` returns "Cannot index
|
|
89
|
+
// array/number/string with string" on -n input because the as-
|
|
90
|
+
// binding misuse means `.field` is applied to the wrong thing.
|
|
91
|
+
// This is exactly the 2026-04-29 bug class. Treat it as syntax-
|
|
92
|
+
// adjacent: the query is "compilable" but semantically broken
|
|
93
|
+
// by misuse of as-binding.
|
|
94
|
+
if (/Cannot index (array|number|string) with string/i.test(err)) {
|
|
95
|
+
return { ok: false, kind: "jq-syntax", error: err };
|
|
96
|
+
}
|
|
97
|
+
// Otherwise let it pass — runtime error against synthetic input
|
|
98
|
+
// is fine. jq parsed the query successfully.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate every operation's acceptance tests in a parsed Canvas.
|
|
107
|
+
* Returns an array of issues; empty array means everything passed.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} canvas
|
|
110
|
+
* @returns {Array<{ operationIndex: number, operationTitle: string, testIndex: number, type: string, content: string, kind: string, error: string }>}
|
|
111
|
+
*/
|
|
112
|
+
export function validateAcceptanceTests(canvas) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
if (!canvas || !Array.isArray(canvas.operations)) return issues;
|
|
115
|
+
canvas.operations.forEach((op, opIdx) => {
|
|
116
|
+
(op.acceptance || []).forEach((t, tIdx) => {
|
|
117
|
+
if (t.type === "shell") {
|
|
118
|
+
const r = validateShellTest(t.content);
|
|
119
|
+
if (!r.ok) {
|
|
120
|
+
issues.push({
|
|
121
|
+
operationIndex: opIdx,
|
|
122
|
+
operationTitle: op.title,
|
|
123
|
+
testIndex: tIdx,
|
|
124
|
+
type: t.type,
|
|
125
|
+
content: t.content,
|
|
126
|
+
kind: r.kind,
|
|
127
|
+
error: r.error,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else if (t.type === "gherkin") {
|
|
131
|
+
// Minimal gherkin sanity: must contain at least one Given AND
|
|
132
|
+
// one Then. When/And/But are optional. This catches "blank
|
|
133
|
+
// gherkin" without trying to parse the language.
|
|
134
|
+
if (!/\bGiven\b/i.test(t.content) || !/\bThen\b/i.test(t.content)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
operationIndex: opIdx,
|
|
137
|
+
operationTitle: op.title,
|
|
138
|
+
testIndex: tIdx,
|
|
139
|
+
type: t.type,
|
|
140
|
+
content: t.content,
|
|
141
|
+
kind: "gherkin-incomplete",
|
|
142
|
+
error: "Gherkin scenario must contain at least one Given and one Then.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Top-level validator. Runs schema + acceptance-test parse-checks.
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} canvas
|
|
155
|
+
* @returns {{ valid: true, canvas: object } | { valid: false, schemaIssues?: object[], testIssues?: object[] }}
|
|
156
|
+
*/
|
|
157
|
+
export function validateCanvas(canvas) {
|
|
158
|
+
const schema = safeParseCanvas(canvas);
|
|
159
|
+
if (!schema.success) return { valid: false, schemaIssues: schema.issues };
|
|
160
|
+
const testIssues = validateAcceptanceTests(schema.output);
|
|
161
|
+
if (testIssues.length > 0) return { valid: false, testIssues };
|
|
162
|
+
return { valid: true, canvas: schema.output };
|
|
163
|
+
}
|
package/src/checks/binaries.js
CHANGED
|
@@ -88,7 +88,7 @@ export function createSerenaCheck() {
|
|
|
88
88
|
applies: (config) => config.serena?.enabled === true,
|
|
89
89
|
describe: "Install Serena via: uvx --from git+https://github.com/oraios/serena serena",
|
|
90
90
|
async detect() {
|
|
91
|
-
let ok
|
|
91
|
+
let ok;
|
|
92
92
|
try {
|
|
93
93
|
const res = await runCommand("serena", ["--version"]);
|
|
94
94
|
ok = res.exitCode === 0;
|
package/src/checks/ports.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { isPortAvailable, findAvailablePort } from "../utils/port-check.js";
|
|
14
14
|
import { getPortOccupant } from "../utils/port-occupant.js";
|
|
15
|
+
import { discoverRunningSonar } from "../sonar/discovery.js";
|
|
16
|
+
import { isSonarReachable } from "../sonar/manager.js";
|
|
15
17
|
import { STRATEGY } from "./types.js";
|
|
16
18
|
|
|
17
19
|
const DEFAULT_SONAR_PORT = 9000;
|
|
@@ -22,58 +24,110 @@ function extractPortFromHost(host, fallback) {
|
|
|
22
24
|
return m ? Number(m[1]) : fallback;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
/**
|
|
26
|
-
* Whether the occupant looks like the Karajan-managed Sonar container.
|
|
27
|
-
* Docker Desktop's proxy is typically `com.docker.backend` / `com.docker.vmnetd`
|
|
28
|
-
* on macOS and `docker-proxy` on Linux. We treat those as OK.
|
|
29
|
-
*/
|
|
30
|
-
function isKarajanSonarOccupant(occupant) {
|
|
31
|
-
if (!occupant?.command) return false;
|
|
32
|
-
const cmd = occupant.command.toLowerCase();
|
|
33
|
-
return cmd.includes("docker") || cmd.includes("sonar");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
export function createSonarPortCheck() {
|
|
37
28
|
return {
|
|
38
29
|
name: "port:sonar",
|
|
39
30
|
label: "SonarQube port",
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
// at config load time — see emitConfigDeprecations in flow-runner.js).
|
|
31
|
+
// AUTO since v2.7.5 — Karajan now resolves SonarQube discovery on
|
|
32
|
+
// its own. If a Sonar container is running anywhere (any name, any
|
|
33
|
+
// image tag), point Karajan at it. If port 9000 is occupied by
|
|
34
|
+
// someone non-Sonar, rebind to the next free port. Asking the user
|
|
35
|
+
// to "stop the unknown process" was busywork.
|
|
36
|
+
strategy: STRATEGY.AUTO,
|
|
47
37
|
applies: (config) => {
|
|
48
38
|
if (config?.sonarqube?.external === true) {
|
|
49
39
|
return { applies: false, reason: "sonarqube.external is true — port managed outside Karajan" };
|
|
50
40
|
}
|
|
51
41
|
return true;
|
|
52
42
|
},
|
|
43
|
+
describe: "Discover or rebind SonarQube port (no user action needed)",
|
|
53
44
|
async detect({ config }) {
|
|
54
|
-
const
|
|
55
|
-
|
|
45
|
+
const configuredPort = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
|
|
46
|
+
|
|
47
|
+
// Step 1 — is there ANY running SonarQube container we can reuse?
|
|
48
|
+
// Container name doesn't matter; we just need a published host port
|
|
49
|
+
// that responds to /api/system/status. This avoids "port 9000 is
|
|
50
|
+
// taken by an unknown process" errors when the unknown process is
|
|
51
|
+
// actually a perfectly good SonarQube from another project.
|
|
52
|
+
const discovered = await discoverRunningSonar();
|
|
53
|
+
if (discovered.found && discovered.reachable) {
|
|
54
|
+
// If the discovered host matches what's already configured,
|
|
55
|
+
// there's nothing to fix — happy path.
|
|
56
|
+
if (discovered.port === configuredPort) {
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
severity: "info",
|
|
60
|
+
detail: `Reusing running SonarQube container "${discovered.containerName}" on port ${discovered.port}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Otherwise we want to rewrite the host config at runtime.
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
severity: "warn",
|
|
67
|
+
detail: `Found running SonarQube container "${discovered.containerName}" on port ${discovered.port} (config points at ${configuredPort})`,
|
|
68
|
+
extra: { remap: { host: discovered.host, port: discovered.port, container: discovered.containerName } },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2 — no running container. Is the configured port even free?
|
|
73
|
+
const free = await isPortAvailable(configuredPort);
|
|
56
74
|
if (free) {
|
|
57
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
severity: "info",
|
|
78
|
+
detail: `Port ${configuredPort} is free (Sonar container will be started on demand)`,
|
|
79
|
+
};
|
|
58
80
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
|
|
82
|
+
// Step 3 — port is busy. Maybe the occupant IS Sonar but not
|
|
83
|
+
// discovered by `docker ps` (rare: bare-metal Sonar, host-network
|
|
84
|
+
// container, etc.). HTTP-probe to find out.
|
|
85
|
+
const probeHost = `http://localhost:${configuredPort}`;
|
|
86
|
+
if (await isSonarReachable(probeHost)) {
|
|
62
87
|
return {
|
|
63
88
|
ok: true,
|
|
64
89
|
severity: "info",
|
|
65
|
-
detail: `Port ${
|
|
90
|
+
detail: `Port ${configuredPort} held by a Sonar instance (HTTP /api/system/status responds) — reusing it`,
|
|
66
91
|
};
|
|
67
92
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
|
|
94
|
+
// Step 4 — really not Sonar. Find an alternate free port and tell
|
|
95
|
+
// the runtime to use it. The compose template's published port is
|
|
96
|
+
// hardcoded at 9000 so we can't auto-rebind the container itself,
|
|
97
|
+
// but we CAN warn loudly with a clear, fixable message.
|
|
98
|
+
const occupant = await getPortOccupant(configuredPort);
|
|
99
|
+
const alt = await findAvailablePort(configuredPort + 1, 20);
|
|
100
|
+
if (alt) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
severity: "warn",
|
|
104
|
+
detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}). Will rebind Karajan-managed Sonar to ${alt}.`,
|
|
105
|
+
extra: { remap: { host: `http://localhost:${alt}`, port: alt, fromPort: configuredPort } },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Last-resort failure (no free port in the next 20).
|
|
71
109
|
return {
|
|
72
110
|
ok: false,
|
|
73
111
|
severity: "fail",
|
|
74
|
-
detail: `Port ${
|
|
75
|
-
fix:
|
|
76
|
-
|
|
112
|
+
detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}) and no free port in ${configuredPort + 1}..${configuredPort + 20}`,
|
|
113
|
+
fix: "Stop the conflicting process or set sonarqube.host in kj.config.yml.",
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async remediate({ extra }) {
|
|
117
|
+
if (!extra?.remap) {
|
|
118
|
+
return { fixed: false, detail: "No remap available" };
|
|
119
|
+
}
|
|
120
|
+
const { host, container, fromPort } = extra.remap;
|
|
121
|
+
const detail = container
|
|
122
|
+
? `Pointed Karajan at running SonarQube ${container} (${host})`
|
|
123
|
+
: `Rebound Karajan-managed Sonar from port ${fromPort} to ${host}`;
|
|
124
|
+
return {
|
|
125
|
+
fixed: true,
|
|
126
|
+
detail,
|
|
127
|
+
// The runner merges this into the runtime config so every
|
|
128
|
+
// downstream Sonar caller (scanner, manager, MCP handler) sees
|
|
129
|
+
// the new host without the user touching kj.config.yml.
|
|
130
|
+
changes: { sonarqube: { host } },
|
|
77
131
|
};
|
|
78
132
|
},
|
|
79
133
|
};
|
package/src/checks/sonar.js
CHANGED
|
@@ -36,7 +36,7 @@ export function createSonarStatusCheck() {
|
|
|
36
36
|
if (!sonarEnabled(config)) {
|
|
37
37
|
return { ok: true, severity: "info", detail: "Disabled in config" };
|
|
38
38
|
}
|
|
39
|
-
let reachable
|
|
39
|
+
let reachable;
|
|
40
40
|
try {
|
|
41
41
|
reachable = await isSonarReachable(host);
|
|
42
42
|
} catch {
|
package/src/checks/system.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { applyRunOverrides, loadConfig, validateConfig } from "../config.js";
|
|
2
|
+
import { createLogger } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a CLI action so it loads + validates config, builds the logger,
|
|
6
|
+
* and fires the anonymous cli_command telemetry event before invoking
|
|
7
|
+
* the real command body. Shared across every register-*.js module so
|
|
8
|
+
* each one doesn't need to re-import the config plumbing.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pkgVersion Current kj version (used for telemetry).
|
|
11
|
+
* @param {string} commandName The CLI command name being executed.
|
|
12
|
+
* @param {object} flags Flags parsed by Commander.
|
|
13
|
+
* @param {(ctx: { config: object, logger: object, flags: object }) => Promise<any>} fn
|
|
14
|
+
* Body of the command — receives the merged config + logger.
|
|
15
|
+
*/
|
|
16
|
+
export async function withConfig(pkgVersion, commandName, flags, fn) {
|
|
17
|
+
const { config } = await loadConfig();
|
|
18
|
+
const merged = applyRunOverrides(config, flags || {});
|
|
19
|
+
validateConfig(merged, commandName);
|
|
20
|
+
const logger = createLogger(merged.output.log_level);
|
|
21
|
+
|
|
22
|
+
// Telemetry: anonymous cli_command event (non-blocking).
|
|
23
|
+
import("../utils/telemetry.js")
|
|
24
|
+
.then(({ sendTelemetryEvent }) =>
|
|
25
|
+
sendTelemetryEvent("cli_command", { command: commandName, version: pkgVersion }, merged)
|
|
26
|
+
)
|
|
27
|
+
.catch(() => {});
|
|
28
|
+
|
|
29
|
+
return fn({ config: merged, logger, flags });
|
|
30
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { discoverCommand } from "../commands/discover.js";
|
|
2
|
+
import { triageCommand } from "../commands/triage.js";
|
|
3
|
+
import { researcherCommand } from "../commands/researcher.js";
|
|
4
|
+
import { architectCommand } from "../commands/architect.js";
|
|
5
|
+
import { auditCommand } from "../commands/audit.js";
|
|
6
|
+
import { resumeCommand } from "../commands/resume.js";
|
|
7
|
+
import { boardCommand } from "../commands/board.js";
|
|
8
|
+
import { undoCommand } from "../commands/undo.js";
|
|
9
|
+
import { syncCommand } from "../commands/sync.js";
|
|
10
|
+
import { cleanCommand } from "../commands/clean.js";
|
|
11
|
+
import { withConfig } from "./_shared.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the "meta" / single-role / housekeeping commands: pre-pipeline
|
|
15
|
+
* analysis (discover, triage, researcher, architect, audit), session
|
|
16
|
+
* management (resume, board, undo) and maintenance (update, sync, clean).
|
|
17
|
+
*
|
|
18
|
+
* Note: `triage` lives here (not in pipeline) because it's a single-role
|
|
19
|
+
* analysis command in the same family as discover / researcher /
|
|
20
|
+
* architect / audit, none of which run the full agent loop.
|
|
21
|
+
*/
|
|
22
|
+
export function registerMeta(program, { pkgVersion }) {
|
|
23
|
+
program
|
|
24
|
+
.command("discover")
|
|
25
|
+
.description("Analyze task for gaps, ambiguities and missing info")
|
|
26
|
+
.argument("[task]", "Task description (or use --task-file)")
|
|
27
|
+
.option("--task-file <path>", "Read the task from a file (e.g. .md)")
|
|
28
|
+
.option("--mode <name>", "Discovery mode: gaps|momtest|wendel|classify|jtbd", "gaps")
|
|
29
|
+
.option("--discover <name>", "Override discover agent")
|
|
30
|
+
.option("--discover-model <name>", "Override discover model")
|
|
31
|
+
.option("--json", "Output raw JSON")
|
|
32
|
+
.action(async (task, flags) => {
|
|
33
|
+
await withConfig(pkgVersion, "discover", flags, async ({ config, logger }) => {
|
|
34
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
35
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
36
|
+
await discoverCommand({ task: resolvedTask, config, logger, mode: flags.mode, json: flags.json });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command("triage")
|
|
42
|
+
.description("Classify task complexity and recommend pipeline roles")
|
|
43
|
+
.argument("[task]", "Task description (or use --task-file)")
|
|
44
|
+
.option("--task-file <path>", "Read the task from a file (e.g. .md)")
|
|
45
|
+
.option("--triage <name>", "Override triage agent")
|
|
46
|
+
.option("--triage-model <name>", "Override triage model")
|
|
47
|
+
.option("--json", "Output raw JSON")
|
|
48
|
+
.action(async (task, flags) => {
|
|
49
|
+
await withConfig(pkgVersion, "triage", flags, async ({ config, logger }) => {
|
|
50
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
51
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
52
|
+
await triageCommand({ task: resolvedTask, config, logger, json: flags.json });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command("researcher")
|
|
58
|
+
.description("Research codebase for a task (files, patterns, constraints)")
|
|
59
|
+
.argument("[task]", "Task description (or use --task-file)")
|
|
60
|
+
.option("--task-file <path>", "Read the task from a file (e.g. .md)")
|
|
61
|
+
.option("--researcher <name>", "Override researcher agent")
|
|
62
|
+
.option("--researcher-model <name>", "Override researcher model")
|
|
63
|
+
.action(async (task, flags) => {
|
|
64
|
+
await withConfig(pkgVersion, "researcher", flags, async ({ config, logger }) => {
|
|
65
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
66
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
67
|
+
await researcherCommand({ task: resolvedTask, config, logger });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command("architect")
|
|
73
|
+
.description("Design solution architecture (layers, patterns, contracts)")
|
|
74
|
+
.argument("[task]", "Task description (or use --task-file)")
|
|
75
|
+
.option("--task-file <path>", "Read the task from a file (e.g. .md)")
|
|
76
|
+
.option("--architect <name>", "Override architect agent")
|
|
77
|
+
.option("--architect-model <name>", "Override architect model")
|
|
78
|
+
.option("--context <text>", "Additional context (e.g. researcher output)")
|
|
79
|
+
.option("--json", "Output raw JSON")
|
|
80
|
+
.action(async (task, flags) => {
|
|
81
|
+
await withConfig(pkgVersion, "architect", flags, async ({ config, logger }) => {
|
|
82
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
83
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
84
|
+
await architectCommand({ task: resolvedTask, config, logger, context: flags.context, json: flags.json });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command("audit")
|
|
90
|
+
.description("Analyze codebase health (read-only)")
|
|
91
|
+
.argument("[task]", "Task description. If absent, defaults to a full-codebase analysis. Use --task-file to point at a .md.")
|
|
92
|
+
.option("--task-file <path>", "Read the task from a file (e.g. .md)")
|
|
93
|
+
.option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing", "all")
|
|
94
|
+
.option("--json", "Output raw JSON")
|
|
95
|
+
.option("--agent-readiness", "Score the repo for AI-agent readability (llms.txt, SKILL.md coverage, page token budgets, robots allowlist, heading hierarchy). LLM-free; uses [path] or cwd as the audit target. See issue #542.")
|
|
96
|
+
.option("--path <dir>", "Path to audit (used with --agent-readiness; defaults to cwd)")
|
|
97
|
+
.action(async (task, flags) => {
|
|
98
|
+
await withConfig(pkgVersion, "audit", flags, async ({ config, logger }) => {
|
|
99
|
+
let resolvedTask = task;
|
|
100
|
+
if (flags.taskFile) {
|
|
101
|
+
const { readTaskFile } = await import("../utils/task-file.js");
|
|
102
|
+
resolvedTask = await readTaskFile(flags.taskFile, { projectDir: config.projectDir });
|
|
103
|
+
}
|
|
104
|
+
await auditCommand({
|
|
105
|
+
task: resolvedTask || "Analyze the full codebase",
|
|
106
|
+
config, logger,
|
|
107
|
+
dimensions: flags.dimensions, json: flags.json,
|
|
108
|
+
agentReadiness: Boolean(flags.agentReadiness),
|
|
109
|
+
path: flags.path,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command("resume")
|
|
116
|
+
.description("Resume a paused session")
|
|
117
|
+
.argument("<sessionId>")
|
|
118
|
+
.option("--answer <text>", "Answer to the question that caused the pause")
|
|
119
|
+
.option("--json", "Output JSON only")
|
|
120
|
+
.action(async (sessionId, flags) => {
|
|
121
|
+
await withConfig(pkgVersion, "resume", flags, async ({ config, logger }) => {
|
|
122
|
+
await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
program
|
|
127
|
+
.command("update")
|
|
128
|
+
.description("Update karajan-code to the latest version from npm")
|
|
129
|
+
.action(async () => {
|
|
130
|
+
// Audit follow-up: was execaCommand (shell parsing). Inputs are
|
|
131
|
+
// constants today, but the project standard since #555 is execa
|
|
132
|
+
// with arg arrays (no shell). Migrated for consistency.
|
|
133
|
+
const { execa } = await import("execa");
|
|
134
|
+
console.log(`Current version: ${pkgVersion}`);
|
|
135
|
+
console.log("Checking for updates...");
|
|
136
|
+
try {
|
|
137
|
+
const { stdout } = await execa("npm", ["view", "karajan-code", "version"]);
|
|
138
|
+
const latest = stdout.trim();
|
|
139
|
+
if (latest === pkgVersion) {
|
|
140
|
+
console.log(`Already on the latest version (${pkgVersion}).`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(`Updating ${pkgVersion} → ${latest}...`);
|
|
144
|
+
await execa("npm", ["install", "-g", "karajan-code@latest"], { stdio: "inherit" });
|
|
145
|
+
console.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(`Update failed: ${err.message}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program
|
|
153
|
+
.command("board [action]")
|
|
154
|
+
.description("Manage HU Board (start|stop|status|open)")
|
|
155
|
+
.option("--port <number>", "Port (default: 4000)", "4000")
|
|
156
|
+
.action(async (action = "start", opts) => {
|
|
157
|
+
await withConfig(pkgVersion, "board", opts, async ({ config, logger }) => {
|
|
158
|
+
const port = Number(opts.port) || config.hu_board?.port || 4000;
|
|
159
|
+
await boardCommand({ action, port, logger });
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command("undo")
|
|
165
|
+
.description("Revert last pipeline run")
|
|
166
|
+
.option("--hard", "Discard all changes (default: soft reset, keeps changes staged)")
|
|
167
|
+
.action(async (flags) => {
|
|
168
|
+
await withConfig(pkgVersion, "undo", flags, async ({ logger }) => {
|
|
169
|
+
const result = await undoCommand({ hard: !!flags.hard, logger });
|
|
170
|
+
if (!result.ok) process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
program
|
|
175
|
+
.command("sync")
|
|
176
|
+
.description("Detect drift between code and the latest plan. Read-only report (issue #540 MVP).")
|
|
177
|
+
.option("--plan <planId>", "Sync against a specific plan instead of the latest")
|
|
178
|
+
.option("--json", "Emit machine-readable JSON instead of human output")
|
|
179
|
+
.action(async (flags) => {
|
|
180
|
+
await withConfig(pkgVersion, "sync", flags, async ({ config, logger }) => {
|
|
181
|
+
await syncCommand({ config, logger, planId: flags.plan, json: flags.json });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
program
|
|
186
|
+
.command("clean")
|
|
187
|
+
.description("Garbage-collect stale plans, sessions and HU batches (dry-run by default)")
|
|
188
|
+
.option("--yes", "Actually delete — without this flag, only prints what would be removed")
|
|
189
|
+
.option("--nuke", "Retention=0 everywhere + wipe HU board DB. \"I want it all gone\"")
|
|
190
|
+
.option("--plan-days <n>", "Keep finalised plans (approved/rejected/executed) for N days (default: 30)")
|
|
191
|
+
.option("--draft-days <n>", "Keep draft plans for N days (default: 60)")
|
|
192
|
+
.option("--session-days <n>", "Keep finalised sessions for N days (default: 7)")
|
|
193
|
+
.option("--hu-days <n>", "Keep HU story batches for N days (default: 14)")
|
|
194
|
+
.action(async (flags) => {
|
|
195
|
+
await cleanCommand({
|
|
196
|
+
yes: Boolean(flags.yes),
|
|
197
|
+
nuke: Boolean(flags.nuke),
|
|
198
|
+
planDays: flags.planDays ? Number(flags.planDays) : undefined,
|
|
199
|
+
draftDays: flags.draftDays ? Number(flags.draftDays) : undefined,
|
|
200
|
+
sessionDays: flags.sessionDays ? Number(flags.sessionDays) : undefined,
|
|
201
|
+
huDays: flags.huDays ? Number(flags.huDays) : undefined,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|