@tech-leads-club/harness-toolkit 0.10.6 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/bin/tlc-cli.ts +60 -1
- package/capabilities/catalog.json +18 -1
- package/dist/compact-before.mjs +79 -78
- package/dist/doctor.mjs +83 -82
- package/dist/init-project.mjs +86 -85
- package/dist/install-runtime.mjs +80 -79
- package/dist/lessons-cli.mjs +82 -81
- package/dist/new-provider.mjs +126 -0
- package/dist/obs-cli.mjs +83 -82
- package/dist/prompt-submit.mjs +79 -78
- package/dist/refresh-model-prices.mjs +80 -79
- package/dist/response-after.mjs +80 -79
- package/dist/run.mjs +79 -78
- package/dist/session-end.mjs +85 -84
- package/dist/session-start.mjs +87 -86
- package/dist/shim.mjs +81 -80
- package/dist/ship-gate.mjs +79 -78
- package/dist/stop.mjs +86 -85
- package/dist/subagent-start.mjs +79 -78
- package/dist/subagent-stop.mjs +80 -79
- package/dist/support.mjs +84 -83
- package/dist/tlc-cli.mjs +225 -97
- package/dist/tool-after.mjs +80 -79
- package/dist/tool-before.mjs +82 -81
- package/dist/tool-failure.mjs +79 -78
- package/dist/uninstall-runtime.mjs +3 -3
- package/docs/architecture.md +16 -3
- package/docs/concepts.md +11 -0
- package/docs/log.md +4 -0
- package/docs/providers/claude-code.md +42 -21
- package/docs/providers/cursor.md +23 -7
- package/docs/troubleshooting.md +1 -0
- package/package.json +1 -1
- package/schema.json +12 -0
- package/skills/harness-init/references/capabilities.md +1 -0
- package/src/contracts/capabilities.ts +8 -1
- package/src/contracts/decision.ts +2 -1
- package/src/core/core.facade.ts +6 -0
- package/src/core/floor/floor.catalog.ts +5 -0
- package/src/core/floor/floor.paths.ts +17 -1
- package/src/core/floor/floor.policy-surface.ts +26 -9
- package/src/core/floor/floor.service.ts +37 -5
- package/src/core/policy/policy.defaults.ts +3 -0
- package/src/core/policy/policy.loader.ts +1 -0
- package/src/core/policy/policy.types.ts +4 -0
- package/src/core/secret-scan/secret-scan.service.ts +96 -0
- package/src/core/secret-scan/secret-scan.store.ts +56 -0
- package/src/entrypoints/run.ts +3 -1
- package/src/entrypoints/tool-after.ts +20 -0
- package/src/entrypoints/tool-before.ts +1 -0
- package/src/providers/claude/claude.capabilities.ts +3 -1
- package/src/providers/claude/claude.inbound.ts +37 -22
- package/src/providers/claude/claude.outbound.ts +4 -0
- package/src/providers/claude/claude.wiring.ts +4 -0
- package/src/providers/claude/index.ts +2 -1
- package/src/providers/cursor/cursor.capabilities.ts +3 -1
- package/src/providers/cursor/cursor.inbound.ts +1 -1
- package/src/providers/cursor/cursor.outbound.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +9 -1
- package/src/providers/cursor/index.ts +2 -1
- package/src/providers/provider.degrade.ts +19 -0
- package/src/providers/provider.port.ts +1 -0
- package/tools/new-provider.ts +251 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// hazard: this file lived at tools/dev/new-provider.ts (two levels below root) until it moved to tools/ —
|
|
6
|
+
// tools/dev/ is excluded from the published package, and bin/tlc-cli.ts's `new-provider` subcommand imports
|
|
7
|
+
// this module at runtime, so a real install had no file to import at all.
|
|
8
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
|
|
10
|
+
// invariant: mirrors provider.contract.test.ts's own safety check ("provider name is safe as a state-file
|
|
11
|
+
// key segment") — no whitespace, no slashes. Reserved names collide with this scaffold's own file layout
|
|
12
|
+
// (`index.ts`) or the generic module concept ("provider").
|
|
13
|
+
const UNSAFE_NAME = /[\s/\\]/;
|
|
14
|
+
const RESERVED_NAMES = new Set(["index", "provider"]);
|
|
15
|
+
|
|
16
|
+
export type NameValidity = { ok: true } | { ok: false; reason: string };
|
|
17
|
+
|
|
18
|
+
export function validateProviderName(name: string): NameValidity {
|
|
19
|
+
if (name.length === 0) {
|
|
20
|
+
return { ok: false, reason: "provider name must not be empty" };
|
|
21
|
+
}
|
|
22
|
+
if (UNSAFE_NAME.test(name)) {
|
|
23
|
+
return { ok: false, reason: "provider name must not contain whitespace or a slash" };
|
|
24
|
+
}
|
|
25
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) {
|
|
26
|
+
return { ok: false, reason: `"${name}" is a reserved name` };
|
|
27
|
+
}
|
|
28
|
+
return { ok: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function capitalize(name: string): string {
|
|
32
|
+
return name.length === 0 ? name : `${name[0]?.toUpperCase()}${name.slice(1)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function detectStub(name: string): string {
|
|
36
|
+
const cap = capitalize(name);
|
|
37
|
+
return `// why: always false until real detection logic replaces it, so the provider stays inert.
|
|
38
|
+
export function detect${cap}(_raw: unknown): boolean {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function capabilitiesStub(name: string): string {
|
|
45
|
+
return `import type { HarnessEventKind, ProviderCapabilities } from "../../contracts/index.ts";
|
|
46
|
+
|
|
47
|
+
export function ${name}Capabilities(): ProviderCapabilities {
|
|
48
|
+
return {
|
|
49
|
+
enforcesHooks: true,
|
|
50
|
+
askSupportedOn: [],
|
|
51
|
+
sessionEnv: false,
|
|
52
|
+
nativeLoopCounter: false,
|
|
53
|
+
dedicatedShellEvent: false,
|
|
54
|
+
toolInputRewrite: false,
|
|
55
|
+
// why: a placeholder, deliberately not a real HarnessEventKind — fails assertSatisfiesContract until replaced.
|
|
56
|
+
toolOutputRewriteOn: ["TODO-fill-in-real-capabilities" as unknown as HarnessEventKind],
|
|
57
|
+
contextAtToolBefore: false,
|
|
58
|
+
contextAtToolAfter: false,
|
|
59
|
+
contextAtStop: false,
|
|
60
|
+
sessionStartContextReliable: false,
|
|
61
|
+
toolOutputAtAfter: false,
|
|
62
|
+
usageInPayload: false,
|
|
63
|
+
effortSignal: false,
|
|
64
|
+
thoughtEvent: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function policyDefaultsStub(name: string): string {
|
|
71
|
+
return `import type { ProviderPolicyDefaults } from "../../contracts/index.ts";
|
|
72
|
+
|
|
73
|
+
export function ${name}PolicyDefaults(): ProviderPolicyDefaults {
|
|
74
|
+
return {
|
|
75
|
+
blockedPatterns: [],
|
|
76
|
+
minEffort: null,
|
|
77
|
+
untrustedTools: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inboundStub(name: string): string {
|
|
84
|
+
return `import type { HarnessEvent, HarnessEventKind } from "../../contracts/index.ts";
|
|
85
|
+
|
|
86
|
+
// why: empty until this host's own hook names are mapped, so every payload falls through to null.
|
|
87
|
+
export const EVENT_KIND_BY_HOOK: Record<string, HarnessEventKind> = {};
|
|
88
|
+
|
|
89
|
+
/** Never throws on a malformed payload — returns null instead. */
|
|
90
|
+
export function ${name}ToEvent(_raw: Record<string, unknown>): HarnessEvent | null {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function wiringStub(name: string): string {
|
|
97
|
+
return `import type { ProviderWiring, RuntimePaths } from "../../contracts/index.ts";
|
|
98
|
+
|
|
99
|
+
// why: a placeholder path, until the real file this provider's host reads hooks from is named.
|
|
100
|
+
const WIRING_TARGET = "TODO: replace with the real wiring target path";
|
|
101
|
+
|
|
102
|
+
export function ${name}WiringTargets(): string[] {
|
|
103
|
+
return [WIRING_TARGET];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// why: an empty entries list, until this adapter's real hook entries replace it (see claude.wiring.ts's ENTRY_SPECS shape).
|
|
107
|
+
export function ${name}Wiring(_runtime: RuntimePaths): ProviderWiring {
|
|
108
|
+
return { target: WIRING_TARGET, strategy: "replace", entries: [] };
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function outboundStub(name: string): string {
|
|
114
|
+
return `import type { Decision, HarnessEvent, Rendered } from "../../contracts/index.ts";
|
|
115
|
+
|
|
116
|
+
// why: an unconditional abstain, until this host's real hook response schema replaces it.
|
|
117
|
+
export function ${name}Render(_decision: Decision, _event: HarnessEvent): Rendered {
|
|
118
|
+
return { stdout: null, exitCode: 0 };
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function indexStub(name: string): string {
|
|
124
|
+
const cap = capitalize(name);
|
|
125
|
+
return `import type { ProviderPort } from "../provider.port.ts";
|
|
126
|
+
import { ${name}Capabilities } from "./${name}.capabilities.ts";
|
|
127
|
+
import { detect${cap} } from "./${name}.detect.ts";
|
|
128
|
+
import { ${name}ToEvent } from "./${name}.inbound.ts";
|
|
129
|
+
import { ${name}Render } from "./${name}.outbound.ts";
|
|
130
|
+
import { ${name}PolicyDefaults } from "./${name}.policy-defaults.ts";
|
|
131
|
+
import { ${name}Wiring, ${name}WiringTargets } from "./${name}.wiring.ts";
|
|
132
|
+
|
|
133
|
+
// why: assembled here so \`tlc harness new-provider\` has one ProviderPort to import into provider.registry.ts.
|
|
134
|
+
export const ${name}Provider: ProviderPort = {
|
|
135
|
+
name: "${name}",
|
|
136
|
+
detect: detect${cap},
|
|
137
|
+
capabilities: ${name}Capabilities,
|
|
138
|
+
policyDefaults: ${name}PolicyDefaults,
|
|
139
|
+
toEvent: ${name}ToEvent,
|
|
140
|
+
render: ${name}Render,
|
|
141
|
+
wiring: ${name}Wiring,
|
|
142
|
+
wiringTargets: ${name}WiringTargets,
|
|
143
|
+
};
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function docSkeleton(name: string): string {
|
|
148
|
+
const cap = capitalize(name);
|
|
149
|
+
const timestamp = new Date().toISOString().slice(0, 10);
|
|
150
|
+
return `---
|
|
151
|
+
type: Provider
|
|
152
|
+
title: "${cap} provider"
|
|
153
|
+
description: "The ${cap} adapter — capability descriptor, event mapping, and wiring target."
|
|
154
|
+
tags: [provider, ${name}]
|
|
155
|
+
timestamp: "${timestamp}"
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
# ${cap} provider
|
|
159
|
+
|
|
160
|
+
Source: \`src/providers/${name}/\`.
|
|
161
|
+
|
|
162
|
+
## Detection
|
|
163
|
+
|
|
164
|
+
TODO: describe \`${name}.detect.ts\`'s real detection rule.
|
|
165
|
+
|
|
166
|
+
## Capability descriptor
|
|
167
|
+
|
|
168
|
+
\`${name}.capabilities.ts\`:
|
|
169
|
+
|
|
170
|
+
<!-- generated:capabilities -->
|
|
171
|
+
|
|
172
|
+
<!-- /generated -->
|
|
173
|
+
|
|
174
|
+
## Policy defaults
|
|
175
|
+
|
|
176
|
+
TODO: describe \`${name}.policy-defaults.ts\`'s defaults.
|
|
177
|
+
|
|
178
|
+
## Event mapping
|
|
179
|
+
|
|
180
|
+
\`${name}.inbound.ts\` maps ${cap}'s own hook names to \`HarnessEventKind\`:
|
|
181
|
+
|
|
182
|
+
<!-- generated:event-mapping -->
|
|
183
|
+
|
|
184
|
+
<!-- /generated -->
|
|
185
|
+
|
|
186
|
+
## Wiring target
|
|
187
|
+
|
|
188
|
+
TODO: describe \`${name}.wiring.ts\`'s wiring target and strategy.
|
|
189
|
+
|
|
190
|
+
## See also
|
|
191
|
+
|
|
192
|
+
- [/providers/index.md](/providers/index.md)
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export type ScaffoldFile = { path: string; content: string };
|
|
197
|
+
|
|
198
|
+
export function scaffoldFiles(name: string): ScaffoldFile[] {
|
|
199
|
+
const dir = join("src", "providers", name);
|
|
200
|
+
return [
|
|
201
|
+
{ path: join(dir, `${name}.detect.ts`), content: detectStub(name) },
|
|
202
|
+
{ path: join(dir, `${name}.capabilities.ts`), content: capabilitiesStub(name) },
|
|
203
|
+
{ path: join(dir, `${name}.policy-defaults.ts`), content: policyDefaultsStub(name) },
|
|
204
|
+
{ path: join(dir, `${name}.inbound.ts`), content: inboundStub(name) },
|
|
205
|
+
{ path: join(dir, `${name}.wiring.ts`), content: wiringStub(name) },
|
|
206
|
+
// why: SPEC_DEVIATION — emits 8 files, not the 6 named in design.md/tasks.md, adding `<name>.outbound.ts` and
|
|
207
|
+
// `index.ts`. Reason: ProviderPort requires a `render` field and an assembled object, the same two-extra-file
|
|
208
|
+
// shape claude/index.ts and cursor/index.ts already use — without them there is no ProviderPort value for T6
|
|
209
|
+
// to register or T7 to run through assertSatisfiesContract (see commit 20da58e).
|
|
210
|
+
{ path: join(dir, `${name}.outbound.ts`), content: outboundStub(name) },
|
|
211
|
+
{ path: join(dir, "index.ts"), content: indexStub(name) },
|
|
212
|
+
{ path: join("docs", "providers", `${name}.md`), content: docSkeleton(name) },
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function scaffold(name: string, root = repoRoot): NameValidity {
|
|
217
|
+
const validity = validateProviderName(name);
|
|
218
|
+
if (!validity.ok) {
|
|
219
|
+
return validity;
|
|
220
|
+
}
|
|
221
|
+
const providerDir = join(root, "src", "providers", name);
|
|
222
|
+
if (existsSync(providerDir)) {
|
|
223
|
+
return { ok: false, reason: `${join("src", "providers", name)} already exists` };
|
|
224
|
+
}
|
|
225
|
+
for (const file of scaffoldFiles(name)) {
|
|
226
|
+
const fullPath = join(root, file.path);
|
|
227
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
228
|
+
writeFileSync(fullPath, file.content, "utf8");
|
|
229
|
+
}
|
|
230
|
+
return { ok: true };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function main(argv: string[], root = repoRoot): void {
|
|
234
|
+
const name = argv[0];
|
|
235
|
+
if (!name) {
|
|
236
|
+
console.error("usage: node tools/dev/new-provider.ts <name>");
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const result = scaffold(name, root);
|
|
241
|
+
if (!result.ok) {
|
|
242
|
+
console.error(`new-provider: refusing — ${result.reason}`);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(`new-provider: scaffolded src/providers/${name}/ and docs/providers/${name}.md`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (import.meta.main) {
|
|
250
|
+
main(process.argv.slice(2));
|
|
251
|
+
}
|