@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,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { runtimeHome } from "../src/platform/paths.ts";
|
|
4
|
+
|
|
5
|
+
const docsDir = join(runtimeHome(), "docs");
|
|
6
|
+
const topic = (process.argv[2] ?? "").toLowerCase();
|
|
7
|
+
|
|
8
|
+
const TOPICS: Record<string, string> = {
|
|
9
|
+
architecture: "architecture.md",
|
|
10
|
+
concepts: "concepts.md",
|
|
11
|
+
measure: "measure.md",
|
|
12
|
+
metrics: "measure.md",
|
|
13
|
+
prices: "measure.md",
|
|
14
|
+
price: "measure.md",
|
|
15
|
+
cost: "measure.md",
|
|
16
|
+
costs: "measure.md",
|
|
17
|
+
diagnose: "diagnose.md",
|
|
18
|
+
doctor: "diagnose.md",
|
|
19
|
+
debug: "diagnose.md",
|
|
20
|
+
init: "init.md",
|
|
21
|
+
setup: "init.md",
|
|
22
|
+
lessons: "lessons.md",
|
|
23
|
+
lesson: "lessons.md",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function printIndex(): void {
|
|
27
|
+
console.log(`tlc harness help
|
|
28
|
+
|
|
29
|
+
TOPICS
|
|
30
|
+
tlc harness help architecture
|
|
31
|
+
tlc harness help concepts
|
|
32
|
+
tlc harness help lessons
|
|
33
|
+
tlc harness help measure
|
|
34
|
+
tlc harness help prices
|
|
35
|
+
tlc harness help diagnose
|
|
36
|
+
tlc harness help init
|
|
37
|
+
|
|
38
|
+
PRICES
|
|
39
|
+
tlc harness prices refresh [all|cursor|litellm]
|
|
40
|
+
tlc harness prices lookup <model-id>
|
|
41
|
+
|
|
42
|
+
ALSO
|
|
43
|
+
tlc harness status | doctor | grind | mode | obs | lessons
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!topic || topic === "help" || topic === "-h" || topic === "--help") {
|
|
48
|
+
printIndex();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const file = TOPICS[topic];
|
|
53
|
+
if (!file) {
|
|
54
|
+
console.error(`unknown topic: ${topic}`);
|
|
55
|
+
printIndex();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const path = join(docsDir, file);
|
|
60
|
+
if (!existsSync(path)) {
|
|
61
|
+
console.error(`missing doc: ${path}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let body = readFileSync(path, "utf8");
|
|
66
|
+
if (topic === "prices" || topic === "price" || topic === "cost" || topic === "costs") {
|
|
67
|
+
const marker = "## Prices";
|
|
68
|
+
const idx = body.indexOf(marker);
|
|
69
|
+
if (idx >= 0) {
|
|
70
|
+
body = `# Prices\n\n${body.slice(idx)}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { applyCursorWiring, renderCursorHooksDocument } from "../bin/write-user-hooks.mjs";
|
|
4
|
+
import type { WiringEntry } from "../src/contracts/index.ts";
|
|
5
|
+
import { DEFAULTS } from "../src/core/policy/policy.defaults.ts";
|
|
6
|
+
import { claudeConfigDir, cursorConfigDir, projectConfigPath, runtimeHome } from "../src/platform/paths.ts";
|
|
7
|
+
import { render, type Screen } from "../src/platform/screen.ts";
|
|
8
|
+
import { PLAIN, type Style } from "../src/platform/style.ts";
|
|
9
|
+
import { applyClaudeWiring } from "../src/providers/claude/claude.wiring.ts";
|
|
10
|
+
|
|
11
|
+
export class UsageError extends Error {}
|
|
12
|
+
|
|
13
|
+
export type InitFlags = {
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
write: boolean;
|
|
16
|
+
minimal: boolean;
|
|
17
|
+
stdinJson: boolean;
|
|
18
|
+
force: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function parseFlags(args: readonly string[]): InitFlags {
|
|
22
|
+
return {
|
|
23
|
+
dryRun: args.includes("--dry-run"),
|
|
24
|
+
write: args.includes("--write") || args.includes("--minimal"),
|
|
25
|
+
minimal: args.includes("--minimal"),
|
|
26
|
+
stdinJson: args.includes("--stdin-json"),
|
|
27
|
+
force: args.includes("--force"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function usageScreen(): Screen {
|
|
32
|
+
return {
|
|
33
|
+
title: "harness init",
|
|
34
|
+
sections: [
|
|
35
|
+
{
|
|
36
|
+
lines: ` tlc harness init --dry-run
|
|
37
|
+
tlc harness init --write [--stdin-json] [--force]
|
|
38
|
+
tlc harness init --minimal
|
|
39
|
+
|
|
40
|
+
--minimal writes a safe agnostic stub (grind/ship off). Prefer the harness-init skill for full discovery.`.split(
|
|
41
|
+
"\n",
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// why: plain by default and never given a style here — the only caller throws it as a UsageError, and an error
|
|
49
|
+
// message is printed on a path that may be redirected.
|
|
50
|
+
export function usageText(style: Style = PLAIN): string {
|
|
51
|
+
return render(usageScreen(), style);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function launcherPath(home = runtimeHome()): string {
|
|
55
|
+
return join(home, "bin", "tlc-exec.mjs");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shimCommand(platform = process.platform): { command: string; argsPrefix: string[] } {
|
|
59
|
+
if (platform === "win32") {
|
|
60
|
+
return { command: "cmd", argsPrefix: ["/c", "node"] };
|
|
61
|
+
}
|
|
62
|
+
return { command: "node", argsPrefix: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type ShimSpec = {
|
|
66
|
+
hookEvent: string;
|
|
67
|
+
handler: string;
|
|
68
|
+
timeoutSeconds: number;
|
|
69
|
+
loopLimit?: number;
|
|
70
|
+
matcher?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const CURSOR_SHIM_SPECS: readonly ShimSpec[] = [
|
|
74
|
+
{ hookEvent: "sessionStart", handler: "session-start", timeoutSeconds: 10 },
|
|
75
|
+
{ hookEvent: "sessionEnd", handler: "session-end", timeoutSeconds: 10 },
|
|
76
|
+
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 10 },
|
|
77
|
+
{ hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10 },
|
|
78
|
+
{ hookEvent: "beforeMCPExecution", handler: "tool-before", timeoutSeconds: 10 },
|
|
79
|
+
{ hookEvent: "beforeReadFile", handler: "tool-before", timeoutSeconds: 5 },
|
|
80
|
+
{ hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5 },
|
|
81
|
+
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
82
|
+
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const CLAUDE_SHIM_SPECS: readonly ShimSpec[] = [
|
|
86
|
+
{ hookEvent: "SessionStart", handler: "session-start", timeoutSeconds: 10 },
|
|
87
|
+
{ hookEvent: "SessionEnd", handler: "session-end", timeoutSeconds: 10 },
|
|
88
|
+
{ hookEvent: "PreToolUse", handler: "tool-before", timeoutSeconds: 10 },
|
|
89
|
+
{ hookEvent: "SubagentStart", handler: "subagent-start", timeoutSeconds: 5 },
|
|
90
|
+
{ hookEvent: "Stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
91
|
+
{ hookEvent: "MessageDisplay", handler: "response-after", timeoutSeconds: 5 },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function cursorShimEntries(launcher: string): WiringEntry[] {
|
|
95
|
+
const { command, argsPrefix } = shimCommand();
|
|
96
|
+
return CURSOR_SHIM_SPECS.map((spec) => ({
|
|
97
|
+
hookEvent: spec.hookEvent,
|
|
98
|
+
handler: spec.handler,
|
|
99
|
+
command,
|
|
100
|
+
args: [...argsPrefix, launcher, "shim", spec.handler],
|
|
101
|
+
timeoutSeconds: spec.timeoutSeconds,
|
|
102
|
+
...(spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}),
|
|
103
|
+
...(spec.matcher !== undefined ? { matcher: spec.matcher } : {}),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function claudeShimEntries(launcher: string): WiringEntry[] {
|
|
108
|
+
return CLAUDE_SHIM_SPECS.map((spec) => ({
|
|
109
|
+
hookEvent: spec.hookEvent,
|
|
110
|
+
handler: spec.handler,
|
|
111
|
+
command: "node",
|
|
112
|
+
args: [launcher, "shim", spec.handler],
|
|
113
|
+
timeoutSeconds: spec.timeoutSeconds,
|
|
114
|
+
...(spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const GITIGNORE_LINE = ".tlc/harness/state/";
|
|
119
|
+
|
|
120
|
+
export function mergeGitignore(root: string): { text: string; changed: boolean } {
|
|
121
|
+
const path = join(root, ".gitignore");
|
|
122
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
123
|
+
const lines = existing.split("\n");
|
|
124
|
+
const alreadyPresent = lines.includes(GITIGNORE_LINE);
|
|
125
|
+
if (alreadyPresent) {
|
|
126
|
+
return { text: existing.endsWith("\n") || existing === "" ? existing : `${existing}\n`, changed: false };
|
|
127
|
+
}
|
|
128
|
+
lines.push(GITIGNORE_LINE);
|
|
129
|
+
const withoutTrailingBlank = lines.filter((line, index, all) => line.length > 0 || index < all.length - 1);
|
|
130
|
+
return { text: `${withoutTrailingBlank.join("\n").replace(/\n+$/, "")}\n`, changed: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolvePolicy(root: string, flags: InitFlags, stdinText: string | null): unknown {
|
|
134
|
+
if (flags.stdinJson && !flags.minimal) {
|
|
135
|
+
if (!stdinText || stdinText.trim() === "") {
|
|
136
|
+
throw new Error("stdin-json: empty stdin");
|
|
137
|
+
}
|
|
138
|
+
return JSON.parse(stdinText);
|
|
139
|
+
}
|
|
140
|
+
if (!flags.minimal && !flags.stdinJson && existsSync(projectConfigPath(root))) {
|
|
141
|
+
return JSON.parse(readFileSync(projectConfigPath(root), "utf8"));
|
|
142
|
+
}
|
|
143
|
+
return DEFAULTS;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type ProviderPresence = { cursor: boolean; claude: boolean };
|
|
147
|
+
|
|
148
|
+
export function detectProviders(dirs: { cursor?: string; claude?: string } = {}): ProviderPresence {
|
|
149
|
+
return {
|
|
150
|
+
cursor: existsSync(dirs.cursor ?? cursorConfigDir()),
|
|
151
|
+
claude: existsSync(dirs.claude ?? claudeConfigDir()),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type InitPlan = {
|
|
156
|
+
policy: unknown;
|
|
157
|
+
cursorHooksDocument: unknown | null;
|
|
158
|
+
claudeHooksPreview: WiringEntry[] | null;
|
|
159
|
+
gitignoreLine: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export function buildPlan(
|
|
163
|
+
root: string,
|
|
164
|
+
flags: InitFlags,
|
|
165
|
+
stdinText: string | null,
|
|
166
|
+
presence: ProviderPresence,
|
|
167
|
+
): InitPlan {
|
|
168
|
+
const policy = resolvePolicy(root, flags, stdinText);
|
|
169
|
+
const launcher = launcherPath();
|
|
170
|
+
return {
|
|
171
|
+
policy,
|
|
172
|
+
cursorHooksDocument: presence.cursor ? renderCursorHooksDocument(cursorShimEntries(launcher)) : null,
|
|
173
|
+
claudeHooksPreview: presence.claude ? claudeShimEntries(launcher) : null,
|
|
174
|
+
gitignoreLine: GITIGNORE_LINE,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type ApplyOutcome = {
|
|
179
|
+
configPath: string;
|
|
180
|
+
cursor: { skipped: true } | { skipped: false; status: string; target: string };
|
|
181
|
+
claude: { skipped: true } | { skipped: false; status: string; target: string };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export function applyPlan(
|
|
185
|
+
root: string,
|
|
186
|
+
flags: InitFlags,
|
|
187
|
+
presence: ProviderPresence,
|
|
188
|
+
stdinText: string | null,
|
|
189
|
+
): ApplyOutcome {
|
|
190
|
+
const policy = resolvePolicy(root, flags, stdinText);
|
|
191
|
+
const configPath = projectConfigPath(root);
|
|
192
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
193
|
+
writeFileSync(configPath, `${JSON.stringify(policy, null, 2)}\n`);
|
|
194
|
+
|
|
195
|
+
const launcher = launcherPath();
|
|
196
|
+
|
|
197
|
+
const cursor = presence.cursor
|
|
198
|
+
? (() => {
|
|
199
|
+
const result = applyCursorWiring(
|
|
200
|
+
{
|
|
201
|
+
target: join(root, ".cursor", "hooks.json"),
|
|
202
|
+
strategy: "replace",
|
|
203
|
+
entries: cursorShimEntries(launcher),
|
|
204
|
+
},
|
|
205
|
+
{ force: flags.force },
|
|
206
|
+
);
|
|
207
|
+
return { skipped: false as const, status: result.status, target: result.target };
|
|
208
|
+
})()
|
|
209
|
+
: { skipped: true as const };
|
|
210
|
+
|
|
211
|
+
const claude = presence.claude
|
|
212
|
+
? (() => {
|
|
213
|
+
const result = applyClaudeWiring(join(root, ".claude", "settings.json"), claudeShimEntries(launcher));
|
|
214
|
+
return {
|
|
215
|
+
skipped: false as const,
|
|
216
|
+
status: result.ok ? (result.changed ? "written" : "unchanged") : "failed",
|
|
217
|
+
target: join(root, ".claude", "settings.json"),
|
|
218
|
+
};
|
|
219
|
+
})()
|
|
220
|
+
: { skipped: true as const };
|
|
221
|
+
|
|
222
|
+
const gitignore = mergeGitignore(root);
|
|
223
|
+
writeFileSync(join(root, ".gitignore"), gitignore.text);
|
|
224
|
+
|
|
225
|
+
return { configPath, cursor, claude };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function readStdin(): Promise<string> {
|
|
229
|
+
const chunks: Buffer[] = [];
|
|
230
|
+
for await (const chunk of process.stdin) {
|
|
231
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
232
|
+
}
|
|
233
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function main(argv: string[]): Promise<void> {
|
|
237
|
+
const root = process.env.TLC_PROJECT_DIR ?? process.cwd();
|
|
238
|
+
const flags = parseFlags(argv);
|
|
239
|
+
|
|
240
|
+
if (!flags.dryRun && !flags.write) {
|
|
241
|
+
throw new UsageError(usageText());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const stdinText = flags.stdinJson ? await readStdin() : null;
|
|
245
|
+
const presence = detectProviders();
|
|
246
|
+
|
|
247
|
+
if (flags.dryRun) {
|
|
248
|
+
console.log(JSON.stringify(buildPlan(root, flags, stdinText, presence), null, 2));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const outcome = applyPlan(root, flags, presence, stdinText);
|
|
253
|
+
console.log(`wrote ${outcome.configPath}`);
|
|
254
|
+
if (outcome.cursor.skipped) {
|
|
255
|
+
console.log("init: cursor not installed — skipped project hooks.json");
|
|
256
|
+
} else {
|
|
257
|
+
console.log(`hooks: ${outcome.cursor.status} ${outcome.cursor.target}`);
|
|
258
|
+
}
|
|
259
|
+
if (outcome.claude.skipped) {
|
|
260
|
+
console.log("init: claude not installed — skipped project settings.json");
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`hooks: ${outcome.claude.status} ${outcome.claude.target}`);
|
|
263
|
+
}
|
|
264
|
+
console.log("updated .gitignore harness entries");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (import.meta.main) {
|
|
268
|
+
try {
|
|
269
|
+
await main(process.argv.slice(2));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof UsageError) {
|
|
272
|
+
console.error(error.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { NPM_MARKER, NPM_PACKAGE } from "../bin/tlc-cli.ts";
|
|
4
|
+
import { conventionalRuntimeHome, runtimeHome, runtimeHomeWasChosen } from "../src/platform/paths.ts";
|
|
5
|
+
import { type Row, render, type Screen } from "../src/platform/screen.ts";
|
|
6
|
+
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* why: an npm-installed copy lives under a directory npm replaces wholesale, so the runtime cannot keep its state
|
|
10
|
+
* there — measured on the packed tarball, which wrote `runtime-cache.json` inside the package on its first run.
|
|
11
|
+
* The package is the delivery vehicle; the runtime that hooks name stays at the conventional home, and this is
|
|
12
|
+
* what puts the code there ([/decisions/ad-056.md](/decisions/ad-056.md)).
|
|
13
|
+
*
|
|
14
|
+
* invariant: an entry here is replaced wholesale, so a file deleted upstream does not survive the update. Nothing
|
|
15
|
+
* outside this list is touched, which is what keeps `config.json`, `state/` and `flags/` the operator's.
|
|
16
|
+
*/
|
|
17
|
+
export const RUNTIME_PAYLOAD = [
|
|
18
|
+
"bin",
|
|
19
|
+
"capabilities",
|
|
20
|
+
"dist",
|
|
21
|
+
"docs",
|
|
22
|
+
"skills",
|
|
23
|
+
"src",
|
|
24
|
+
"tools",
|
|
25
|
+
"config.example.json",
|
|
26
|
+
"model-aliases.json",
|
|
27
|
+
"model-prices.cursor.json",
|
|
28
|
+
"model-prices.json",
|
|
29
|
+
"package.json",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/** Never copied and never removed. The reason the split exists. */
|
|
33
|
+
export const OPERATOR_OWNED = ["config.json", "state", "flags"] as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inside a payload entry and still not shipped.
|
|
37
|
+
*
|
|
38
|
+
* why: `tools/dev` holds the checks that validate *this* repository — its module boundaries, its screen contract,
|
|
39
|
+
* its decision records. A user's install has none of that to validate, and with Bun present the launcher resolves
|
|
40
|
+
* an entry straight from source, so copying them would put runnable repo-only commands on their machine. The
|
|
41
|
+
* clone route is different on purpose: a checkout is the repository, and a contributor needs them
|
|
42
|
+
* ([/decisions/ad-068.md](/decisions/ad-068.md)).
|
|
43
|
+
*/
|
|
44
|
+
export const NOT_SHIPPED = [join("tools", "dev"), join("tools", "__test__")] as const;
|
|
45
|
+
|
|
46
|
+
export function isShipped(relativePath: string): boolean {
|
|
47
|
+
const normalised = relativePath.split(sep).join("/");
|
|
48
|
+
return !NOT_SHIPPED.some((excluded) => {
|
|
49
|
+
const prefix = excluded.split(sep).join("/");
|
|
50
|
+
return normalised === prefix || normalised.startsWith(`${prefix}/`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type InstallReport = {
|
|
55
|
+
kind: "copied" | "in-place";
|
|
56
|
+
source: string;
|
|
57
|
+
dest: string;
|
|
58
|
+
entries: string[];
|
|
59
|
+
missing: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The physical location of the copy that launched us, which is not the home once an npm shim is driving one. */
|
|
63
|
+
export function originRoot(env: NodeJS.ProcessEnv = process.env): string {
|
|
64
|
+
const declared = env.TLC_ORIGIN?.trim();
|
|
65
|
+
if (declared && declared.length > 0) {
|
|
66
|
+
return resolve(declared);
|
|
67
|
+
}
|
|
68
|
+
// hazard: `runtimeHome()` reads `process.env` and would ignore the env passed in, which made this untestable
|
|
69
|
+
// and would have read the wrong home for any caller that supplies one.
|
|
70
|
+
const home = env.TLC_HOME?.trim();
|
|
71
|
+
return home && home.length > 0 ? resolve(home) : conventionalRuntimeHome();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function installRuntime(source: string, dest: string): InstallReport {
|
|
75
|
+
if (resolve(source) === resolve(dest)) {
|
|
76
|
+
// why: the git route already has the code at the destination. Copying a directory onto itself is the one
|
|
77
|
+
// input that turns a sync into data loss.
|
|
78
|
+
return { kind: "in-place", source, dest, entries: [], missing: [] };
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(dest, { recursive: true });
|
|
81
|
+
const entries: string[] = [];
|
|
82
|
+
const missing: string[] = [];
|
|
83
|
+
for (const entry of RUNTIME_PAYLOAD) {
|
|
84
|
+
const from = join(source, entry);
|
|
85
|
+
if (!existsSync(from)) {
|
|
86
|
+
missing.push(entry);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const to = join(dest, entry);
|
|
90
|
+
rmSync(to, { recursive: true, force: true });
|
|
91
|
+
cpSync(from, to, {
|
|
92
|
+
recursive: true,
|
|
93
|
+
filter: (src) => isShipped(relative(source, src)),
|
|
94
|
+
});
|
|
95
|
+
entries.push(entry);
|
|
96
|
+
}
|
|
97
|
+
// why: a directory with no `.git` used to classify as `unmanaged`, which doctor reports as a failure and update
|
|
98
|
+
// answers with "re-install with the one-liner". The marker is how the thing that created this directory says
|
|
99
|
+
// what it is, rather than leaving every later reader to guess from the contents.
|
|
100
|
+
writeFileSync(
|
|
101
|
+
join(dest, NPM_MARKER),
|
|
102
|
+
`Installed by \`tlc harness install\` from ${source}.\nUpdate with: npm i -g ${NPM_PACKAGE}@latest && tlc harness install\n`,
|
|
103
|
+
"utf8",
|
|
104
|
+
);
|
|
105
|
+
const config = join(dest, "config.json");
|
|
106
|
+
const example = join(dest, "config.example.json");
|
|
107
|
+
if (!existsSync(config) && existsSync(example)) {
|
|
108
|
+
writeFileSync(config, readFileSync(example, "utf8"), "utf8");
|
|
109
|
+
}
|
|
110
|
+
return { kind: "copied", source, dest, entries, missing };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function installScreen(report: InstallReport): Screen {
|
|
114
|
+
if (report.kind === "in-place") {
|
|
115
|
+
return {
|
|
116
|
+
title: "harness install",
|
|
117
|
+
sections: [
|
|
118
|
+
{ rows: [{ label: "runtime", value: `already at ${report.dest} — nothing to copy`, level: "ok" }] },
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const rows: Row[] = [
|
|
123
|
+
{ label: "installed", value: `${report.entries.length} path(s) → ${report.dest}`, level: "ok" },
|
|
124
|
+
{ label: "from", value: report.source },
|
|
125
|
+
];
|
|
126
|
+
if (report.missing.length > 0) {
|
|
127
|
+
// why: a payload entry absent from the source is a packaging fault, not a passing install. `tools/` was
|
|
128
|
+
// missing from the published `files` list the first time this ran.
|
|
129
|
+
rows.push({
|
|
130
|
+
label: "packaging",
|
|
131
|
+
value: `MISSING from the source: ${report.missing.join(", ")}`,
|
|
132
|
+
level: "fail",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return { title: "harness install", sections: [{ rows }] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function installReportText(report: InstallReport, style: Style = PLAIN): string {
|
|
139
|
+
return render(installScreen(report), style);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* hazard: not the resolved home. On the very first npm run nothing is installed yet, so the resolved home *is*
|
|
144
|
+
* the package — and asking it where to install would answer "here", which is the one place that cannot hold
|
|
145
|
+
* state. The conventional path is the answer unless the operator named one themselves.
|
|
146
|
+
*/
|
|
147
|
+
export function installDest(env: NodeJS.ProcessEnv = process.env): string {
|
|
148
|
+
// hazard: the destination cannot be carried on `TLC_HOME`. Setting that makes the launcher resolve the runtime
|
|
149
|
+
// to a directory that does not exist yet on a first install, and it then refuses for want of the bundles it was
|
|
150
|
+
// being asked to put there. A separate variable keeps resolution and destination independent.
|
|
151
|
+
const explicit = env.TLC_INSTALL_DEST?.trim();
|
|
152
|
+
if (explicit && explicit.length > 0) {
|
|
153
|
+
return resolve(explicit);
|
|
154
|
+
}
|
|
155
|
+
return runtimeHomeWasChosen(env) ? runtimeHome(env) : conventionalRuntimeHome();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (import.meta.main) {
|
|
159
|
+
const source = originRoot();
|
|
160
|
+
const dest = installDest();
|
|
161
|
+
const report = installRuntime(source, dest);
|
|
162
|
+
console.log(installReportText(report, createStyle()));
|
|
163
|
+
process.exit(report.missing.length > 0 ? 1 : 0);
|
|
164
|
+
}
|