@tech-leads-club/harness-toolkit 0.2.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/CHANGELOG.md +95 -0
- package/LICENSE +84 -0
- package/NOTICE +9 -0
- package/README.md +480 -0
- package/bin/tlc +10 -0
- package/bin/tlc-build +80 -0
- package/bin/tlc-cli.ts +1509 -0
- package/bin/tlc-exec +10 -0
- package/bin/tlc-exec.cmd +4 -0
- package/bin/tlc-exec.d.mts +60 -0
- package/bin/tlc-exec.mjs +248 -0
- package/bin/tlc.cmd +4 -0
- package/bin/tlc.mjs +7 -0
- package/bin/write-user-hooks.d.mts +40 -0
- package/bin/write-user-hooks.mjs +123 -0
- package/capabilities/catalog.json +418 -0
- package/config.example.json +51 -0
- package/dist/compact-before.mjs +7839 -0
- package/dist/doctor.mjs +8263 -0
- package/dist/help-topic.mjs +82 -0
- package/dist/init-project.mjs +993 -0
- package/dist/install-runtime.mjs +969 -0
- package/dist/lessons-cli.mjs +7258 -0
- package/dist/obs-cli.mjs +7025 -0
- package/dist/price-lookup.mjs +221 -0
- package/dist/prompt-submit.mjs +7860 -0
- package/dist/refresh-model-prices.mjs +158 -0
- package/dist/response-after.mjs +7879 -0
- package/dist/run.mjs +7830 -0
- package/dist/session-end.mjs +7937 -0
- package/dist/session-start.mjs +8056 -0
- package/dist/shim.mjs +52 -0
- package/dist/stop.mjs +8494 -0
- package/dist/subagent-start.mjs +7861 -0
- package/dist/subagent-stop.mjs +7851 -0
- package/dist/support.mjs +7045 -0
- package/dist/tlc-cli.mjs +8063 -0
- package/dist/tool-after.mjs +8113 -0
- package/dist/tool-before.mjs +7985 -0
- package/dist/tool-failure.mjs +7850 -0
- package/dist/uninstall-runtime.mjs +1307 -0
- package/docs/architecture.md +270 -0
- package/docs/concepts.md +562 -0
- package/docs/coverage.md +55 -0
- package/docs/decisions/ad-001.md +32 -0
- package/docs/decisions/ad-002.md +51 -0
- package/docs/decisions/ad-003.md +30 -0
- package/docs/decisions/ad-004.md +37 -0
- package/docs/decisions/ad-005.md +36 -0
- package/docs/decisions/ad-006.md +49 -0
- package/docs/decisions/ad-007.md +36 -0
- package/docs/decisions/ad-008.md +54 -0
- package/docs/decisions/ad-009.md +61 -0
- package/docs/decisions/ad-010.md +45 -0
- package/docs/decisions/ad-011.md +59 -0
- package/docs/decisions/ad-012.md +71 -0
- package/docs/decisions/ad-013.md +87 -0
- package/docs/decisions/ad-014.md +56 -0
- package/docs/decisions/ad-015.md +33 -0
- package/docs/decisions/ad-016.md +98 -0
- package/docs/decisions/ad-017.md +65 -0
- package/docs/decisions/ad-018.md +77 -0
- package/docs/decisions/ad-019.md +75 -0
- package/docs/decisions/ad-020.md +88 -0
- package/docs/decisions/ad-021.md +57 -0
- package/docs/decisions/ad-022.md +120 -0
- package/docs/decisions/ad-023.md +87 -0
- package/docs/decisions/ad-024.md +85 -0
- package/docs/decisions/ad-025.md +92 -0
- package/docs/decisions/ad-026.md +131 -0
- package/docs/decisions/ad-027.md +90 -0
- package/docs/decisions/ad-028.md +90 -0
- package/docs/decisions/ad-029.md +73 -0
- package/docs/decisions/ad-030.md +97 -0
- package/docs/decisions/ad-031.md +94 -0
- package/docs/decisions/ad-032.md +83 -0
- package/docs/decisions/ad-033.md +89 -0
- package/docs/decisions/ad-034.md +86 -0
- package/docs/decisions/ad-035.md +86 -0
- package/docs/decisions/ad-036.md +68 -0
- package/docs/decisions/ad-037.md +47 -0
- package/docs/decisions/ad-038.md +52 -0
- package/docs/decisions/ad-039.md +69 -0
- package/docs/decisions/ad-040.md +89 -0
- package/docs/decisions/ad-041.md +98 -0
- package/docs/decisions/ad-042.md +82 -0
- package/docs/decisions/ad-043.md +79 -0
- package/docs/decisions/ad-044.md +61 -0
- package/docs/decisions/ad-045.md +94 -0
- package/docs/decisions/ad-046.md +111 -0
- package/docs/decisions/ad-047.md +96 -0
- package/docs/decisions/ad-048.md +85 -0
- package/docs/decisions/ad-049.md +66 -0
- package/docs/decisions/ad-050.md +94 -0
- package/docs/decisions/ad-051.md +69 -0
- package/docs/decisions/ad-052.md +69 -0
- package/docs/decisions/ad-053.md +78 -0
- package/docs/decisions/ad-054.md +98 -0
- package/docs/decisions/ad-055.md +74 -0
- package/docs/decisions/ad-056.md +85 -0
- package/docs/decisions/ad-057.md +68 -0
- package/docs/decisions/ad-058.md +97 -0
- package/docs/decisions/ad-059.md +82 -0
- package/docs/decisions/ad-060.md +75 -0
- package/docs/decisions/ad-061.md +68 -0
- package/docs/decisions/ad-062.md +72 -0
- package/docs/decisions/ad-063.md +84 -0
- package/docs/decisions/ad-064.md +79 -0
- package/docs/decisions/ad-065.md +81 -0
- package/docs/decisions/ad-066.md +111 -0
- package/docs/decisions/ad-067.md +64 -0
- package/docs/decisions/ad-068.md +79 -0
- package/docs/decisions/ad-069.md +74 -0
- package/docs/decisions/ad-070.md +86 -0
- package/docs/decisions/ad-071.md +93 -0
- package/docs/decisions/ad-072.md +82 -0
- package/docs/decisions/ad-073.md +102 -0
- package/docs/decisions/ad-074.md +91 -0
- package/docs/decisions/ad-075.md +79 -0
- package/docs/decisions/ad-076.md +102 -0
- package/docs/decisions/ad-077.md +94 -0
- package/docs/decisions/ad-078.md +84 -0
- package/docs/decisions/ad-079.md +73 -0
- package/docs/decisions/ad-080.md +86 -0
- package/docs/decisions/ad-081.md +70 -0
- package/docs/decisions/ad-082.md +79 -0
- package/docs/decisions/ad-083.md +88 -0
- package/docs/decisions/index.md +111 -0
- package/docs/diagnose.md +248 -0
- package/docs/index.md +52 -0
- package/docs/init.md +101 -0
- package/docs/lessons.md +294 -0
- package/docs/log.md +139 -0
- package/docs/measure.md +178 -0
- package/docs/providers/claude-code.md +118 -0
- package/docs/providers/cursor.md +110 -0
- package/docs/providers/index.md +71 -0
- package/docs/troubleshooting.md +138 -0
- package/model-aliases.json +12 -0
- package/model-prices.cursor.json +410 -0
- package/model-prices.json +1 -0
- package/package.json +57 -0
- package/skills/harness-init/SKILL.md +241 -0
- package/skills/harness-init/evals/trigger_evals.json +86 -0
- package/skills/harness-init/references/capabilities.md +113 -0
- package/src/contracts/capabilities.ts +30 -0
- package/src/contracts/decision.ts +21 -0
- package/src/contracts/effort.ts +15 -0
- package/src/contracts/harness-event.ts +87 -0
- package/src/contracts/index.ts +6 -0
- package/src/contracts/policy-defaults.ts +14 -0
- package/src/contracts/wiring.ts +20 -0
- package/src/core/attest/attest.service.ts +0 -0
- package/src/core/capability/capability.service.ts +62 -0
- package/src/core/capability/capability.store.ts +51 -0
- package/src/core/capability/capability.types.ts +56 -0
- package/src/core/comment-policy/comment-policy.service.ts +307 -0
- package/src/core/comment-policy/comment-policy.types.ts +6 -0
- package/src/core/comment-policy/comment-resolvability.ts +134 -0
- package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
- package/src/core/comment-policy/comment-syntax.store.ts +65 -0
- package/src/core/comment-policy/comment-syntax.types.ts +24 -0
- package/src/core/core.facade.ts +461 -0
- package/src/core/duplication/duplication.service.ts +219 -0
- package/src/core/floor/floor.catalog.ts +54 -0
- package/src/core/floor/floor.paths.ts +90 -0
- package/src/core/floor/floor.policy-surface.ts +311 -0
- package/src/core/floor/floor.service.ts +306 -0
- package/src/core/floor/floor.tokenize.ts +179 -0
- package/src/core/floor/floor.verb.ts +28 -0
- package/src/core/floor/index.ts +1 -0
- package/src/core/gate/gate.artifact.ts +157 -0
- package/src/core/gate/gate.command.ts +130 -0
- package/src/core/gate/gate.findings.ts +201 -0
- package/src/core/gate/gate.inputs.ts +0 -0
- package/src/core/gate/gate.lock.ts +283 -0
- package/src/core/gate/gate.service.ts +28 -0
- package/src/core/gate/gate.types.ts +67 -0
- package/src/core/handoff/handoff.service.ts +43 -0
- package/src/core/handoff/handoff.store.ts +59 -0
- package/src/core/handoff/handoff.types.ts +89 -0
- package/src/core/index.ts +52 -0
- package/src/core/integrity/state-seal.ts +81 -0
- package/src/core/lesson/lesson.authored.ts +96 -0
- package/src/core/lesson/lesson.credit.ts +51 -0
- package/src/core/lesson/lesson.garden.ts +283 -0
- package/src/core/lesson/lesson.link.ts +93 -0
- package/src/core/lesson/lesson.score.ts +61 -0
- package/src/core/lesson/lesson.select.ts +209 -0
- package/src/core/lesson/lesson.service.ts +110 -0
- package/src/core/lesson/lesson.store.ts +317 -0
- package/src/core/lesson/lesson.sync.ts +70 -0
- package/src/core/lesson/lesson.types.ts +92 -0
- package/src/core/lesson/lesson.validity.ts +44 -0
- package/src/core/observability/observability.report.ts +326 -0
- package/src/core/observability/observability.service.ts +328 -0
- package/src/core/observability/observability.store.ts +269 -0
- package/src/core/observability/observability.types.ts +193 -0
- package/src/core/observability/observability.why.ts +206 -0
- package/src/core/observe/observe.service.ts +76 -0
- package/src/core/plan/plan.detect.ts +47 -0
- package/src/core/plan/plan.service.ts +81 -0
- package/src/core/plan/plan.types.ts +25 -0
- package/src/core/policy/policy.defaults.ts +103 -0
- package/src/core/policy/policy.guard.ts +34 -0
- package/src/core/policy/policy.integrity.ts +238 -0
- package/src/core/policy/policy.loader.ts +118 -0
- package/src/core/policy/policy.operator.ts +82 -0
- package/src/core/policy/policy.posture.ts +79 -0
- package/src/core/policy/policy.rails.ts +45 -0
- package/src/core/policy/policy.types.ts +157 -0
- package/src/core/presence/presence.service.ts +111 -0
- package/src/core/presence/presence.store.ts +59 -0
- package/src/core/presence/presence.types.ts +9 -0
- package/src/core/release/release.decisions.ts +147 -0
- package/src/core/release/release.seen.ts +38 -0
- package/src/core/shell-policy/shell-policy.service.ts +215 -0
- package/src/core/shell-policy/shell-policy.stall.ts +57 -0
- package/src/core/shell-policy/shell-policy.types.ts +26 -0
- package/src/core/ship/ship.ledger.ts +76 -0
- package/src/core/ship/ship.service.ts +137 -0
- package/src/core/ship/ship.types.ts +19 -0
- package/src/core/stagnation/stagnation.resolution.ts +87 -0
- package/src/core/stagnation/stagnation.service.ts +20 -0
- package/src/core/stagnation/stagnation.store.ts +49 -0
- package/src/core/stagnation/stagnation.types.ts +12 -0
- package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
- package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
- package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
- package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
- package/src/core/supply-chain/supply-chain.service.ts +217 -0
- package/src/core/turn/turn.activity.ts +94 -0
- package/src/core/turn/turn.autopilot.ts +131 -0
- package/src/core/turn/turn.failure-signals.ts +193 -0
- package/src/core/turn/turn.loop-counter.ts +71 -0
- package/src/core/turn/turn.types.ts +14 -0
- package/src/core/untrusted/untrusted.detect.ts +53 -0
- package/src/core/untrusted/untrusted.recall.ts +87 -0
- package/src/core/untrusted/untrusted.service.ts +129 -0
- package/src/core/untrusted/untrusted.store.ts +75 -0
- package/src/core/untrusted/untrusted.types.ts +42 -0
- package/src/entrypoints/compact-before.ts +14 -0
- package/src/entrypoints/prompt-submit.ts +25 -0
- package/src/entrypoints/response-after.ts +60 -0
- package/src/entrypoints/run.ts +179 -0
- package/src/entrypoints/session-end.ts +58 -0
- package/src/entrypoints/session-start.ts +172 -0
- package/src/entrypoints/shim.ts +46 -0
- package/src/entrypoints/stop.ts +876 -0
- package/src/entrypoints/subagent-start.ts +16 -0
- package/src/entrypoints/subagent-stop.ts +32 -0
- package/src/entrypoints/support.ts +182 -0
- package/src/entrypoints/tool-after.ts +111 -0
- package/src/entrypoints/tool-before.ts +186 -0
- package/src/entrypoints/tool-failure.ts +26 -0
- package/src/platform/backoff.ts +49 -0
- package/src/platform/cli-output.ts +30 -0
- package/src/platform/env-scope.ts +22 -0
- package/src/platform/fs-atomic.ts +142 -0
- package/src/platform/fs-jsonl.ts +24 -0
- package/src/platform/git.ts +187 -0
- package/src/platform/paths.ts +80 -0
- package/src/platform/pricing.ts +229 -0
- package/src/platform/process.ts +64 -0
- package/src/platform/sanitize.ts +23 -0
- package/src/platform/screen.ts +46 -0
- package/src/platform/style.ts +103 -0
- package/src/providers/claude/claude.capabilities.ts +25 -0
- package/src/providers/claude/claude.detect.ts +14 -0
- package/src/providers/claude/claude.inbound.ts +252 -0
- package/src/providers/claude/claude.lessons-view.ts +33 -0
- package/src/providers/claude/claude.outbound.ts +65 -0
- package/src/providers/claude/claude.policy-defaults.ts +9 -0
- package/src/providers/claude/claude.transcript.ts +62 -0
- package/src/providers/claude/claude.wiring.ts +245 -0
- package/src/providers/claude/index.ts +17 -0
- package/src/providers/cursor/cursor.capabilities.ts +28 -0
- package/src/providers/cursor/cursor.detect.ts +14 -0
- package/src/providers/cursor/cursor.inbound.ts +203 -0
- package/src/providers/cursor/cursor.lessons-view.ts +30 -0
- package/src/providers/cursor/cursor.outbound.ts +43 -0
- package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +214 -0
- package/src/providers/cursor/index.ts +17 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider.degrade.ts +152 -0
- package/src/providers/provider.port.ts +20 -0
- package/src/providers/provider.registry.ts +28 -0
- package/tools/doctor.ts +631 -0
- package/tools/help-topic.ts +74 -0
- package/tools/init-project.ts +277 -0
- package/tools/install-runtime.ts +164 -0
- package/tools/lessons-cli.ts +474 -0
- package/tools/obs-cli.ts +175 -0
- package/tools/price-lookup.ts +57 -0
- package/tools/refresh-model-prices.ts +210 -0
- package/tools/test-env.mjs +28 -0
- package/tools/test-env.names.d.mts +1 -0
- package/tools/test-env.names.mjs +14 -0
- package/tools/uninstall-runtime.ts +463 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
4
|
+
|
|
5
|
+
export async function readStdinText(): Promise<string> {
|
|
6
|
+
const chunks: Buffer[] = [];
|
|
7
|
+
for await (const chunk of process.stdin) {
|
|
8
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
9
|
+
}
|
|
10
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runProcess(args: {
|
|
14
|
+
command: string[];
|
|
15
|
+
cwd?: string;
|
|
16
|
+
input?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
20
|
+
const [file, ...argv] = args.command;
|
|
21
|
+
if (file === undefined) {
|
|
22
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
23
|
+
}
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn(file, argv, {
|
|
26
|
+
cwd: args.cwd,
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"] as const,
|
|
28
|
+
env: args.env ?? process.env,
|
|
29
|
+
});
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
const timer =
|
|
34
|
+
args.timeoutMs === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: setTimeout(() => {
|
|
37
|
+
timedOut = true;
|
|
38
|
+
child.kill("SIGKILL");
|
|
39
|
+
}, args.timeoutMs);
|
|
40
|
+
|
|
41
|
+
child.stdout.on("data", (c: Buffer) => {
|
|
42
|
+
stdout += c.toString();
|
|
43
|
+
});
|
|
44
|
+
child.stderr.on("data", (c: Buffer) => {
|
|
45
|
+
stderr += c.toString();
|
|
46
|
+
});
|
|
47
|
+
child.on("error", (error) => {
|
|
48
|
+
if (timer) clearTimeout(timer);
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
child.on("close", (code) => {
|
|
52
|
+
if (timer) clearTimeout(timer);
|
|
53
|
+
if (timedOut) {
|
|
54
|
+
resolve({ exitCode: TIMEOUT_EXIT_CODE, stdout, stderr: `${stderr}\n(process timed out)` });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
58
|
+
});
|
|
59
|
+
if (args.input !== undefined) {
|
|
60
|
+
child.stdin.write(args.input);
|
|
61
|
+
}
|
|
62
|
+
child.stdin.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const ALLOWED = /[A-Za-z0-9._-]/;
|
|
2
|
+
const EMPTY_PLACEHOLDER = "_empty_";
|
|
3
|
+
|
|
4
|
+
export function sanitizeSegment(input: string): string {
|
|
5
|
+
if (input.length === 0) {
|
|
6
|
+
return EMPTY_PLACEHOLDER;
|
|
7
|
+
}
|
|
8
|
+
let out = "";
|
|
9
|
+
for (const ch of input) {
|
|
10
|
+
if (ALLOWED.test(ch)) {
|
|
11
|
+
out += ch;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
for (const byte of Buffer.from(ch, "utf8")) {
|
|
15
|
+
out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeSeparators(input: string): string {
|
|
22
|
+
return input.replace(/\\/g, "/");
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { KV_WIDTH, type StatusLevel, type Style, SYMBOLS } from "./style.ts";
|
|
2
|
+
|
|
3
|
+
export type Row = { label: string; value: string; level?: StatusLevel };
|
|
4
|
+
|
|
5
|
+
export type Section = { title?: string; rows?: Row[]; lines?: string[] };
|
|
6
|
+
|
|
7
|
+
export type Screen = {
|
|
8
|
+
title: string;
|
|
9
|
+
summary?: string[];
|
|
10
|
+
sections: Section[];
|
|
11
|
+
footer?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// why: screens describe their content and never their paint, so spacing, colour and alignment are decided once.
|
|
15
|
+
// A new screen can only emit this shape, which is what makes "no screen outside the standard" mechanical.
|
|
16
|
+
export function render(screen: Screen, style: Style): string {
|
|
17
|
+
const out: string[] = [style.heading(screen.title.toUpperCase())];
|
|
18
|
+
|
|
19
|
+
if (screen.summary && screen.summary.length > 0) {
|
|
20
|
+
out.push(` ${screen.summary.join(style.dim(` ${SYMBOLS.bar} `))}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const width = Math.max(
|
|
24
|
+
KV_WIDTH,
|
|
25
|
+
...screen.sections.flatMap((section) => (section.rows ?? []).map((row) => row.label.length + 1)),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const section of screen.sections) {
|
|
29
|
+
out.push("");
|
|
30
|
+
if (section.title) {
|
|
31
|
+
out.push(style.paint("accent", section.title));
|
|
32
|
+
}
|
|
33
|
+
for (const row of section.rows ?? []) {
|
|
34
|
+
const value = row.level ? style.status(row.level, row.value) : row.value;
|
|
35
|
+
out.push(style.kv(row.label, value, width));
|
|
36
|
+
}
|
|
37
|
+
for (const line of section.lines ?? []) {
|
|
38
|
+
out.push(line === "" ? "" : ` ${line}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (screen.footer) {
|
|
43
|
+
out.push("", style.footer(screen.footer));
|
|
44
|
+
}
|
|
45
|
+
return out.join("\n");
|
|
46
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const COLORS = {
|
|
2
|
+
structure: "#3d3a4a",
|
|
3
|
+
accent: "#a78bfa",
|
|
4
|
+
success: "#6ee7b7",
|
|
5
|
+
warning: "#d4a574",
|
|
6
|
+
error: "#f87171",
|
|
7
|
+
info: "#93c5fd",
|
|
8
|
+
textMain: "#f5f5f7",
|
|
9
|
+
textMuted: "#9ca3af",
|
|
10
|
+
textDim: "#6b7280",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export const SYMBOLS = {
|
|
14
|
+
check: "✔",
|
|
15
|
+
cross: "✖",
|
|
16
|
+
warning: "⚠",
|
|
17
|
+
arrow: "→",
|
|
18
|
+
arrowRight: "▸",
|
|
19
|
+
dot: "•",
|
|
20
|
+
bar: "│",
|
|
21
|
+
rule: "══",
|
|
22
|
+
dash: "──",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
function rgb(hex: string): string {
|
|
26
|
+
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
|
|
27
|
+
if (!match) {
|
|
28
|
+
return "255;255;255";
|
|
29
|
+
}
|
|
30
|
+
return [match[1], match[2], match[3]].map((part) => Number.parseInt(part as string, 16)).join(";");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ESC = String.fromCharCode(27);
|
|
34
|
+
const RESET = `${ESC}[0m`;
|
|
35
|
+
|
|
36
|
+
export type ColorName = keyof typeof COLORS;
|
|
37
|
+
|
|
38
|
+
// hazard: the TTY check is load-bearing, not politeness. This CLI's output is redirected into files, quoted into
|
|
39
|
+
// gate follow-ups and read by the repository's own checkers, and an escape reaching any of those already sent an
|
|
40
|
+
// agent to fix `39msrc/…`, a file that does not exist.
|
|
41
|
+
export function colorEnabled(
|
|
42
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
43
|
+
argv: readonly string[] = process.argv,
|
|
44
|
+
isTty: boolean = process.stdout.isTTY === true,
|
|
45
|
+
): boolean {
|
|
46
|
+
if ("NO_COLOR" in env) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (argv.includes("--no-color")) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return isTty;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type StatusLevel = "ok" | "warn" | "fail" | "info";
|
|
56
|
+
|
|
57
|
+
export type Style = {
|
|
58
|
+
paint: (name: ColorName, text: string) => string;
|
|
59
|
+
bold: (text: string) => string;
|
|
60
|
+
dim: (text: string) => string;
|
|
61
|
+
heading: (text: string) => string;
|
|
62
|
+
footer: (text: string) => string;
|
|
63
|
+
// why: one shape for a label/value row and one for a status line, so a new screen inherits the standard instead
|
|
64
|
+
// of inventing spacing ([/decisions/ad-063.md](/decisions/ad-063.md)).
|
|
65
|
+
kv: (label: string, value: string, width?: number) => string;
|
|
66
|
+
status: (level: StatusLevel, text: string) => string;
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const STATUS_COLOR: Record<StatusLevel, ColorName> = {
|
|
71
|
+
ok: "success",
|
|
72
|
+
warn: "warning",
|
|
73
|
+
fail: "error",
|
|
74
|
+
info: "info",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const STATUS_MARK: Record<StatusLevel, string> = {
|
|
78
|
+
ok: SYMBOLS.check,
|
|
79
|
+
warn: SYMBOLS.warning,
|
|
80
|
+
fail: SYMBOLS.cross,
|
|
81
|
+
info: SYMBOLS.arrowRight,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const KV_WIDTH = 16;
|
|
85
|
+
|
|
86
|
+
// why: truecolor written directly because this package has no runtime dependency and ships no binary
|
|
87
|
+
// ([/decisions/ad-012.md](/decisions/ad-012.md)).
|
|
88
|
+
export function createStyle(enabled = colorEnabled()): Style {
|
|
89
|
+
const wrap = (code: string, text: string) => (enabled ? `${ESC}[${code}m${text}${RESET}` : text);
|
|
90
|
+
const paint = (name: ColorName, text: string) => wrap(`38;2;${rgb(COLORS[name])}`, text);
|
|
91
|
+
return {
|
|
92
|
+
enabled,
|
|
93
|
+
paint,
|
|
94
|
+
bold: (text) => wrap("1", text),
|
|
95
|
+
dim: (text) => paint("textDim", text),
|
|
96
|
+
heading: (text) => paint("accent", `${SYMBOLS.rule} ${text} ${SYMBOLS.rule}`),
|
|
97
|
+
footer: (text) => paint("textDim", `${SYMBOLS.dash} ${text} ${SYMBOLS.dash}`),
|
|
98
|
+
kv: (label, value, width = KV_WIDTH) => ` ${paint("textMuted", `${label}:`.padEnd(width))} ${value}`,
|
|
99
|
+
status: (level, text) => `${paint(STATUS_COLOR[level], STATUS_MARK[level])} ${text}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const PLAIN: Style = createStyle(false);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProviderCapabilities } from "../../contracts/index.ts";
|
|
2
|
+
|
|
3
|
+
export function claudeCapabilities(): ProviderCapabilities {
|
|
4
|
+
return {
|
|
5
|
+
enforcesHooks: true,
|
|
6
|
+
askSupportedOn: ["tool.before", "shell.before", "mcp.before", "read.before"],
|
|
7
|
+
sessionEnv: false,
|
|
8
|
+
nativeLoopCounter: false,
|
|
9
|
+
dedicatedShellEvent: false,
|
|
10
|
+
toolInputRewrite: true,
|
|
11
|
+
toolOutputRewrite: true,
|
|
12
|
+
contextAtToolBefore: true,
|
|
13
|
+
contextAtToolAfter: true,
|
|
14
|
+
// why: `Stop` accepts `hookSpecificOutput.additionalContext` for feedback that continues the turn, which is
|
|
15
|
+
// exactly what a non-blocking gate advisory is.
|
|
16
|
+
contextAtStop: true,
|
|
17
|
+
// why: `SessionStart` delivers `hookSpecificOutput.additionalContext`, capped at 10,000 characters. Only that
|
|
18
|
+
// field — a top-level `additional_context` is read as well and not deduplicated, so emitting both injects twice.
|
|
19
|
+
sessionStartContextReliable: true,
|
|
20
|
+
toolOutputAtAfter: true,
|
|
21
|
+
usageInPayload: false,
|
|
22
|
+
effortSignal: true,
|
|
23
|
+
thoughtEvent: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// why: Claude payloads use PascalCase hook_event_name + cwd/transcript_path; Cursor payloads don't.
|
|
2
|
+
const PASCAL_CASE_EVENT_NAME = /^[A-Z][a-zA-Z0-9]*$/;
|
|
3
|
+
|
|
4
|
+
export function detectClaude(raw: unknown): boolean {
|
|
5
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const value = raw as Record<string, unknown>;
|
|
9
|
+
const eventName = value.hook_event_name;
|
|
10
|
+
if (typeof eventName !== "string" || !PASCAL_CASE_EVENT_NAME.test(eventName)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return typeof value.cwd === "string" || typeof value.transcript_path === "string";
|
|
14
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { isEffortLevel } from "../../contracts/effort.ts";
|
|
2
|
+
import type { EffortLevel, HarnessEvent, HarnessEventKind } from "../../contracts/index.ts";
|
|
3
|
+
import { sanitizeSegment } from "../../platform/sanitize.ts";
|
|
4
|
+
|
|
5
|
+
const EVENT_KIND_BY_HOOK: Record<string, HarnessEventKind> = {
|
|
6
|
+
SessionStart: "session.start",
|
|
7
|
+
SessionEnd: "session.end",
|
|
8
|
+
UserPromptSubmit: "prompt.submit",
|
|
9
|
+
PostToolUseFailure: "tool.failure",
|
|
10
|
+
SubagentStart: "subagent.start",
|
|
11
|
+
SubagentStop: "subagent.stop",
|
|
12
|
+
Stop: "stop",
|
|
13
|
+
PreCompact: "compact.before",
|
|
14
|
+
MessageDisplay: "response.after",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MCP_TOOL_NAME = /^mcp__/;
|
|
18
|
+
|
|
19
|
+
function asString(value: unknown): string | undefined {
|
|
20
|
+
return typeof value === "string" ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asNumber(value: unknown): number | undefined {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
28
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
29
|
+
? (value as Record<string, unknown>)
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asStatus(value: unknown): "completed" | "aborted" | "error" | undefined {
|
|
34
|
+
return value === "completed" || value === "aborted" || value === "error" ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sessionKeyFor(raw: Record<string, unknown>): string {
|
|
38
|
+
const seed = asString(raw.session_id) ?? "default";
|
|
39
|
+
return `claude-${sanitizeSegment(seed)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function projectDirFor(raw: Record<string, unknown>): string {
|
|
43
|
+
const envDir = process.env.CLAUDE_PROJECT_DIR;
|
|
44
|
+
if (envDir) {
|
|
45
|
+
return envDir;
|
|
46
|
+
}
|
|
47
|
+
const cwd = asString(raw.cwd);
|
|
48
|
+
if (cwd) {
|
|
49
|
+
return cwd;
|
|
50
|
+
}
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function effortFor(raw: Record<string, unknown>): EffortLevel | undefined {
|
|
55
|
+
const effort = asRecord(raw.effort);
|
|
56
|
+
const level = effort?.level;
|
|
57
|
+
return isEffortLevel(level) ? level : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// why: PreToolUse fans out by tool_name — Claude has no dedicated shell/MCP/read event.
|
|
61
|
+
function preToolUseKind(toolName: string | undefined): HarnessEventKind {
|
|
62
|
+
if (toolName === "Bash") {
|
|
63
|
+
return "shell.before";
|
|
64
|
+
}
|
|
65
|
+
if (toolName && MCP_TOOL_NAME.test(toolName)) {
|
|
66
|
+
return "mcp.before";
|
|
67
|
+
}
|
|
68
|
+
if (toolName === "Read") {
|
|
69
|
+
return "read.before";
|
|
70
|
+
}
|
|
71
|
+
return "tool.before";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function postToolUseKind(toolName: string | undefined): HarnessEventKind {
|
|
75
|
+
if (toolName === "Bash") {
|
|
76
|
+
return "shell.after";
|
|
77
|
+
}
|
|
78
|
+
if (toolName && MCP_TOOL_NAME.test(toolName)) {
|
|
79
|
+
return "mcp.after";
|
|
80
|
+
}
|
|
81
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
82
|
+
return "edit.after";
|
|
83
|
+
}
|
|
84
|
+
return "tool.after";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Never throws on a malformed payload — returns null instead. */
|
|
88
|
+
export function claudeToEvent(raw: Record<string, unknown>): HarnessEvent | null {
|
|
89
|
+
const hookEventName = asString(raw.hook_event_name);
|
|
90
|
+
if (!hookEventName) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const toolName = asString(raw.tool_name);
|
|
95
|
+
const toolInput = asRecord(raw.tool_input);
|
|
96
|
+
|
|
97
|
+
let eventKind: HarnessEventKind | undefined;
|
|
98
|
+
if (hookEventName === "PreToolUse") {
|
|
99
|
+
eventKind = preToolUseKind(toolName);
|
|
100
|
+
} else if (hookEventName === "PostToolUse") {
|
|
101
|
+
eventKind = postToolUseKind(toolName);
|
|
102
|
+
} else {
|
|
103
|
+
eventKind = EVENT_KIND_BY_HOOK[hookEventName];
|
|
104
|
+
}
|
|
105
|
+
if (!eventKind) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const event: HarnessEvent = {
|
|
110
|
+
provider: "claude",
|
|
111
|
+
event: eventKind,
|
|
112
|
+
sessionKey: sessionKeyFor(raw),
|
|
113
|
+
projectDir: projectDirFor(raw),
|
|
114
|
+
raw,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const permissionMode = asString(raw.permission_mode);
|
|
118
|
+
if (permissionMode) {
|
|
119
|
+
event.permissionMode = permissionMode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isSpawnEvent = eventKind === "subagent.start" || eventKind === "subagent.stop";
|
|
123
|
+
const model = isSpawnEvent ? undefined : asString(raw.model);
|
|
124
|
+
if (model) {
|
|
125
|
+
event.model = model;
|
|
126
|
+
}
|
|
127
|
+
const effort = effortFor(raw);
|
|
128
|
+
if (effort) {
|
|
129
|
+
event.effort = effort;
|
|
130
|
+
}
|
|
131
|
+
const contextUsagePercent = asNumber(raw.context_usage_percent);
|
|
132
|
+
if (contextUsagePercent !== undefined) {
|
|
133
|
+
event.contextUsagePercent = contextUsagePercent;
|
|
134
|
+
}
|
|
135
|
+
const transcriptPath = asString(raw.transcript_path);
|
|
136
|
+
if (transcriptPath) {
|
|
137
|
+
event.transcriptPath = transcriptPath;
|
|
138
|
+
}
|
|
139
|
+
if (!isSpawnEvent) {
|
|
140
|
+
const callerAgentType = asString(raw.agent_type);
|
|
141
|
+
if (callerAgentType) {
|
|
142
|
+
event.subagentType = callerAgentType;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
switch (eventKind) {
|
|
147
|
+
case "prompt.submit": {
|
|
148
|
+
const text = asString(raw.prompt);
|
|
149
|
+
if (text !== undefined) {
|
|
150
|
+
event.text = text;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "response.after": {
|
|
155
|
+
const text = asString(raw.text);
|
|
156
|
+
if (text !== undefined) {
|
|
157
|
+
event.text = text;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case "shell.before":
|
|
162
|
+
case "shell.after": {
|
|
163
|
+
const command = toolInput ? asString(toolInput.command) : undefined;
|
|
164
|
+
if (command !== undefined) {
|
|
165
|
+
event.command = command;
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "mcp.before":
|
|
170
|
+
case "mcp.after": {
|
|
171
|
+
if (toolName) {
|
|
172
|
+
event.toolName = toolName;
|
|
173
|
+
}
|
|
174
|
+
if (toolInput) {
|
|
175
|
+
event.toolInput = toolInput;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "read.before": {
|
|
180
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
181
|
+
if (filePath !== undefined) {
|
|
182
|
+
event.filePath = filePath;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "edit.after": {
|
|
187
|
+
if (toolName) {
|
|
188
|
+
event.toolName = toolName;
|
|
189
|
+
}
|
|
190
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
191
|
+
if (filePath !== undefined) {
|
|
192
|
+
event.filePath = filePath;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "tool.before":
|
|
197
|
+
case "tool.after":
|
|
198
|
+
case "tool.failure": {
|
|
199
|
+
if (toolName) {
|
|
200
|
+
event.toolName = toolName;
|
|
201
|
+
}
|
|
202
|
+
// why: an object here, unlike the two string fields Cursor uses. Serialising is the translation this layer
|
|
203
|
+
// exists to do, and core reads one shape ([/decisions/ad-004.md](/decisions/ad-004.md)).
|
|
204
|
+
const toolOutput = raw.tool_response;
|
|
205
|
+
if (toolOutput !== undefined && toolOutput !== null) {
|
|
206
|
+
event.toolOutput = typeof toolOutput === "string" ? toolOutput : JSON.stringify(toolOutput);
|
|
207
|
+
}
|
|
208
|
+
if (toolInput) {
|
|
209
|
+
event.toolInput = toolInput;
|
|
210
|
+
}
|
|
211
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
212
|
+
if (filePath !== undefined) {
|
|
213
|
+
event.filePath = filePath;
|
|
214
|
+
}
|
|
215
|
+
const spawnSubagentType = toolInput ? asString(toolInput.subagent_type) : undefined;
|
|
216
|
+
if (spawnSubagentType) {
|
|
217
|
+
event.spawnSubagentType = spawnSubagentType;
|
|
218
|
+
}
|
|
219
|
+
const spawnModel = toolInput ? asString(toolInput.model) : undefined;
|
|
220
|
+
if (spawnModel) {
|
|
221
|
+
event.spawnModel = spawnModel;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "subagent.start":
|
|
226
|
+
case "subagent.stop": {
|
|
227
|
+
const spawnSubagentType =
|
|
228
|
+
asString(raw.agent_type) ??
|
|
229
|
+
asString(raw.subagent_type) ??
|
|
230
|
+
(toolInput ? asString(toolInput.subagent_type) : undefined);
|
|
231
|
+
if (spawnSubagentType) {
|
|
232
|
+
event.spawnSubagentType = spawnSubagentType;
|
|
233
|
+
}
|
|
234
|
+
const spawnModel = (toolInput ? asString(toolInput.model) : undefined) ?? asString(raw.model);
|
|
235
|
+
if (spawnModel) {
|
|
236
|
+
event.spawnModel = spawnModel;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "stop": {
|
|
241
|
+
const status = asStatus(raw.status);
|
|
242
|
+
if (status) {
|
|
243
|
+
event.status = status;
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
default:
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return event;
|
|
252
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { projectConfigPath } from "../../platform/paths.ts";
|
|
4
|
+
|
|
5
|
+
const IMPORT_LINE = "@.tlc/harness/lessons.md";
|
|
6
|
+
|
|
7
|
+
// why: mirrors core/lesson/lesson.garden.ts's lessonsMarkdownPath — providers cannot import core, so the join is duplicated.
|
|
8
|
+
export function claudeLessonsSourcePath(root: string): string {
|
|
9
|
+
return join(dirname(projectConfigPath(root)), "lessons.md");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function claudeMdPath(root: string): string {
|
|
13
|
+
return join(root, "CLAUDE.md");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderClaudeLessonsView(root: string): string | null {
|
|
17
|
+
if (!existsSync(claudeLessonsSourcePath(root))) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const path = claudeMdPath(root);
|
|
22
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
23
|
+
const alreadyImported = existing.split("\n").some((line) => line.trim() === IMPORT_LINE);
|
|
24
|
+
if (alreadyImported) {
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
29
|
+
const content = existing.length > 0 ? `${existing}${separator}\n${IMPORT_LINE}\n` : `${IMPORT_LINE}\n`;
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
writeFileSync(path, content, "utf8");
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Decision, HarnessEvent, HarnessEventKind, Rendered } from "../../contracts/index.ts";
|
|
2
|
+
|
|
3
|
+
// why: Claude has no dedicated thought event (thoughtEvent: false) — this entry is never reached.
|
|
4
|
+
const HOOK_EVENT_NAME_BY_KIND: Record<HarnessEventKind, string> = {
|
|
5
|
+
"session.start": "SessionStart",
|
|
6
|
+
"session.end": "SessionEnd",
|
|
7
|
+
"prompt.submit": "UserPromptSubmit",
|
|
8
|
+
"tool.before": "PreToolUse",
|
|
9
|
+
"tool.after": "PostToolUse",
|
|
10
|
+
"tool.failure": "PostToolUseFailure",
|
|
11
|
+
"shell.before": "PreToolUse",
|
|
12
|
+
"shell.after": "PostToolUse",
|
|
13
|
+
"mcp.before": "PreToolUse",
|
|
14
|
+
"mcp.after": "PostToolUse",
|
|
15
|
+
"read.before": "PreToolUse",
|
|
16
|
+
"edit.after": "PostToolUse",
|
|
17
|
+
"subagent.start": "SubagentStart",
|
|
18
|
+
"subagent.stop": "SubagentStop",
|
|
19
|
+
stop: "Stop",
|
|
20
|
+
"compact.before": "PreCompact",
|
|
21
|
+
"response.after": "MessageDisplay",
|
|
22
|
+
"thought.after": "MessageDisplay",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function renderPermission(
|
|
26
|
+
permissionDecision: "allow" | "deny" | "ask",
|
|
27
|
+
hookEventName: string,
|
|
28
|
+
reason: string | undefined,
|
|
29
|
+
): string {
|
|
30
|
+
const hookSpecificOutput: Record<string, unknown> = { hookEventName, permissionDecision };
|
|
31
|
+
if (reason !== undefined) {
|
|
32
|
+
hookSpecificOutput.permissionDecisionReason = reason;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify({ hookSpecificOutput });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function claudeRender(decision: Decision, event: HarnessEvent): Rendered {
|
|
38
|
+
const hookEventName = HOOK_EVENT_NAME_BY_KIND[event.event];
|
|
39
|
+
switch (decision.kind) {
|
|
40
|
+
case "abstain":
|
|
41
|
+
return { stdout: null, exitCode: 0 };
|
|
42
|
+
case "allow":
|
|
43
|
+
return { stdout: renderPermission("allow", hookEventName, undefined), exitCode: 0 };
|
|
44
|
+
case "deny":
|
|
45
|
+
return { stdout: renderPermission("deny", hookEventName, decision.reason), exitCode: 0 };
|
|
46
|
+
case "ask":
|
|
47
|
+
return { stdout: renderPermission("ask", hookEventName, decision.reason), exitCode: 0 };
|
|
48
|
+
case "context":
|
|
49
|
+
return {
|
|
50
|
+
stdout: JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: decision.text } }),
|
|
51
|
+
exitCode: 0,
|
|
52
|
+
};
|
|
53
|
+
case "continue":
|
|
54
|
+
return { stdout: JSON.stringify({ decision: "block", reason: decision.text }), exitCode: 0 };
|
|
55
|
+
case "rewriteInput":
|
|
56
|
+
return {
|
|
57
|
+
stdout: JSON.stringify({ hookSpecificOutput: { hookEventName, updatedInput: decision.input } }),
|
|
58
|
+
exitCode: 0,
|
|
59
|
+
};
|
|
60
|
+
default: {
|
|
61
|
+
const exhaustive: never = decision;
|
|
62
|
+
throw new Error(`unreachable decision kind: ${JSON.stringify(exhaustive)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|