litclaude-ai 0.2.2
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/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const HUD_ACCENT_THEMES = Object.freeze([
|
|
2
|
+
{ name: "cyan", code: 81, mood: "LitClaude default" },
|
|
3
|
+
{ name: "blue", code: 39, mood: "electric blue" },
|
|
4
|
+
{ name: "teal", code: 49, mood: "bright teal" },
|
|
5
|
+
{ name: "green", code: 118, mood: "neon green" },
|
|
6
|
+
{ name: "lavender", code: 177, mood: "bright violet" },
|
|
7
|
+
{ name: "rose", code: 198, mood: "hot pink" },
|
|
8
|
+
{ name: "gold", code: 220, mood: "signal amber" },
|
|
9
|
+
{ name: "orange", code: 208, mood: "vivid orange" },
|
|
10
|
+
{ name: "slate", code: 75, mood: "ice blue" },
|
|
11
|
+
{ name: "gray", code: 231, mood: "bright white" },
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const accentNames = new Set(HUD_ACCENT_THEMES.map((theme) => theme.name));
|
|
15
|
+
|
|
16
|
+
export const normalizeHudAccent = (value) => {
|
|
17
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
18
|
+
return accentNames.has(normalized) ? normalized : "cyan";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const themeForAccent = (value) => {
|
|
22
|
+
const normalized = normalizeHudAccent(value);
|
|
23
|
+
return HUD_ACCENT_THEMES.find((theme) => theme.name === normalized) ?? HUD_ACCENT_THEMES[0];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const accentCodeForName = (value) => themeForAccent(value).code;
|
|
27
|
+
|
|
28
|
+
// --- LitClaude neon brand banner (D21) -------------------------------------
|
|
29
|
+
// `[🔥LITCLAUDE vX.Y.Z]` rendered as a neon glow. Truecolor terminals get a
|
|
30
|
+
// per-letter hot-pink -> electric-cyan gradient; 256-color terminals fall back
|
|
31
|
+
// to bright-magenta bold; NO_COLOR is plain text. The 🔥 is mandatory and the
|
|
32
|
+
// brand is accent-independent (LITCLAUDE_HUD_ACCENT still themes the HUD body).
|
|
33
|
+
const NEON_BRAND = "LITCLAUDE";
|
|
34
|
+
const NEON_START = [255, 45, 149]; // #ff2d95 hot pink
|
|
35
|
+
const NEON_END = [0, 229, 255]; // #00e5ff electric cyan
|
|
36
|
+
|
|
37
|
+
export const supportsTruecolor = (env = process.env) =>
|
|
38
|
+
/truecolor|24bit/iu.test(String(env.COLORTERM || ""));
|
|
39
|
+
|
|
40
|
+
export const litBrandPrefix = (version, { noColor = false, truecolor = false } = {}) => {
|
|
41
|
+
const label = `[🔥${NEON_BRAND} v${version}]`;
|
|
42
|
+
if (noColor) return label;
|
|
43
|
+
if (!truecolor) {
|
|
44
|
+
// 256-color neon fallback: bright magenta, bold.
|
|
45
|
+
return `\x1b[1m\x1b[38;5;201m${label}\x1b[0m`;
|
|
46
|
+
}
|
|
47
|
+
const n = NEON_BRAND.length;
|
|
48
|
+
let out = `\x1b[1m[🔥`;
|
|
49
|
+
for (let i = 0; i < n; i += 1) {
|
|
50
|
+
const t = n === 1 ? 0 : i / (n - 1);
|
|
51
|
+
const r = Math.round(NEON_START[0] + (NEON_END[0] - NEON_START[0]) * t);
|
|
52
|
+
const g = Math.round(NEON_START[1] + (NEON_END[1] - NEON_START[1]) * t);
|
|
53
|
+
const b = Math.round(NEON_START[2] + (NEON_END[2] - NEON_START[2]) * t);
|
|
54
|
+
out += `\x1b[38;2;${r};${g};${b}m${NEON_BRAND[i]}`;
|
|
55
|
+
}
|
|
56
|
+
out += `\x1b[38;2;${NEON_END[0]};${NEON_END[1]};${NEON_END[2]}m v${version}]\x1b[0m`;
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { appendLitgoalLedger } from "./ledger.mjs";
|
|
4
|
+
import {
|
|
5
|
+
litgoalBriefPath,
|
|
6
|
+
litgoalGoalsPath,
|
|
7
|
+
litgoalLedgerPath,
|
|
8
|
+
litgoalLockDir,
|
|
9
|
+
} from "./paths.mjs";
|
|
10
|
+
import { readLitgoalState, LitgoalStateError, withLitgoalLock, writeLitgoalState } from "./state.mjs";
|
|
11
|
+
|
|
12
|
+
const subcommands = [
|
|
13
|
+
["create-goals", "Create durable goal records from a brief."],
|
|
14
|
+
["status", "Print the current litgoal status."],
|
|
15
|
+
["criteria", "List or update success criteria."],
|
|
16
|
+
["record-evidence", "Record evidence as JSON."],
|
|
17
|
+
["checkpoint", "Write a continuation checkpoint."],
|
|
18
|
+
["steer", "Record a steering decision."],
|
|
19
|
+
["record-review-blockers", "Record review blockers."],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const helpText = `Usage: litclaude-ai litgoal <command> [...args]
|
|
23
|
+
litclaude-ai litgoal --help
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
${subcommands.map(([name, description]) => ` ${name.padEnd(22)} ${description}`).join("\n")}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
class LitgoalCliError extends Error {
|
|
30
|
+
constructor(message, status = 64) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "LitgoalCliError";
|
|
33
|
+
this.status = status;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nowIso = () => new Date().toISOString();
|
|
38
|
+
|
|
39
|
+
const hasFlag = (args, flag) => args.includes(flag);
|
|
40
|
+
|
|
41
|
+
const optionValue = (args, flag) => {
|
|
42
|
+
const index = args.indexOf(flag);
|
|
43
|
+
if (index === -1) return undefined;
|
|
44
|
+
const value = args[index + 1];
|
|
45
|
+
if (!value || value.startsWith("--")) return "";
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const requireOption = (args, flag, label = flag.slice(2)) => {
|
|
50
|
+
const value = optionValue(args, flag);
|
|
51
|
+
if (!value) throw new LitgoalCliError(`missing ${label}`);
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const parseEvidenceJson = (args) => {
|
|
56
|
+
const index = args.indexOf("--json");
|
|
57
|
+
if (index === -1) return {};
|
|
58
|
+
const value = args[index + 1];
|
|
59
|
+
if (!value || value.startsWith("--")) return {};
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(value);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new LitgoalCliError("invalid litgoal JSON");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const emit = (io, payload, outputJson) => {
|
|
68
|
+
if (outputJson) {
|
|
69
|
+
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
io.stdout.write(`${payload.message ?? JSON.stringify(payload)}\n`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const readState = (cwd) => readLitgoalState(litgoalGoalsPath(cwd), null);
|
|
76
|
+
|
|
77
|
+
const requireState = (cwd) => {
|
|
78
|
+
const state = readState(cwd);
|
|
79
|
+
if (!state) throw new LitgoalStateError("litgoal state not found");
|
|
80
|
+
return state;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const appendLedger = (cwd, entry) => appendLitgoalLedger(litgoalLedgerPath(cwd), entry);
|
|
84
|
+
|
|
85
|
+
const defaultCriterion = (brief) => ({
|
|
86
|
+
id: "criterion-1",
|
|
87
|
+
description: "Objective has observable verification evidence.",
|
|
88
|
+
expectedEvidence: `Evidence that "${brief}" is complete.`,
|
|
89
|
+
status: "pending",
|
|
90
|
+
evidence: [],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const createGoals = (cwd, args) => {
|
|
94
|
+
const brief = requireOption(args, "--brief", "brief").trim();
|
|
95
|
+
if (!brief) throw new LitgoalCliError("missing brief");
|
|
96
|
+
return withLitgoalLock(litgoalLockDir(cwd), () => {
|
|
97
|
+
const timestamp = nowIso();
|
|
98
|
+
const state = {
|
|
99
|
+
version: 1,
|
|
100
|
+
objective: brief,
|
|
101
|
+
status: "active",
|
|
102
|
+
criteria: [defaultCriterion(brief)],
|
|
103
|
+
blockers: [],
|
|
104
|
+
steering: [],
|
|
105
|
+
checkpoints: [],
|
|
106
|
+
createdAt: timestamp,
|
|
107
|
+
updatedAt: timestamp,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
mkdirSync(dirname(litgoalBriefPath(cwd)), { recursive: true });
|
|
111
|
+
writeFileSync(litgoalBriefPath(cwd), `${brief}\n`);
|
|
112
|
+
writeLitgoalState(litgoalGoalsPath(cwd), state);
|
|
113
|
+
appendLedger(cwd, {
|
|
114
|
+
event: "goal.created",
|
|
115
|
+
objective: brief,
|
|
116
|
+
status: state.status,
|
|
117
|
+
criteria: state.criteria.map(({ id, status }) => ({ id, status })),
|
|
118
|
+
});
|
|
119
|
+
return state;
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const listCriteria = (cwd) => {
|
|
124
|
+
const state = requireState(cwd);
|
|
125
|
+
return { criteria: state.criteria ?? [] };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const updateCriterionWithEvidence = (cwd, args) => {
|
|
129
|
+
const evidence = parseEvidenceJson(args);
|
|
130
|
+
return withLitgoalLock(litgoalLockDir(cwd), () => {
|
|
131
|
+
const criterionId = requireOption(args, "--criterion", "criterion");
|
|
132
|
+
const status = optionValue(args, "--status") || "pass";
|
|
133
|
+
if (!["pass", "fail", "blocked"].includes(status)) {
|
|
134
|
+
throw new LitgoalCliError("invalid evidence status");
|
|
135
|
+
}
|
|
136
|
+
const state = requireState(cwd);
|
|
137
|
+
const criterion = state.criteria?.find(({ id }) => id === criterionId);
|
|
138
|
+
if (!criterion) throw new LitgoalStateError("unknown criterion");
|
|
139
|
+
|
|
140
|
+
const record = {
|
|
141
|
+
status,
|
|
142
|
+
...evidence,
|
|
143
|
+
recordedAt: nowIso(),
|
|
144
|
+
};
|
|
145
|
+
criterion.status = status;
|
|
146
|
+
criterion.evidence = [...(criterion.evidence ?? []), record];
|
|
147
|
+
state.updatedAt = record.recordedAt;
|
|
148
|
+
writeLitgoalState(litgoalGoalsPath(cwd), state);
|
|
149
|
+
appendLedger(cwd, {
|
|
150
|
+
event: "evidence.recorded",
|
|
151
|
+
criterionId,
|
|
152
|
+
status,
|
|
153
|
+
evidence,
|
|
154
|
+
});
|
|
155
|
+
return { criterion };
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const checkpoint = (cwd, args) =>
|
|
160
|
+
withLitgoalLock(litgoalLockDir(cwd), () => {
|
|
161
|
+
const state = requireState(cwd);
|
|
162
|
+
const status = optionValue(args, "--status") || "active";
|
|
163
|
+
if (!["active", "complete", "blocked"].includes(status)) {
|
|
164
|
+
throw new LitgoalCliError("invalid checkpoint status");
|
|
165
|
+
}
|
|
166
|
+
const criteria = state.criteria ?? [];
|
|
167
|
+
if (status === "complete" && !criteria.every((criterion) => criterion.status === "pass")) {
|
|
168
|
+
throw new LitgoalStateError("criteria must pass before completion");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const record = {
|
|
172
|
+
status,
|
|
173
|
+
note: optionValue(args, "--note") || "",
|
|
174
|
+
createdAt: nowIso(),
|
|
175
|
+
};
|
|
176
|
+
state.status = status;
|
|
177
|
+
state.checkpoints = [...(state.checkpoints ?? []), record];
|
|
178
|
+
state.updatedAt = record.createdAt;
|
|
179
|
+
writeLitgoalState(litgoalGoalsPath(cwd), state);
|
|
180
|
+
appendLedger(cwd, { event: "checkpoint.recorded", ...record });
|
|
181
|
+
return { status: state.status, checkpoint: record };
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const steer = (cwd, args) =>
|
|
185
|
+
withLitgoalLock(litgoalLockDir(cwd), () => {
|
|
186
|
+
const state = requireState(cwd);
|
|
187
|
+
const kind = requireOption(args, "--kind", "kind");
|
|
188
|
+
if (!["scope", "priority", "blocker", "quality", "handoff"].includes(kind)) {
|
|
189
|
+
throw new LitgoalCliError("invalid steering kind");
|
|
190
|
+
}
|
|
191
|
+
const record = {
|
|
192
|
+
kind,
|
|
193
|
+
note: requireOption(args, "--note", "note"),
|
|
194
|
+
createdAt: nowIso(),
|
|
195
|
+
};
|
|
196
|
+
state.steering = [...(state.steering ?? []), record];
|
|
197
|
+
state.updatedAt = record.createdAt;
|
|
198
|
+
writeLitgoalState(litgoalGoalsPath(cwd), state);
|
|
199
|
+
appendLedger(cwd, { event: "steering.recorded", ...record });
|
|
200
|
+
return record;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const recordReviewBlockers = (cwd, args) =>
|
|
204
|
+
withLitgoalLock(litgoalLockDir(cwd), () => {
|
|
205
|
+
const state = requireState(cwd);
|
|
206
|
+
const blocker = optionValue(args, "--blocker") || optionValue(args, "--note") || "review blocker recorded";
|
|
207
|
+
const record = {
|
|
208
|
+
blocker,
|
|
209
|
+
status: optionValue(args, "--status") || "blocked",
|
|
210
|
+
createdAt: nowIso(),
|
|
211
|
+
};
|
|
212
|
+
state.blockers = [...(state.blockers ?? []), record];
|
|
213
|
+
state.updatedAt = record.createdAt;
|
|
214
|
+
writeLitgoalState(litgoalGoalsPath(cwd), state);
|
|
215
|
+
appendLedger(cwd, { event: "review_blocker.recorded", ...record });
|
|
216
|
+
return record;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const runCommand = (cwd, command, args) => {
|
|
220
|
+
switch (command) {
|
|
221
|
+
case "create-goals":
|
|
222
|
+
return createGoals(cwd, args);
|
|
223
|
+
case "status":
|
|
224
|
+
return requireState(cwd);
|
|
225
|
+
case "criteria":
|
|
226
|
+
return listCriteria(cwd);
|
|
227
|
+
case "record-evidence":
|
|
228
|
+
return updateCriterionWithEvidence(cwd, args);
|
|
229
|
+
case "checkpoint":
|
|
230
|
+
return checkpoint(cwd, args);
|
|
231
|
+
case "steer":
|
|
232
|
+
return steer(cwd, args);
|
|
233
|
+
case "record-review-blockers":
|
|
234
|
+
return recordReviewBlockers(cwd, args);
|
|
235
|
+
default:
|
|
236
|
+
throw new LitgoalCliError(`Unknown litgoal command: ${command}`);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const runLitgoalCli = (argv, io = { stdout: process.stdout, stderr: process.stderr }, cwd = process.cwd()) => {
|
|
241
|
+
const [command, ...rest] = argv;
|
|
242
|
+
|
|
243
|
+
if (!command || command === "--help" || command === "-h") {
|
|
244
|
+
io.stdout.write(helpText);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!subcommands.some(([name]) => name === command)) {
|
|
249
|
+
io.stderr.write(`Unknown litgoal command: ${command}\n`);
|
|
250
|
+
return 64;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const outputJson = hasFlag(rest, "--json");
|
|
255
|
+
const result = runCommand(cwd, command, rest);
|
|
256
|
+
emit(io, result, outputJson);
|
|
257
|
+
return 0;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof LitgoalCliError || error instanceof LitgoalStateError) {
|
|
260
|
+
io.stderr.write(`${error.message}\n`);
|
|
261
|
+
return error.status;
|
|
262
|
+
}
|
|
263
|
+
io.stderr.write("litgoal runtime error\n");
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, fsyncSync, mkdirSync, openSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const appendLitgoalLedger = (path, entry) => {
|
|
5
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
6
|
+
const record = {
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
...entry,
|
|
9
|
+
};
|
|
10
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`);
|
|
11
|
+
// Flush the appended record to disk for durability.
|
|
12
|
+
const fd = openSync(path, "r");
|
|
13
|
+
fsyncSync(fd);
|
|
14
|
+
closeSync(fd);
|
|
15
|
+
return record;
|
|
16
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const litgoalStateDir = (cwd = process.cwd()) => join(resolve(cwd), ".litclaude", "litgoal");
|
|
4
|
+
export const litgoalBriefPath = (cwd = process.cwd()) => join(litgoalStateDir(cwd), "brief.md");
|
|
5
|
+
export const litgoalGoalsPath = (cwd = process.cwd()) => join(litgoalStateDir(cwd), "goals.json");
|
|
6
|
+
export const litgoalLedgerPath = (cwd = process.cwd()) => join(litgoalStateDir(cwd), "ledger.jsonl");
|
|
7
|
+
export const litgoalLockDir = (cwd = process.cwd()) => join(litgoalStateDir(cwd), ".lock");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export class LitgoalStateError extends Error {
|
|
5
|
+
constructor(message, status = 65) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "LitgoalStateError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const readLitgoalState = (path, fallback = {}) => {
|
|
13
|
+
if (!existsSync(path)) return fallback;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
throw new LitgoalStateError("corrupt litgoal state");
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const writeLitgoalState = (path, state) => {
|
|
22
|
+
const dir = dirname(path);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
25
|
+
writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
26
|
+
// Flush tmp file data to disk before rename so the rename is durable.
|
|
27
|
+
const fd = openSync(tmpPath, "r");
|
|
28
|
+
fsyncSync(fd);
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
renameSync(tmpPath, path);
|
|
31
|
+
// Best-effort: fsync the parent directory so the directory entry is durable.
|
|
32
|
+
// Some filesystems (NTFS, FAT, certain network mounts) reject dir fsync with
|
|
33
|
+
// EINVAL/EPERM/EISDIR — swallow those silently.
|
|
34
|
+
try {
|
|
35
|
+
const dfd = openSync(dir, "r");
|
|
36
|
+
fsyncSync(dfd);
|
|
37
|
+
closeSync(dfd);
|
|
38
|
+
} catch {
|
|
39
|
+
// best-effort only; file data is already durable via the tmp fsync above
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const sleepSync = (milliseconds) => {
|
|
44
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const withLitgoalLock = (lockDir, fn) => {
|
|
48
|
+
mkdirSync(dirname(lockDir), { recursive: true });
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(lockDir, { recursive: false });
|
|
53
|
+
break;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (Date.now() - startedAt > 5000) {
|
|
56
|
+
throw new LitgoalStateError("litgoal state lock timed out");
|
|
57
|
+
}
|
|
58
|
+
sleepSync(20);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return fn();
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const mutationToolNames = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit", "notebookedit"]);
|
|
2
|
+
|
|
3
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
|
|
5
|
+
const addString = (paths, value) => {
|
|
6
|
+
if (typeof value === "string" && value.length > 0) paths.add(value);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const addStringArray = (paths, value) => {
|
|
10
|
+
if (!Array.isArray(value)) return;
|
|
11
|
+
for (const item of value) addString(paths, item);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const patchHeaderPath = (line) => {
|
|
15
|
+
for (const prefix of ["*** Add File: ", "*** Update File: ", "*** Move to: "]) {
|
|
16
|
+
if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const addPatchInput = (paths, value) => {
|
|
22
|
+
if (typeof value !== "string") return;
|
|
23
|
+
for (const line of value.split(/\r?\n/u)) {
|
|
24
|
+
const path = patchHeaderPath(line);
|
|
25
|
+
if (path) paths.add(path);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const addPatchRecords = (paths, value) => {
|
|
30
|
+
if (!Array.isArray(value)) return;
|
|
31
|
+
for (const item of value) {
|
|
32
|
+
if (!isRecord(item)) continue;
|
|
33
|
+
addString(paths, item.path);
|
|
34
|
+
addString(paths, item.filePath);
|
|
35
|
+
addString(paths, item.file_path);
|
|
36
|
+
addString(paths, item.movePath);
|
|
37
|
+
addString(paths, item.move_path);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isFailedResponse = (value) =>
|
|
42
|
+
isRecord(value) && (value.isError === true || value.is_error === true || value.error === true || value.status === "error");
|
|
43
|
+
|
|
44
|
+
export const extractMutatedFilePaths = (input) => {
|
|
45
|
+
const toolName = typeof input?.tool_name === "string" ? input.tool_name.toLowerCase() : "";
|
|
46
|
+
if (!mutationToolNames.has(toolName)) return [];
|
|
47
|
+
if (isFailedResponse(input.tool_response)) return [];
|
|
48
|
+
|
|
49
|
+
const toolInput = isRecord(input.tool_input) ? input.tool_input : {};
|
|
50
|
+
const paths = new Set();
|
|
51
|
+
addString(paths, toolInput.path);
|
|
52
|
+
addString(paths, toolInput.filePath);
|
|
53
|
+
addString(paths, toolInput.file_path);
|
|
54
|
+
addStringArray(paths, toolInput.paths);
|
|
55
|
+
addStringArray(paths, toolInput.filePaths);
|
|
56
|
+
addStringArray(paths, toolInput.file_paths);
|
|
57
|
+
addPatchInput(paths, toolInput.input);
|
|
58
|
+
addPatchInput(paths, toolInput.patch);
|
|
59
|
+
addPatchInput(paths, toolInput.command);
|
|
60
|
+
addPatchRecords(paths, toolInput.files);
|
|
61
|
+
addPatchRecords(paths, toolInput.changes);
|
|
62
|
+
return [...paths];
|
|
63
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
5
|
+
|
|
6
|
+
const normalizeSessionId = (sessionId) => {
|
|
7
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return undefined;
|
|
8
|
+
return sessionId;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const activeWork = (state, sessionId) => {
|
|
12
|
+
if (!state || typeof state !== "object" || !state.works || typeof state.works !== "object") return null;
|
|
13
|
+
const active = typeof state.active_work_id === "string" ? state.works[state.active_work_id] : undefined;
|
|
14
|
+
if (active?.status === "active") return active;
|
|
15
|
+
|
|
16
|
+
const normalizedSession = normalizeSessionId(sessionId);
|
|
17
|
+
if (!normalizedSession) return null;
|
|
18
|
+
return Object.values(state.works).find((work) =>
|
|
19
|
+
work?.status === "active" && Array.isArray(work.session_ids) && work.session_ids.includes(normalizedSession),
|
|
20
|
+
) ?? null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const firstUncheckedTopLevelTask = (text) => {
|
|
24
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
25
|
+
const match = /^- \[ \]\s+(.+?)\s*$/u.exec(line);
|
|
26
|
+
if (match) return match[1];
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const planTitle = (text) => {
|
|
32
|
+
const match = /^#\s+(.+?)\s*$/mu.exec(text);
|
|
33
|
+
return match?.[1] ?? "";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const sanitizePath = (path) => path.replaceAll("\\", "/");
|
|
37
|
+
|
|
38
|
+
const renderDirective = ({ planPath, ledgerPath, nextTask, worktreePath, planName, title }) => {
|
|
39
|
+
const lines = [
|
|
40
|
+
"LitClaude start-work continuation is active.",
|
|
41
|
+
`Plan: ${sanitizePath(planPath)}`,
|
|
42
|
+
`Ledger: ${sanitizePath(ledgerPath)}`,
|
|
43
|
+
`Next top-level task: ${nextTask}`,
|
|
44
|
+
"Before edits: reread the plan, ledger, Boulder state, and git status; continue with PIN -> RED -> GREEN -> VERIFY -> SURFACE -> REVIEW -> CLEAN -> RECORD.",
|
|
45
|
+
];
|
|
46
|
+
if (worktreePath) lines.splice(3, 0, `Worktree: ${sanitizePath(worktreePath)}`);
|
|
47
|
+
if (title) lines.splice(2, 0, `Title: ${title}`);
|
|
48
|
+
if (planName) lines.splice(1, 0, `Work: ${planName}`);
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const createStartWorkContinuation = (root, options = {}) => {
|
|
53
|
+
const boulderPath = join(root, ".litclaude", "boulder.json");
|
|
54
|
+
if (!existsSync(boulderPath)) return null;
|
|
55
|
+
|
|
56
|
+
let state;
|
|
57
|
+
try {
|
|
58
|
+
state = readJson(boulderPath);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const work = activeWork(state, options.sessionId);
|
|
64
|
+
if (!work || typeof work.active_plan !== "string") return null;
|
|
65
|
+
|
|
66
|
+
const planPath = resolve(root, work.active_plan);
|
|
67
|
+
if (!existsSync(planPath)) return null;
|
|
68
|
+
const planText = readFileSync(planPath, "utf8");
|
|
69
|
+
const nextTask = firstUncheckedTopLevelTask(planText);
|
|
70
|
+
if (!nextTask) return null;
|
|
71
|
+
|
|
72
|
+
const ledgerPath = join(root, ".litclaude", "start-work", "ledger.jsonl");
|
|
73
|
+
return renderDirective({
|
|
74
|
+
planPath: work.active_plan,
|
|
75
|
+
ledgerPath: existsSync(ledgerPath) ? ".litclaude/start-work/ledger.jsonl" : ".litclaude/start-work/ledger.jsonl",
|
|
76
|
+
nextTask,
|
|
77
|
+
worktreePath: typeof work.worktree_path === "string" ? work.worktree_path : "",
|
|
78
|
+
planName: typeof work.plan_name === "string" ? work.plan_name : basename(planPath),
|
|
79
|
+
title: planTitle(planText),
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const runStartWorkContinuationCli = (defaultRoot, args, io = { stdout: process.stdout, stderr: process.stderr }) => {
|
|
84
|
+
const outputJson = args.includes("--json");
|
|
85
|
+
const rootIndex = args.indexOf("--root");
|
|
86
|
+
const sessionIndex = args.indexOf("--session-id");
|
|
87
|
+
const root = rootIndex >= 0 && typeof args[rootIndex + 1] === "string" ? resolve(args[rootIndex + 1]) : defaultRoot;
|
|
88
|
+
const sessionId = sessionIndex >= 0 ? args[sessionIndex + 1] : undefined;
|
|
89
|
+
const directive = createStartWorkContinuation(root, { sessionId });
|
|
90
|
+
|
|
91
|
+
if (outputJson) {
|
|
92
|
+
io.stdout.write(`${JSON.stringify({ status: directive ? "active" : "idle", directive }, null, 2)}\n`);
|
|
93
|
+
} else if (directive) {
|
|
94
|
+
io.stdout.write(`${directive}\n`);
|
|
95
|
+
} else {
|
|
96
|
+
io.stdout.write("START_WORK_CONTINUATION_IDLE\n");
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const includesAll = (text, terms) => terms.every((term) => text.includes(term));
|
|
6
|
+
|
|
7
|
+
const readText = (path) => readFileSync(path, "utf8");
|
|
8
|
+
|
|
9
|
+
const runHookProbe = (root) => {
|
|
10
|
+
const hookPath = join(root, "plugins", "litclaude", "bin", "litclaude-hook.js");
|
|
11
|
+
const result = spawnSync(process.execPath, [hookPath, "user-prompt-submit"], {
|
|
12
|
+
cwd: root,
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
input: JSON.stringify({
|
|
15
|
+
hook_event_name: "UserPromptSubmit",
|
|
16
|
+
prompt: "$dynamic-workflow verify delegation",
|
|
17
|
+
cwd: root,
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
if (result.status !== 0) return "";
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(result.stdout).hookSpecificOutput?.additionalContext ?? "";
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createWorkflowCheckReport = (root, version) => {
|
|
29
|
+
const commandPath = join(root, "plugins", "litclaude", "commands", "dynamic-workflow.md");
|
|
30
|
+
const agentsPath = join(root, "docs", "agents.md");
|
|
31
|
+
const skillsRoot = join(root, "plugins", "litclaude", "skills");
|
|
32
|
+
const hookContext = runHookProbe(root);
|
|
33
|
+
const commandText = existsSync(commandPath) ? readText(commandPath) : "";
|
|
34
|
+
const agentsText = existsSync(agentsPath) ? readText(agentsPath) : "";
|
|
35
|
+
const orchestrationSkillText = ["lit-loop", "lit-plan", "start-work", "review-work"]
|
|
36
|
+
.map((skillName) => {
|
|
37
|
+
const path = join(skillsRoot, skillName, "SKILL.md");
|
|
38
|
+
return existsSync(path) ? readText(path) : "";
|
|
39
|
+
})
|
|
40
|
+
.join("\n");
|
|
41
|
+
const reliabilityTerms = ["TASK:", "DELIVERABLE", "SCOPE", "VERIFY", "short wait", "missing deliverable", "BLOCKED:"];
|
|
42
|
+
|
|
43
|
+
const checks = {
|
|
44
|
+
goalGuidance: includesAll(`${hookContext}\n${commandText}`, ["get_goal", "create_goal", "update_goal", "/goal"]),
|
|
45
|
+
dynamicWorkflowGuidance: includesAll(`${hookContext}\n${commandText}`, ["Workflow", "EnterWorktree", "claude --worktree"]),
|
|
46
|
+
subagentDelegation: includesAll(`${hookContext}\n${commandText}\n${agentsText}`, [
|
|
47
|
+
"prometheus-planner",
|
|
48
|
+
"boulder-executor",
|
|
49
|
+
"oracle-verifier",
|
|
50
|
+
"qa-runner",
|
|
51
|
+
]),
|
|
52
|
+
subagentReliability: includesAll(`${hookContext}\n${commandText}\n${orchestrationSkillText}`, reliabilityTerms),
|
|
53
|
+
commandHookAgreement: includesAll(hookContext, reliabilityTerms) && includesAll(commandText, reliabilityTerms),
|
|
54
|
+
dynamicWorkflowCommand: existsSync(commandPath),
|
|
55
|
+
hookRoute: hookContext.includes("/litclaude:dynamic-workflow"),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const missing = Object.entries(checks)
|
|
59
|
+
.filter(([, passed]) => !passed)
|
|
60
|
+
.map(([name]) => name);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
status: missing.length === 0 ? "pass" : "fail",
|
|
64
|
+
version,
|
|
65
|
+
checks,
|
|
66
|
+
missing,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const runWorkflowCheckCli = (root, version, args, io = { stdout: process.stdout, stderr: process.stderr }) => {
|
|
71
|
+
const outputJson = args.includes("--json");
|
|
72
|
+
const report = createWorkflowCheckReport(root, version);
|
|
73
|
+
if (outputJson) {
|
|
74
|
+
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
75
|
+
} else {
|
|
76
|
+
io.stdout.write(`WORKFLOW_CHECK_${report.status.toUpperCase()}\n`);
|
|
77
|
+
for (const [name, passed] of Object.entries(report.checks)) {
|
|
78
|
+
io.stdout.write(`${name}=${passed ? "pass" : "fail"}\n`);
|
|
79
|
+
}
|
|
80
|
+
if (report.missing.length) io.stdout.write(`missing=${report.missing.join(",")}\n`);
|
|
81
|
+
}
|
|
82
|
+
return report.status === "pass" ? 0 : 1;
|
|
83
|
+
};
|