@tech-leads-club/harness-toolkit 0.16.3 → 0.16.5

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 CHANGED
@@ -486,7 +486,7 @@ See `tlc harness help architecture` or [`docs/architecture.md`](docs/architectur
486
486
  | `tlc harness build` | Compile `dist/` for the Node fallback path |
487
487
  | `tlc harness test` | Run the full local gate |
488
488
  | `tlc harness grind [on\|off]` | Lint/test follow-ups on stop |
489
- | `tlc harness pause` / `resume` | Disable / enable stop checks |
489
+ | `tlc harness pause` / `resume` | Circuit breaker: every hook steps aside except the floor / back on |
490
490
  | `tlc harness mode solo\|paired\|focus` | Operator posture |
491
491
  | `tlc harness attest` | Tamper-evident record of what each session ran under |
492
492
  | `tlc harness handoff` | Handoff state between turns and sessions — the sanctioned reader |
package/bin/tlc-cli.ts CHANGED
@@ -116,7 +116,7 @@ export function statusScreen(root: string): Screen {
116
116
  },
117
117
  {
118
118
  label: "gates",
119
- value: report.gatesPaused ? "PAUSED — stop checks disabled" : "active",
119
+ value: report.gatesPaused ? "PAUSED — every hook steps aside; only the floor runs" : "active",
120
120
  level: report.gatesPaused ? "warn" : "ok",
121
121
  },
122
122
  ],
@@ -185,13 +185,13 @@ export function setPaused(root: string, on: boolean): string {
185
185
  if (on) {
186
186
  writeFileSync(path, "");
187
187
  coreFacade.policy.refreshPolicyBaselines(root);
188
- return "gates PAUSED — stop checks disabled until `tlc harness resume`";
188
+ return "harness PAUSED — every hook steps aside (only the floor still runs) until `tlc harness resume`";
189
189
  }
190
190
  if (existsSync(path)) {
191
191
  rmSync(path);
192
192
  }
193
193
  coreFacade.policy.refreshPolicyBaselines(root);
194
- return "gates ACTIVE again";
194
+ return "harness ACTIVE again";
195
195
  }
196
196
 
