@tech-leads-club/harness-toolkit 0.8.0 → 0.9.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/dist/compact-before.mjs +62 -62
- package/dist/doctor.mjs +75 -75
- package/dist/init-project.mjs +55 -55
- package/dist/lessons-cli.mjs +51 -51
- package/dist/prompt-submit.mjs +61 -61
- package/dist/response-after.mjs +62 -62
- package/dist/run.mjs +61 -61
- package/dist/session-end.mjs +66 -66
- package/dist/session-start.mjs +56 -56
- package/dist/stop.mjs +84 -84
- package/dist/subagent-start.mjs +61 -61
- package/dist/subagent-stop.mjs +64 -64
- package/dist/support.mjs +19 -19
- package/dist/tool-after.mjs +64 -64
- package/dist/tool-before.mjs +77 -77
- package/dist/tool-failure.mjs +61 -61
- package/docs/log.md +4 -0
- package/package.json +1 -1
- package/src/contracts/harness-event.ts +7 -0
- package/src/entrypoints/stop.ts +17 -4
- package/src/entrypoints/support.ts +13 -1
- package/src/entrypoints/tool-before.ts +8 -2
- package/src/providers/claude/claude.inbound.ts +8 -0
- package/src/providers/cursor/cursor.inbound.ts +10 -0
package/docs/log.md
CHANGED
|
@@ -13,6 +13,10 @@ Generated from `docs/decisions/` — do not edit by hand. Run `node tools/render
|
|
|
13
13
|
A reserved file of the [OKF v0.1](/decisions/ad-013.md) bundle: entries grouped under ISO 8601 headings,
|
|
14
14
|
newest first. For what landed in which npm release, see `CHANGELOG.md` at the repository root.
|
|
15
15
|
|
|
16
|
+
## 2026-08-29
|
|
17
|
+
|
|
18
|
+
- **AD-114** — Rule proof `since HEAD` resolves sha from the event's own working directory, not the project root ([/decisions/ad-114.md](/decisions/ad-114.md))
|
|
19
|
+
|
|
16
20
|
## 2026-08-26
|
|
17
21
|
|
|
18
22
|
- **AD-106** — A build step's own exit code cannot be the publish guarantee when it is also a recovery path ([/decisions/ad-106.md](/decisions/ad-106.md))
|
package/package.json
CHANGED
|
@@ -52,6 +52,13 @@ export type HarnessEvent = {
|
|
|
52
52
|
event: HarnessEventKind;
|
|
53
53
|
sessionKey: string;
|
|
54
54
|
projectDir: string;
|
|
55
|
+
/**
|
|
56
|
+
* The host's own report of the actual working directory for this event — the worktree root after
|
|
57
|
+
* the agent enters one, or the directory after a `cd` ([/decisions/ad-114.md](/decisions/ad-114.md)).
|
|
58
|
+
* Absent when the host does not report a per-event working directory. Distinct from `projectDir`,
|
|
59
|
+
* which anchors state/config and deliberately does not move into a worktree.
|
|
60
|
+
*/
|
|
61
|
+
cwd?: string;
|
|
55
62
|
// hazard: the `spawn*` pair describes the child of a spawn; the unprefixed fields describe the
|
|
56
63
|
// running agent. Conflating them clobbers sticky parent state.
|
|
57
64
|
model?: string;
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
import { flagsDir } from "../platform/paths.ts";
|
|
14
14
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
15
15
|
import { main } from "./run.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentGitSha,
|
|
18
|
+
formatLessonsBlock,
|
|
19
|
+
obsConfigFor,
|
|
20
|
+
sessionIdFromKey,
|
|
21
|
+
shaScopeRoot,
|
|
22
|
+
} from "./support.ts";
|
|
17
23
|
|
|
18
24
|
const STAGNATION_FOLLOWUP = [
|
|
19
25
|
"BLOCKED: identical validation fingerprint repeated — no progress between attempts.",
|
|
@@ -31,6 +37,7 @@ const STAGNATION_FOLLOWUP = [
|
|
|
31
37
|
*/
|
|
32
38
|
async function observeGateForRules(args: {
|
|
33
39
|
root: string;
|
|
40
|
+
shaRoot: string;
|
|
34
41
|
sessionKey: string;
|
|
35
42
|
policy: Policy;
|
|
36
43
|
gate: string;
|
|
@@ -40,7 +47,7 @@ async function observeGateForRules(args: {
|
|
|
40
47
|
return;
|
|
41
48
|
}
|
|
42
49
|
coreFacade.rules.observeGate(args.root, args.policy.rules, args.gate, {
|
|
43
|
-
sha: await currentGitSha(args.
|
|
50
|
+
sha: await currentGitSha(args.shaRoot),
|
|
44
51
|
sessionKey: args.sessionKey,
|
|
45
52
|
at: new Date().toISOString(),
|
|
46
53
|
});
|
|
@@ -179,6 +186,7 @@ async function creditPendingLessons(args: {
|
|
|
179
186
|
|
|
180
187
|
async function runLockedGate(args: {
|
|
181
188
|
root: string;
|
|
189
|
+
shaRoot: string;
|
|
182
190
|
provider: string;
|
|
183
191
|
session: string;
|
|
184
192
|
gate: "lint" | "test" | "docs";
|
|
@@ -429,6 +437,7 @@ async function failGate(args: {
|
|
|
429
437
|
*/
|
|
430
438
|
async function decideStopRules(
|
|
431
439
|
root: string,
|
|
440
|
+
shaRoot: string,
|
|
432
441
|
policy: Policy,
|
|
433
442
|
sessionKey: string,
|
|
434
443
|
): Promise<ReturnType<typeof coreFacade.rules.decideStop>> {
|
|
@@ -439,13 +448,14 @@ async function decideStopRules(
|
|
|
439
448
|
}
|
|
440
449
|
return coreFacade.rules.decideStop(root, policy.rules, {
|
|
441
450
|
...context,
|
|
442
|
-
sha: await currentGitSha(
|
|
451
|
+
sha: await currentGitSha(shaRoot),
|
|
443
452
|
});
|
|
444
453
|
}
|
|
445
454
|
|
|
446
455
|
export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerContext): Promise<Decision> => {
|
|
447
456
|
const { policy, capabilities } = ctx;
|
|
448
457
|
const root = event.projectDir;
|
|
458
|
+
const shaRoot = shaScopeRoot(event);
|
|
449
459
|
const provider = event.provider;
|
|
450
460
|
const sessionKey = event.sessionKey;
|
|
451
461
|
const session = sessionIdFromKey(event);
|
|
@@ -562,6 +572,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
562
572
|
if (policy.grind.enabled && policy.grind.lintCommand && codeTargets.length > 0) {
|
|
563
573
|
const run = await runLockedGate({
|
|
564
574
|
root,
|
|
575
|
+
shaRoot,
|
|
565
576
|
provider,
|
|
566
577
|
session,
|
|
567
578
|
pendingCredit,
|
|
@@ -599,6 +610,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
599
610
|
const recordFiles = testTargets.length > 0 ? testTargets : codeTargets;
|
|
600
611
|
const run = await runLockedGate({
|
|
601
612
|
root,
|
|
613
|
+
shaRoot,
|
|
602
614
|
provider,
|
|
603
615
|
session,
|
|
604
616
|
pendingCredit,
|
|
@@ -757,6 +769,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
757
769
|
if (policy.docs.command && policy.docs.command.length > 0) {
|
|
758
770
|
const run = await runLockedGate({
|
|
759
771
|
root,
|
|
772
|
+
shaRoot,
|
|
760
773
|
provider,
|
|
761
774
|
session,
|
|
762
775
|
pendingCredit,
|
|
@@ -900,7 +913,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
900
913
|
* invariant: `warn` returns `context`, which does not block. Everything else refuses the stop, so a rule the
|
|
901
914
|
* operator wrote cannot be ended past.
|
|
902
915
|
*/
|
|
903
|
-
const stopRules = await decideStopRules(root, policy, sessionKey);
|
|
916
|
+
const stopRules = await decideStopRules(root, shaRoot, policy, sessionKey);
|
|
904
917
|
if (stopRules.decision.kind !== "abstain") {
|
|
905
918
|
if (stopRules.decision.kind !== "context") {
|
|
906
919
|
const worst = coreFacade.rules.strictest(stopRules.outcomes);
|
|
@@ -58,6 +58,18 @@ export async function currentGitBranch(root: string): Promise<string | null> {
|
|
|
58
58
|
return branch.length > 0 ? branch : null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* why: `event.projectDir` prefers `CLAUDE_PROJECT_DIR`, which the host deliberately keeps pointed at the
|
|
63
|
+
* session's original root — including inside a git worktree, where it would otherwise return the wrong
|
|
64
|
+
* HEAD for a `since HEAD` rule proof. `event.cwd` is the field the host actually moves when the agent is
|
|
65
|
+
* working in a worktree or after a `cd`. Only the rules engine's proof sha needs this; every other use of
|
|
66
|
+
* `event.projectDir` (state dir, policy config, presence) is deliberately left alone
|
|
67
|
+
* ([/decisions/ad-114.md](/decisions/ad-114.md)).
|
|
68
|
+
*/
|
|
69
|
+
export function shaScopeRoot(event: HarnessEvent): string {
|
|
70
|
+
return event.cwd ?? event.projectDir;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export async function currentGitSha(root: string): Promise<string | null> {
|
|
62
74
|
if (!existsSync(join(root, ".git"))) {
|
|
63
75
|
return null;
|
|
@@ -205,7 +217,7 @@ export async function observeForRules(
|
|
|
205
217
|
if (!coreFacade.rules.wants(event.projectDir, config, event)) {
|
|
206
218
|
return;
|
|
207
219
|
}
|
|
208
|
-
const sha = await currentGitSha(event
|
|
220
|
+
const sha = await currentGitSha(shaScopeRoot(event));
|
|
209
221
|
coreFacade.rules.observe(event.projectDir, config, event, {
|
|
210
222
|
sha,
|
|
211
223
|
sessionKey: event.sessionKey,
|
|
@@ -2,7 +2,13 @@ import type { Decision, HarnessEvent } from "../contracts/index.ts";
|
|
|
2
2
|
import { coreFacade } from "../core/index.ts";
|
|
3
3
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
4
|
import { main } from "./run.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
currentGitSha,
|
|
7
|
+
obsConfigFor,
|
|
8
|
+
readModelFromToolInput,
|
|
9
|
+
shaScopeRoot,
|
|
10
|
+
subagentSpawnInput,
|
|
11
|
+
} from "./support.ts";
|
|
6
12
|
|
|
7
13
|
const READONLY_BLOCKED_TOOLS = new Set(["Write", "Delete", "Shell"]);
|
|
8
14
|
|
|
@@ -61,7 +67,7 @@ async function rulesDecision(event: HarnessEvent, ctx: HandlerContext): Promise<
|
|
|
61
67
|
}
|
|
62
68
|
// why twice: the first pass answers whether any rule fired at all, which costs no git. Only then is the sha
|
|
63
69
|
// worth a process, and the second pass is the one whose verdict counts.
|
|
64
|
-
const sha = await currentGitSha(event
|
|
70
|
+
const sha = await currentGitSha(shaScopeRoot(event));
|
|
65
71
|
const verdict = coreFacade.rules.decideAction(event.projectDir, config, trigger, {
|
|
66
72
|
sha,
|
|
67
73
|
sessionKey: event.sessionKey,
|
|
@@ -119,6 +119,14 @@ export function claudeToEvent(raw: Record<string, unknown>): HarnessEvent | null
|
|
|
119
119
|
event.permissionMode = permissionMode;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// why: `cwd` is a common field on every Claude hook payload, and the host docs confirm it tracks the
|
|
123
|
+
// agent into a worktree or after a `cd` — unlike `projectDir`, which stays at the session's original
|
|
124
|
+
// root ([/decisions/ad-114.md](/decisions/ad-114.md)).
|
|
125
|
+
const cwd = asString(raw.cwd);
|
|
126
|
+
if (cwd) {
|
|
127
|
+
event.cwd = cwd;
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
const isSpawnEvent = eventKind === "subagent.start" || eventKind === "subagent.stop";
|
|
123
131
|
const model = isSpawnEvent ? undefined : asString(raw.model);
|
|
124
132
|
if (model) {
|
|
@@ -142,6 +142,16 @@ export function cursorToEvent(raw: Record<string, unknown>): HarnessEvent | null
|
|
|
142
142
|
if (output !== undefined) {
|
|
143
143
|
event.toolOutput = output;
|
|
144
144
|
}
|
|
145
|
+
// why: confirmed against cursor.com/docs/hooks — `cwd` exists only on `beforeShellExecution`
|
|
146
|
+
// (this event's `shell.before` half), not on `afterShellExecution` or any other event this
|
|
147
|
+
// adapter maps. Never guessed onto an event kind the host does not report it for
|
|
148
|
+
// ([/decisions/ad-114.md](/decisions/ad-114.md)).
|
|
149
|
+
if (eventKind === "shell.before") {
|
|
150
|
+
const cwd = asString(raw.cwd);
|
|
151
|
+
if (cwd !== undefined) {
|
|
152
|
+
event.cwd = cwd;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
145
155
|
break;
|
|
146
156
|
}
|
|
147
157
|
case "mcp.before":
|