@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.
@@ -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:5,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(`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -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 sha = await currentGitSha(shaScopeRoot(event));
18
- if (sha) {
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" };
@@ -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, this is the string `HEAD`, which is exactly the previous behaviour.
493
- const turnBase = handoff.turn_base_sha ?? "HEAD";
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 or its seal diverged —
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?.turn_base_sha ?? "HEAD";
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.turn_base_sha ?? "HEAD",
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: 5, failClosed: true },
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 },