@tech-leads-club/harness-toolkit 0.5.1 → 0.7.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/bin/generate-schema.ts +67 -0
- package/bin/tlc-build.mjs +26 -1
- package/bin/tlc-cli.ts +43 -11
- package/config.example.json +1 -0
- package/dist/compact-before.mjs +83 -83
- package/dist/doctor.mjs +79 -79
- package/dist/init-project.mjs +87 -87
- package/dist/install-runtime.mjs +82 -82
- package/dist/lessons-cli.mjs +86 -86
- package/dist/obs-cli.mjs +79 -79
- package/dist/prompt-submit.mjs +83 -83
- package/dist/refresh-model-prices.mjs +82 -82
- package/dist/response-after.mjs +83 -83
- package/dist/run.mjs +83 -83
- package/dist/session-end.mjs +89 -89
- package/dist/session-start.mjs +91 -91
- package/dist/shim.mjs +81 -81
- package/dist/stop.mjs +89 -89
- package/dist/subagent-start.mjs +83 -83
- package/dist/subagent-stop.mjs +84 -84
- package/dist/support.mjs +87 -87
- package/dist/tlc-cli.mjs +103 -102
- package/dist/tool-after.mjs +83 -83
- package/dist/tool-before.mjs +85 -85
- package/dist/tool-failure.mjs +83 -83
- package/dist/uninstall-runtime.mjs +4 -4
- package/docs/concepts.md +4 -1
- package/docs/log.md +8 -0
- package/package.json +4 -2
- package/schema.json +545 -0
- package/src/core/capability/capability.store.ts +27 -1
- package/src/core/comment-policy/comment-policy.service.ts +3 -8
- package/src/core/core.facade.ts +7 -1
- package/src/core/floor/floor.policy-surface.ts +1 -1
- package/src/core/handoff/handoff.service.ts +30 -0
- package/src/core/handoff/handoff.types.ts +0 -2
- package/src/core/lesson/lesson.types.ts +5 -0
- package/src/core/policy/policy.loader.ts +5 -1
- package/src/core/policy/policy.shadow.ts +68 -2
- package/src/core/turn/turn.failure-signals.ts +3 -1
- package/src/entrypoints/stop.ts +16 -14
- package/src/entrypoints/subagent-stop.ts +8 -6
- package/tools/doctor.ts +48 -0
- package/tools/init-project.ts +19 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import * as TJS from "typescript-json-schema";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* why: `PartialPolicy`, not `Policy` — a human-written config is always partial, and `Policy`'s every
|
|
7
|
+
* field being non-optional would mark a minimal config invalid in every editor.
|
|
8
|
+
*/
|
|
9
|
+
const ROOT_TYPE = "PartialPolicy";
|
|
10
|
+
const SOURCE_FILE = "src/core/policy/policy.types.ts";
|
|
11
|
+
|
|
12
|
+
function compilerOptionsFrom(root: string): Record<string, unknown> {
|
|
13
|
+
const raw = JSON.parse(readFileSync(join(root, "tsconfig.json"), "utf8")) as {
|
|
14
|
+
compilerOptions: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
return raw.compilerOptions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* invariant: `noExtraProps` is what makes an unknown key (the `format` class of bug) a schema
|
|
21
|
+
* violation instead of something the schema silently accepts.
|
|
22
|
+
*/
|
|
23
|
+
export function generateConfigSchema(rawRoot: string): Record<string, unknown> {
|
|
24
|
+
// why: TypeScript prints an import() type query against its own canonicalised path — a `.` segment
|
|
25
|
+
// or backslashes in the caller's spelling would silently defeat redactBuildPath's string match below.
|
|
26
|
+
const root = resolve(rawRoot);
|
|
27
|
+
const program = TJS.getProgramFromFiles([join(root, SOURCE_FILE)], compilerOptionsFrom(root), root);
|
|
28
|
+
const schema = TJS.generateSchema(program, ROOT_TYPE, {
|
|
29
|
+
required: true,
|
|
30
|
+
noExtraProps: true,
|
|
31
|
+
strictNullChecks: true,
|
|
32
|
+
});
|
|
33
|
+
if (schema === null) {
|
|
34
|
+
throw new Error(`typescript-json-schema produced no schema for ${ROOT_TYPE} in ${SOURCE_FILE}`);
|
|
35
|
+
}
|
|
36
|
+
const properties = (schema as { properties?: Record<string, unknown> }).properties ?? {};
|
|
37
|
+
const withSchemaProp = {
|
|
38
|
+
...schema,
|
|
39
|
+
properties: {
|
|
40
|
+
// why: a JSON Schema meta-key, not a PartialPolicy field — typescript-json-schema never emits
|
|
41
|
+
// it, and `noExtraProps` would otherwise make every real config with a `$schema` line invalid.
|
|
42
|
+
$schema: { type: "string" },
|
|
43
|
+
...properties,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return redactBuildPath(withSchemaProp, root);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* hazard: a field typed `Partial<Policy["grind"]>` (an indexed-access type, not its own named alias)
|
|
51
|
+
* has no clean name to give its `$ref`, so the generator falls back to printing the full structural
|
|
52
|
+
* type — including an `import("<absolute path>")` type query for every cross-file reference inside
|
|
53
|
+
* it. That absolute path is the machine that ran the build, encoded twice: once raw in `$ref` targets
|
|
54
|
+
* that are also definition keys, once URI-percent-encoded in the `$ref` string itself. Published
|
|
55
|
+
* as-is, it would leak the CI runner's (or a contributor's) filesystem layout into a public schema.
|
|
56
|
+
*/
|
|
57
|
+
// why: TypeScript always prints module specifiers with forward slashes, regardless of host OS — on
|
|
58
|
+
// Windows `root` itself is backslash-separated, so the raw string alone never matches what got printed.
|
|
59
|
+
function redactBuildPath(schema: Record<string, unknown>, root: string): Record<string, unknown> {
|
|
60
|
+
const posixRoot = root.replace(/\\/g, "/");
|
|
61
|
+
const candidates = [root, posixRoot, encodeURIComponent(root), encodeURIComponent(posixRoot)];
|
|
62
|
+
let serialized = JSON.stringify(schema);
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
serialized = serialized.split(candidate).join(".");
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(serialized) as Record<string, unknown>;
|
|
67
|
+
}
|
package/bin/tlc-build.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* entrypoint, and the missing bundle only surfaces when a hook fires on somebody's machine.
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
19
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { basename, dirname, join } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
|
|
@@ -115,3 +115,28 @@ for (const entry of readdirSync(dist, { withFileTypes: true })) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
console.log(`tlc-build: ok (${bundles} bundles)`);
|
|
118
|
+
|
|
119
|
+
// why: dynamic, and a missing module degrades rather than fails — typescript-json-schema is a
|
|
120
|
+
// devDependency, absent from a plain install, and the dist rebuild above is this script's real
|
|
121
|
+
// recovery contract (bin/tlc-exec.mjs's "dist missing, run tlc-build.mjs" message). Never committed
|
|
122
|
+
// (.gitignore) either way, so nothing here needs the freshness gate `dist/` no longer has.
|
|
123
|
+
let generateConfigSchema;
|
|
124
|
+
try {
|
|
125
|
+
({ generateConfigSchema } = await import("./generate-schema.ts"));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code !== "ERR_MODULE_NOT_FOUND") {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
console.log("tlc-build: schema.json skipped — typescript-json-schema is not installed here");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (generateConfigSchema) {
|
|
134
|
+
try {
|
|
135
|
+
const schema = generateConfigSchema(root);
|
|
136
|
+
writeFileSync(join(root, "schema.json"), `${JSON.stringify(schema, null, 2)}\n`);
|
|
137
|
+
console.log("tlc-build: schema.json ok");
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(`tlc-build: schema.json generation failed — ${error instanceof Error ? error.message : error}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
package/bin/tlc-cli.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
flagsDir,
|
|
21
21
|
isOnPath,
|
|
22
22
|
launcherBinDir,
|
|
23
|
+
loopsDir,
|
|
23
24
|
machineHome,
|
|
24
25
|
projectConfigPath,
|
|
25
26
|
projectStateDir,
|
|
@@ -193,6 +194,32 @@ export function setPaused(root: string, on: boolean): string {
|
|
|
193
194
|
return "gates ACTIVE again";
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
/**
|
|
198
|
+
* why: the operator's escape hatch for a stuck gate — `blockers` and `previous_gaps` are project-wide,
|
|
199
|
+
* so a grind-cap or stagnation signal from one session can block every later subagent until either a
|
|
200
|
+
* clean stop clears it or this runs. Denied from inside a session by `policy-surface-write`.
|
|
201
|
+
*/
|
|
202
|
+
export async function resetStuckState(root: string): Promise<string> {
|
|
203
|
+
const cleared = await coreFacade.handoff.clearStuckSignals(root);
|
|
204
|
+
const dir = loopsDir(root);
|
|
205
|
+
let loopFiles = 0;
|
|
206
|
+
if (existsSync(dir)) {
|
|
207
|
+
loopFiles = readdirSync(dir).length;
|
|
208
|
+
rmSync(dir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
if (cleared.length === 0 && loopFiles === 0) {
|
|
211
|
+
return "nothing stuck — no blockers and no grind-loop state to clear";
|
|
212
|
+
}
|
|
213
|
+
const parts: string[] = [];
|
|
214
|
+
if (cleared.length > 0) {
|
|
215
|
+
parts.push(`cleared blockers for: ${cleared.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
if (loopFiles > 0) {
|
|
218
|
+
parts.push(`reset ${loopFiles} grind-loop counter(s)`);
|
|
219
|
+
}
|
|
220
|
+
return parts.join("; ");
|
|
221
|
+
}
|
|
222
|
+
|
|
196
223
|
// hazard: this used to map `focus` onto a second spelling before writing, so the word the operator typed and the
|
|
197
224
|
// word the config field stored were different — and a config written from the documented word then matched no
|
|
198
225
|
// branch at all. One word per posture, and nothing translates.
|
|
@@ -263,14 +290,9 @@ export function handoffScreen(report: HandoffReport): Screen {
|
|
|
263
290
|
rows.push({ label, value: String(value), level });
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
["gaps", slice.previous_gaps?.map((gap) => gap.summary)],
|
|
270
|
-
] as const) {
|
|
271
|
-
if (list && list.length > 0) {
|
|
272
|
-
rows.push({ label, value: list.slice(0, 6).join(" | ") });
|
|
273
|
-
}
|
|
293
|
+
const gaps = slice.previous_gaps?.map((gap) => gap.summary);
|
|
294
|
+
if (gaps && gaps.length > 0) {
|
|
295
|
+
rows.push({ label: "gaps", value: gaps.slice(0, 6).join(" | ") });
|
|
274
296
|
}
|
|
275
297
|
sections.push({ title: `${name} (updated ${slice.updated_at})`, rows });
|
|
276
298
|
}
|
|
@@ -863,6 +885,7 @@ TOPICS
|
|
|
863
885
|
|
|
864
886
|
CONTROL
|
|
865
887
|
tlc harness grind [on|off] tlc harness pause | resume tlc harness mode solo|paired|focus
|
|
888
|
+
tlc harness reset clear a stuck blocker/grind-loop signal from the handoff
|
|
866
889
|
tlc harness gate test-command <cmd> [args...] tlc harness gate lint-command <cmd> [args...]
|
|
867
890
|
tlc harness attest tamper-evident record of what each session ran under
|
|
868
891
|
tlc harness policy show a policy that changed out of band; accept <path> to clear it
|
|
@@ -1128,6 +1151,7 @@ export type Action =
|
|
|
1128
1151
|
| { kind: "grind"; on: boolean }
|
|
1129
1152
|
| { kind: "pause" }
|
|
1130
1153
|
| { kind: "resume" }
|
|
1154
|
+
| { kind: "reset" }
|
|
1131
1155
|
| { kind: "mode"; value: string }
|
|
1132
1156
|
| { kind: "gate"; field: GateField; argv: string[] }
|
|
1133
1157
|
| { kind: "attest" }
|
|
@@ -1193,6 +1217,8 @@ export function route(args: string[]): Action {
|
|
|
1193
1217
|
case "resume":
|
|
1194
1218
|
case "r":
|
|
1195
1219
|
return { kind: "resume" };
|
|
1220
|
+
case "reset":
|
|
1221
|
+
return { kind: "reset" };
|
|
1196
1222
|
case "mode":
|
|
1197
1223
|
case "m": {
|
|
1198
1224
|
const modeArg = args[1];
|
|
@@ -1612,13 +1638,13 @@ function runInstall(toolArgs: string[], root: string): never {
|
|
|
1612
1638
|
process.exit(r.status ?? 1);
|
|
1613
1639
|
}
|
|
1614
1640
|
|
|
1615
|
-
function main(argv: string[]): void {
|
|
1641
|
+
async function main(argv: string[]): Promise<void> {
|
|
1616
1642
|
const root = resolveProjectRoot();
|
|
1617
1643
|
const group = (argv[0] ?? "").toLowerCase();
|
|
1618
1644
|
if (group !== "harness") {
|
|
1619
1645
|
console.error(`unknown: ${argv[0] ?? ""}`);
|
|
1620
1646
|
console.error(
|
|
1621
|
-
"usage: tlc harness <status|doctor|help|grind|pause|resume|mode|obs|prices|lessons|init|update|test|build>",
|
|
1647
|
+
"usage: tlc harness <status|doctor|help|grind|pause|resume|reset|mode|obs|prices|lessons|init|update|test|build>",
|
|
1622
1648
|
);
|
|
1623
1649
|
process.exit(1);
|
|
1624
1650
|
}
|
|
@@ -1744,6 +1770,9 @@ function main(argv: string[]): void {
|
|
|
1744
1770
|
case "resume":
|
|
1745
1771
|
console.log(setPaused(root, false));
|
|
1746
1772
|
break;
|
|
1773
|
+
case "reset":
|
|
1774
|
+
console.log(await resetStuckState(root));
|
|
1775
|
+
break;
|
|
1747
1776
|
case "mode":
|
|
1748
1777
|
try {
|
|
1749
1778
|
console.log(setMode(root, action.value));
|
|
@@ -1794,5 +1823,8 @@ function main(argv: string[]): void {
|
|
|
1794
1823
|
}
|
|
1795
1824
|
|
|
1796
1825
|
if (import.meta.main) {
|
|
1797
|
-
main(process.argv.slice(2))
|
|
1826
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
1827
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
});
|
|
1798
1830
|
}
|