197
197
  /**
@@ -59,3 +59,21 @@ export function decideRuntime(input: RuntimeDecisionInput): RuntimeDecision;
59
59
 
60
60
  export function main(argv?: string[]): void;
61
61
  export const HOOK_ENTRIES: Set<string>;
62
+
63
+ export const HOOK_TRACE_MAX_BYTES: number;
64
+
65
+ export function hookTracePath(harnessHome: string): string;
66
+
67
+ export function appendHookTrace(harnessHome: string, record: Record<string, unknown>): void;
68
+
69
+ export type HookIdentity =
70
+ | {
71
+ event?: string;
72
+ tool?: string;
73
+ tool_use_id?: string;
74
+ generation_id?: string;
75
+ conversation_id?: string;
76
+ }
77
+ | { parse_error: true };
78
+
79
+ export function hookIdentity(input: Buffer): HookIdentity;
package/bin/tlc-exec.mjs CHANGED
@@ -1,6 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, writeSync } from "node:fs";
3
+ import {
4
+ appendFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ statSync,
11
+ writeFileSync,
12
+ writeSync,
13
+ } from "node:fs";
4
14
  import { homedir } from "node:os";
5
15
  import { delimiter, dirname, join } from "node:path";
6
16
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -191,6 +201,52 @@ function carryOn() {
191
201
  process.exit(0);
192
202
  }
193
203
 
204
+ export const HOOK_TRACE_MAX_BYTES = 5 * 1024 * 1024;
205
+
206
+ export function hookTracePath(harnessHome) {
207
+ return join(harnessHome, "state", "hook-trace.jsonl");
208
+ }
209
+
210
+ export function appendHookTrace(harnessHome, record) {
211
+ if (isPackagedCopy(harnessHome)) {
212
+ return;
213
+ }
214
+ try {
215
+ const path = hookTracePath(harnessHome);
216
+ mkdirSync(dirname(path), { recursive: true });
217
+ try {
218
+ if (statSync(path).size > HOOK_TRACE_MAX_BYTES) {
219
+ renameSync(path, `${path}.1`);
220
+ }
221
+ } catch {}
222
+ appendFileSync(path, `${JSON.stringify({ ts: new Date().toISOString(), pid: process.pid, ...record })}\n`);
223
+ } catch {}
224
+ }
225
+
226
+ export function hookIdentity(input) {
227
+ try {
228
+ const parsed = JSON.parse(input.toString("utf8"));
229
+ const pick = (key) => (typeof parsed?.[key] === "string" ? parsed[key] : undefined);
230
+ return {
231
+ event: pick("hook_event_name"),
232
+ tool: pick("tool_name"),
233
+ tool_use_id: pick("tool_use_id"),
234
+ generation_id: pick("generation_id"),
235
+ conversation_id: pick("conversation_id") ?? pick("session_id"),
236
+ };
237
+ } catch {
238
+ return { parse_error: true };
239
+ }
240
+ }
241
+
242
+ function readHookInput() {
243
+ try {
244
+ return { input: readFileSync(0) };
245
+ } catch (error) {
246
+ return { input: null, error: error instanceof Error ? error.message : String(error) };
247
+ }
248
+ }
249
+
194
250
  export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExists, srcPath }) {
195
251
  const distPath = join(harnessHome, "dist", `${entry}.mjs`);
196
252
  if (bunPath && srcPath && distExists) {
@@ -236,8 +292,24 @@ export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExis
236
292
 
237
293
  function run(harnessHome, command, commandArgs, origin = harnessHome, entry = "") {
238
294
  const isHook = HOOK_ENTRIES.has(entry);
295
+ const started = Date.now();
296
+ const hookInput = isHook ? readHookInput() : { input: null };
297
+ if (isHook) {
298
+ appendHookTrace(harnessHome, {
299
+ phase: "start",
300
+ entry,
301
+ ppid: process.ppid,
302
+ cwd: process.cwd(),
303
+ node: process.version,
304
+ uptime_ms: Math.round(process.uptime() * 1000),
305
+ stdin_bytes: hookInput.input?.length ?? null,
306
+ stdin_error: hookInput.error,
307
+ ...(hookInput.input ? hookIdentity(hookInput.input) : {}),
308
+ });
309
+ }
239
310
  const result = spawnSync(command, commandArgs, {
240
- stdio: isHook ? ["inherit", "pipe", "inherit"] : "inherit",
311
+ ...(hookInput.input ? { input: hookInput.input } : {}),
312
+ stdio: isHook ? [hookInput.input ? "pipe" : "inherit", "pipe", "inherit"] : "inherit",
241
313
  maxBuffer: 64 * 1024 * 1024,
242
314
  // why: `TLC_ORIGIN` is where this copy physically lives, which is not `TLC_HOME` once an npm-installed shim
243
315
  // is driving the runtime installed under the conventional path. `tlc harness install` needs the former as
@@ -254,6 +326,18 @@ function run(harnessHome, command, commandArgs, origin = harnessHome, entry = ""
254
326
  },
255
327
  shell: false,
256
328
  });
329
+ if (isHook) {
330
+ appendHookTrace(harnessHome, {
331
+ phase: "end",
332
+ entry,
333
+ ms: Date.now() - started,
334
+ status: result.status,
335
+ signal: result.signal,
336
+ spawn_error: result.error?.message,
337
+ stdout_bytes: result.stdout?.length ?? 0,
338
+ verdict: result.stdout?.toString("utf8").trim().slice(0, 60),
339
+ });
340
+ }
257
341
  if (result.error) {
258
342
  console.error(`tlc: failed to start ${command}: ${result.error.message}`);
259
343
  if (HOOK_ENTRIES.has(entry)) {
@@ -284,6 +368,14 @@ export function main(argv = process.argv) {
284
368
  console.error(" entry: session-start | tool-before | stop | doctor | ...");
285
369
  process.exit(2);
286
370
  }
371
+ if (HOOK_ENTRIES.has(entry)) {
372
+ process.on("uncaughtException", (error) => {
373
+ const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
374
+ appendHookTrace(harnessHome, { phase: "crash", entry, error: detail.slice(0, 500) });
375
+ console.error(`tlc: ${entry} launcher failed (${detail.split("\n")[0]}); carrying on without a verdict.`);
376
+ carryOn();
377
+ });
378
+ }
287
379
  const args = argv.slice(3);
288
380
 
289
381
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);