@tech-leads-club/harness-toolkit 0.16.0 → 0.16.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/dist/compact-before.mjs +1 -1
- package/dist/doctor.mjs +1 -1
- package/dist/prompt-submit.mjs +33 -33
- package/dist/response-after.mjs +1 -1
- package/dist/run.mjs +1 -1
- package/dist/session-end.mjs +1 -1
- package/dist/session-start.mjs +1 -1
- package/dist/ship-gate.mjs +75 -75
- package/dist/stop.mjs +80 -80
- package/dist/subagent-start.mjs +1 -1
- package/dist/subagent-stop.mjs +1 -1
- package/dist/support.mjs +10 -10
- package/dist/tool-after.mjs +71 -71
- package/dist/tool-before.mjs +80 -80
- package/dist/tool-failure.mjs +1 -1
- package/docs/log.md +4 -0
- package/package.json +1 -1
- package/src/core/handoff/handoff.types.ts +7 -0
- package/src/entrypoints/prompt-submit.ts +8 -3
- package/src/entrypoints/stop.ts +4 -2
- package/src/entrypoints/support.ts +25 -3
- package/src/entrypoints/tool-after.ts +2 -2
- package/src/providers/cursor/cursor.wiring.ts +1 -1
package/dist/tool-failure.mjs
CHANGED
|
@@ -157,6 +157,6 @@ description: Harness-learned lessons (auto-synced from gate failures)
|
|
|
157
157
|
alwaysApply: true
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
${J}`;return WV(Q,q,"utf8"),Q}function u3($,Z){let J={permission:$};if(Z.userNote!==void 0)J.user_message=Z.userNote;return J.agent_message=Z.reason,JSON.stringify(J)}function h3($,Z){switch($.kind){case"abstain":return{stdout:"{}",exitCode:0};case"allow":return{stdout:JSON.stringify({permission:"allow"}),exitCode:0};case"deny":return{stdout:u3("deny",$),exitCode:0};case"ask":return{stdout:u3("ask",$),exitCode:0};case"context":{let J={};if($.env!==void 0)J.env=$.env;return J.additional_context=$.text,{stdout:JSON.stringify(J),exitCode:0}}case"continue":return{stdout:JSON.stringify({followup_message:$.text}),exitCode:0};case"rewriteInput":return{stdout:JSON.stringify({updated_input:$.input}),exitCode:0};case"rewriteOutput":return{stdout:JSON.stringify({updated_mcp_tool_output:{result_json:$.output}}),exitCode:0};default:throw Error(`unreachable decision kind: ${JSON.stringify($)}`)}}function p3(){return{blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])","composer-2\\.5-fast"],minEffort:null,untrustedTools:["Fetch","WebSearch"]}}import{join as VV}from"node:path";function c3(){return VV(J9(),"hooks.json")}function d3(){return[c3()]}var GV=[{hookEvent:"sessionStart",handler:"session-start",timeoutSeconds:10},{hookEvent:"sessionEnd",handler:"session-end",timeoutSeconds:10},{hookEvent:"beforeSubmitPrompt",handler:"prompt-submit",timeoutSeconds:5},{hookEvent:"afterAgentThought",handler:"tool-after",timeoutSeconds:5},{hookEvent:"preCompact",handler:"compact-before",timeoutSeconds:5},{hookEvent:"subagentStart",handler:"subagent-start",timeoutSeconds:5,failClosed:!0},{hookEvent:"subagentStop",handler:"subagent-stop",timeoutSeconds:5},{hookEvent:"preToolUse",handler:"tool-before",timeoutSeconds:
|
|
160
|
+
${J}`;return WV(Q,q,"utf8"),Q}function u3($,Z){let J={permission:$};if(Z.userNote!==void 0)J.user_message=Z.userNote;return J.agent_message=Z.reason,JSON.stringify(J)}function h3($,Z){switch($.kind){case"abstain":return{stdout:"{}",exitCode:0};case"allow":return{stdout:JSON.stringify({permission:"allow"}),exitCode:0};case"deny":return{stdout:u3("deny",$),exitCode:0};case"ask":return{stdout:u3("ask",$),exitCode:0};case"context":{let J={};if($.env!==void 0)J.env=$.env;return J.additional_context=$.text,{stdout:JSON.stringify(J),exitCode:0}}case"continue":return{stdout:JSON.stringify({followup_message:$.text}),exitCode:0};case"rewriteInput":return{stdout:JSON.stringify({updated_input:$.input}),exitCode:0};case"rewriteOutput":return{stdout:JSON.stringify({updated_mcp_tool_output:{result_json:$.output}}),exitCode:0};default:throw Error(`unreachable decision kind: ${JSON.stringify($)}`)}}function p3(){return{blockedPatterns:["-fast(?:$|[^a-z0-9])","/fast(?:$|[^a-z0-9])","composer-2\\.5-fast"],minEffort:null,untrustedTools:["Fetch","WebSearch"]}}import{join as VV}from"node:path";function c3(){return VV(J9(),"hooks.json")}function d3(){return[c3()]}var GV=[{hookEvent:"sessionStart",handler:"session-start",timeoutSeconds:10},{hookEvent:"sessionEnd",handler:"session-end",timeoutSeconds:10},{hookEvent:"beforeSubmitPrompt",handler:"prompt-submit",timeoutSeconds:5},{hookEvent:"afterAgentThought",handler:"tool-after",timeoutSeconds:5},{hookEvent:"preCompact",handler:"compact-before",timeoutSeconds:5},{hookEvent:"subagentStart",handler:"subagent-start",timeoutSeconds:5,failClosed:!0},{hookEvent:"subagentStop",handler:"subagent-stop",timeoutSeconds:5},{hookEvent:"preToolUse",handler:"tool-before",timeoutSeconds:10,failClosed:!0},{hookEvent:"postToolUse",handler:"tool-after",timeoutSeconds:5},{hookEvent:"postToolUseFailure",handler:"tool-failure",timeoutSeconds:5},{hookEvent:"beforeShellExecution",handler:"tool-before",timeoutSeconds:10,failClosed:!0},{hookEvent:"afterShellExecution",handler:"tool-after",timeoutSeconds:10},{hookEvent:"beforeMCPExecution",handler:"tool-before",timeoutSeconds:10},{hookEvent:"afterMCPExecution",handler:"tool-after",timeoutSeconds:5},{hookEvent:"beforeReadFile",handler:"tool-before",timeoutSeconds:5},{hookEvent:"afterFileEdit",handler:"tool-after",timeoutSeconds:30,matcher:"Write"},{hookEvent:"stop",handler:"stop",timeoutSeconds:120,loopLimit:5},{hookEvent:"afterAgentResponse",handler:"response-after",timeoutSeconds:5,matcher:"AgentResponse"}];function l3($){let J=[$.launcherPath],Q=GV.map((q)=>({hookEvent:q.hookEvent,handler:q.handler,command:"node",args:[...J,q.handler],timeoutSeconds:q.timeoutSeconds,...q.failClosed!==void 0?{failClosed:q.failClosed}:{},...q.matcher!==void 0?{matcher:q.matcher}:{},...q.loopLimit!==void 0?{loopLimit:q.loopLimit}:{}}));return{target:c3(),strategy:"replace",entries:Q}}var i3={name:"cursor",detect:f3,capabilities:b3,policyDefaults:p3,toEvent:w3,render:h3,wiring:l3,wiringTargets:d3,lessonsView:g3};var y$=[i3,S3];function s5($,Z){let J=Z.filter((Q)=>Q.detect($));if(J.length===0)return{provider:null,ambiguous:!1,matchedNames:[]};return{provider:J[0]??null,ambiguous:J.length>1,matchedNames:J.map((Q)=>Q.name)}}var n3=D.observability.DEFAULT_OBS,a3={...n3,debugEnabled:!0};function l8($,Z=n3){return{...Z,globalSpool:$.obs.globalSpool,includePayloads:$.obs.includePayloads,maxAttrChars:$.obs.maxAttrChars,sessionCostAlertUsd:$.obs.sessionCostAlertUsd,retentionDays:$.obs.retentionDays}}function r3($){let Z=`${$.provider}-`;return $.sessionKey.startsWith(Z)?$.sessionKey.slice(Z.length):$.sessionKey}function s3($,Z){return[...D.policy.forProvider($,Z.name)??[],...Z.policyDefaults().blockedPatterns]}var UV=6000;function HV($){if($.filePath===void 0)return!1;if($.event==="edit.after")return!0;return _8($.toolName)}function MV($){return $ instanceof Error?$.message:String($)}function r($,Z,J,Q="unknown",q={}){try{L(BV(H($),"obs.jsonl"),{schema:"harness.observability.v1",provider:Q,kind:Z,level:"signal",ts:new Date().toISOString(),...q,attrs:J})}catch{}}function KV($){return $.event==="shell.before"||$.event==="mcp.before"}function FV($){if(!KV($))return;r($.projectDir,"hook.enter",{event:$.event,toolName:$.toolName??"none",sessionKey:$.sessionKey},$.provider,{trace_id:D.observability.deriveTraceId($.sessionKey),session_id:$.sessionKey})}function CV($,Z,J){if(J.kind!=="deny"&&J.kind!=="ask")return;if($.event==="shell.before")return;D.observability.recordObs($.projectDir,l8(Z),{provider:$.provider,kind:"policy.deny",sessionKey:$.sessionKey,attrs:{event:$.event,tool_name:$.toolName,permission:J.kind,rule:J.rule??"none",diagnostic:J.diagnostic??"none"}})}function OV($){return $!==null&&typeof $==="object"&&!Array.isArray($)?$:{}}async function _V($,Z={}){let J=Z.readStdin??R9,Q=Z.now?Z.now():new Date,q={stdout:null,exitCode:0},W=(await J()).trim();if(!W)return r(process.cwd(),"adapter.unrecognized",{reason:"empty-stdin"}),{event:null,decision:{kind:"abstain"},rendered:q};let Y;try{Y=JSON.parse(W)}catch{return r(process.cwd(),"adapter.unrecognized",{reason:"invalid-json"}),{event:null,decision:{kind:"abstain"},rendered:q}}let z=s5(Y,y$);if(!z.provider)return r(process.cwd(),"adapter.unrecognized",{reason:"no-provider-match"}),{event:null,decision:{kind:"abstain"},rendered:q};if(z.ambiguous)r(process.cwd(),"adapter.ambiguous",{matched:z.matchedNames});let V=z.provider,G=V.toEvent(OV(Y));if(!G)return r(process.cwd(),"adapter.unrecognized",{reason:"unrecognized-event",provider:V.name}),{event:null,decision:{kind:"abstain"},rendered:q};let U=V.capabilities();FV(G);try{let M=D.policy.loadPolicy(G.projectDir);if(G.model)D.subagentPolicy.upsertParentModelState(G.projectDir,G.sessionKey,{model:G.model},s3(M.subagents.blockedPatterns,V));D.presence.heartbeat(G.projectDir,{provider:G.provider,session:r3(G),...HV(G)?{file:G.filePath}:{},now:Q});let B=y$.flatMap((E)=>E.wiringTargets()),T=await $(G,{policy:M,capabilities:U,provider:V,now:Q,protectedPaths:B}),S=d8(T,G,U,{contextBudgetChars:UV});CV(G,M,S);let d=V.render(S,G);return{event:G,decision:S,rendered:d}}catch(M){r(G.projectDir,"adapter.error",{provider:G.provider,event:G.event,message:MV(M)});let B={kind:"abstain"};return{event:G,decision:B,rendered:V.render(B,G)}}}async function o3($){let Z=await _V($);if(Z.rendered.stdout!==null){let J=Z.rendered.stdout;process.stdout.write(J.endsWith(`
|
|
161
161
|
`)?J:`${J}
|
|
162
162
|
`)}process.exit(Z.rendered.exitCode)}var IV=($,Z)=>{return D.observability.recordAudit($.projectDir,$.event,$.raw,Z.policy.obs.globalSpool),D.observability.recordObs($.projectDir,l8(Z.policy,a3),{provider:$.provider,kind:"tool.fail",sessionKey:$.sessionKey,model:$.model,attrs:{tool_name:$.toolName,file_path:$.filePath,status:$.status}}),{kind:"abstain"}};if(t5.main==t5.module)await o3(IV);export{IV as toolFailureHandler};
|
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-09-16
|
|
17
|
+
|
|
18
|
+
- **AD-144** — turn_base_sha records the git root it came from, and a mismatch falls back to HEAD ([/decisions/ad-144.md](/decisions/ad-144.md))
|
|
19
|
+
|
|
16
20
|
## 2026-09-12
|
|
17
21
|
|
|
18
22
|
- **AD-139** — lessonsView joins ProviderPort, closing the one dispatcher that bypassed it ([/decisions/ad-139.md](/decisions/ad-139.md))
|
package/package.json
CHANGED
|
@@ -36,6 +36,13 @@ export type HandoffProviderSlice = {
|
|
|
36
36
|
* ([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
37
37
|
*/
|
|
38
38
|
turn_base_sha?: string;
|
|
39
|
+
/**
|
|
40
|
+
* why: the git root `turn_base_sha` was captured from. A session that moves between independent
|
|
41
|
+
* worktrees can submit one turn's prompt from one root and stop it from another; without this, the
|
|
42
|
+
* stale sha from the first root gets diffed against the second, reading months of someone else's
|
|
43
|
+
* history as "added this turn" ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
44
|
+
*/
|
|
45
|
+
turn_base_root?: string;
|
|
39
46
|
plan_paths?: string[];
|
|
40
47
|
plan_at?: string;
|
|
41
48
|
plan_snippet?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HarnessEvent } from "../contracts/index.ts";
|
|
2
2
|
import { coreFacade } from "../core/index.ts";
|
|
3
|
+
import { gitRootOf } from "../platform/git.ts";
|
|
3
4
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
4
5
|
import { main } from "./run.ts";
|
|
5
6
|
import { currentGitSha, obsConfigFor, shaScopeRoot } from "./support.ts";
|
|
@@ -14,10 +15,14 @@ export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: Han
|
|
|
14
15
|
// why: the exact AD-114 divergence, now against turn_base_sha instead of a rule's proof sha — a worktree
|
|
15
16
|
// session recorded the main checkout's HEAD as the turn's start, so every gate diffing against it saw a
|
|
16
17
|
// whole untouched file read as "added this turn" ([/decisions/ad-117.md](/decisions/ad-117.md)).
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const shaRoot = shaScopeRoot(event);
|
|
19
|
+
const [sha, gitRoot] = await Promise.all([currentGitSha(shaRoot), gitRootOf(shaRoot)]);
|
|
20
|
+
// why: a sha with no recorded root cannot later be told apart from one captured in a different git root
|
|
21
|
+
// entirely — `resolveTurnBase` treats that ambiguity as unsafe, so both are written together or neither is
|
|
22
|
+
// ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
23
|
+
if (sha && gitRoot) {
|
|
19
24
|
await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, event.sessionKey, {
|
|
20
|
-
slice: { turn_base_sha: sha },
|
|
25
|
+
slice: { turn_base_sha: sha, turn_base_root: gitRoot },
|
|
21
26
|
});
|
|
22
27
|
}
|
|
23
28
|
return { kind: "abstain" };
|
package/src/entrypoints/stop.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
currentGitSha,
|
|
19
19
|
formatLessonsBlock,
|
|
20
20
|
obsConfigFor,
|
|
21
|
+
resolveTurnBase,
|
|
21
22
|
sessionIdFromKey,
|
|
22
23
|
shaScopeRoot,
|
|
23
24
|
} from "./support.ts";
|
|
@@ -489,8 +490,9 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
|
|
|
489
490
|
// past its own changes, and every gate below then saw an empty diff and skipped — the comment gate in a repo
|
|
490
491
|
// whose task was "schema v2 + tests + commit" ([/decisions/ad-058.md](/decisions/ad-058.md)).
|
|
491
492
|
//
|
|
492
|
-
// invariant: absent,
|
|
493
|
-
|
|
493
|
+
// invariant: absent, or captured from a different git root than `shaRoot`, this is the string `HEAD`,
|
|
494
|
+
// which is exactly the previous behaviour for the absent case ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
495
|
+
const turnBase = await resolveTurnBase(handoff, shaRoot);
|
|
494
496
|
/**
|
|
495
497
|
* why: collected rather than returned. A turn may defer more than one gate to the same neighbour, and the reply
|
|
496
498
|
* says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
|
|
@@ -92,8 +92,9 @@ export type TurnScope = {
|
|
|
92
92
|
* at `stop` or, since AD-116, before `commit`/`push`/`pr-open` ships it. A second, differently-scoped
|
|
93
93
|
* answer to the same question is the drift AD-071 already named once ([/decisions/ad-116.md](/decisions/ad-116.md)).
|
|
94
94
|
*
|
|
95
|
-
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent
|
|
96
|
-
* identical to `stop.ts`'s own fallback
|
|
95
|
+
* invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent, its seal diverged, or
|
|
96
|
+
* `resolveTurnBase` finds a different git root — identical to `stop.ts`'s own fallback
|
|
97
|
+
* ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
97
98
|
* why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
|
|
98
99
|
* (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
|
|
99
100
|
* branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
|
|
@@ -107,7 +108,7 @@ export async function computeTurnScope(
|
|
|
107
108
|
): Promise<TurnScope> {
|
|
108
109
|
const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
|
|
109
110
|
const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
|
|
110
|
-
const turnBase = handoff
|
|
111
|
+
const turnBase = await resolveTurnBase(handoff, gitRoot);
|
|
111
112
|
const rawChangedFiles = await listChangedRepoFiles(gitRoot, turnBase);
|
|
112
113
|
const otherSessionFiles = await coreFacade.presence.filesClaimedByOtherLiveSessions(
|
|
113
114
|
root,
|
|
@@ -159,6 +160,27 @@ export async function pendingCommentViolations(
|
|
|
159
160
|
);
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
/**
|
|
164
|
+
* why: `turn_base_sha` alone cannot tell "this session's own HEAD" from "the HEAD of a worktree the shell
|
|
165
|
+
* was sitting in three turns ago" — both are just a sha. Comparing the git root it was captured from
|
|
166
|
+
* against the root being diffed against is what tells the two apart, the same way an absent
|
|
167
|
+
* `turn_base_sha` already falls back to `HEAD` rather than diffing against nothing, instead of diffing one
|
|
168
|
+
* repository's turn against an unrelated one's entire history ([/decisions/ad-144.md](/decisions/ad-144.md)).
|
|
169
|
+
*
|
|
170
|
+
* invariant: a slice with a sha but no recorded root (state written before this existed) resolves to
|
|
171
|
+
* `HEAD`, the same as no slice at all — safe by default, and self-healing on the next `prompt.submit`.
|
|
172
|
+
*/
|
|
173
|
+
export async function resolveTurnBase(
|
|
174
|
+
slice: { turn_base_sha?: string; turn_base_root?: string } | undefined,
|
|
175
|
+
gitRoot: string,
|
|
176
|
+
): Promise<string> {
|
|
177
|
+
if (!slice?.turn_base_sha || !slice.turn_base_root) {
|
|
178
|
+
return "HEAD";
|
|
179
|
+
}
|
|
180
|
+
const currentRoot = await gitRootOf(gitRoot);
|
|
181
|
+
return currentRoot !== null && currentRoot === slice.turn_base_root ? slice.turn_base_sha : "HEAD";
|
|
182
|
+
}
|
|
183
|
+
|
|
162
184
|
export async function currentGitSha(root: string): Promise<string | null> {
|
|
163
185
|
const gitRoot = await gitRootOf(root);
|
|
164
186
|
if (gitRoot === null) {
|
|
@@ -6,7 +6,7 @@ import { normalizeSeparators } from "../platform/sanitize.ts";
|
|
|
6
6
|
import { readClaudeUsage } from "../providers/index.ts";
|
|
7
7
|
import type { Handler, HandlerContext } from "./run.ts";
|
|
8
8
|
import { main } from "./run.ts";
|
|
9
|
-
import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules } from "./support.ts";
|
|
9
|
+
import { OBS_CONFIG_AUDIT, obsConfigFor, observeForRules, resolveTurnBase } from "./support.ts";
|
|
10
10
|
|
|
11
11
|
const OBS_KIND_BY_EVENT: Partial<Record<HarnessEventKind, ObsKind>> = {
|
|
12
12
|
"tool.after": "tool.end",
|
|
@@ -73,7 +73,7 @@ async function commentEditAdvisory(event: HarnessEvent, ctx: HandlerContext): Pr
|
|
|
73
73
|
event.projectDir,
|
|
74
74
|
targets,
|
|
75
75
|
policy.comments.mode,
|
|
76
|
-
handoff.
|
|
76
|
+
await resolveTurnBase(handoff, event.projectDir),
|
|
77
77
|
);
|
|
78
78
|
if (hits.length === 0) {
|
|
79
79
|
return null;
|
|
@@ -28,7 +28,7 @@ const ENTRY_SPECS: readonly EntrySpec[] = [
|
|
|
28
28
|
{ hookEvent: "preCompact", handler: "compact-before", timeoutSeconds: 5 },
|
|
29
29
|
{ hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5, failClosed: true },
|
|
30
30
|
{ hookEvent: "subagentStop", handler: "subagent-stop", timeoutSeconds: 5 },
|
|
31
|
-
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds:
|
|
31
|
+
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|
|
32
32
|
{ hookEvent: "postToolUse", handler: "tool-after", timeoutSeconds: 5 },
|
|
33
33
|
{ hookEvent: "postToolUseFailure", handler: "tool-failure", timeoutSeconds: 5 },
|
|
34
34
|
{ hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|