@tech-leads-club/harness-toolkit 0.12.0 → 0.12.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/docs/log.md CHANGED
@@ -16,6 +16,7 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
16
16
  ## 2026-09-09
17
17
 
18
18
  - **AD-128** — pr-open and push recognize their gh api equivalent, and pr-merge exists ([/decisions/ad-128.md](/decisions/ad-128.md))
19
+ - **AD-129** — Turn-scope git diffing runs at the event's own working directory, not the project root ([/decisions/ad-129.md](/decisions/ad-129.md))
19
20
 
20
21
  ## 2026-09-08
21
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -49,13 +49,13 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
49
49
  }
50
50
  const { policy } = ctx;
51
51
  const root = event.projectDir;
52
+ const shaRoot = shaScopeRoot(event);
52
53
  const provider = event.provider;
53
54
  const sessionKey = event.sessionKey;
54
55
  const session = sessionIdFromKey(event);
55
- const scope = await computeTurnScope(root, provider, sessionKey, policy);
56
- const shaRoot = shaScopeRoot(event);
56
+ const scope = await computeTurnScope(root, shaRoot, provider, sessionKey, policy);
57
57
 
58
- const commentHits = await pendingCommentViolations(root, provider, sessionKey, policy);
58
+ const commentHits = await pendingCommentViolations(root, shaRoot, provider, sessionKey, policy);
59
59
  if (commentHits.length > 0) {
60
60
  return {
61
61
  kind: "deny",
@@ -143,13 +143,13 @@ export async function shipGateVerdict(event: HarnessEvent, ctx: HandlerContext):
143
143
  }
144
144
 
145
145
  if (policy.duplication.enabled && scope.codeTargets.length > 0) {
146
- const added = await listAddedLines(root, scope.codeTargets, scope.turnBase);
147
- const tracked = await listTrackedFiles(root);
146
+ const added = await listAddedLines(shaRoot, scope.codeTargets, scope.turnBase);
147
+ const tracked = await listTrackedFiles(shaRoot);
148
148
  const scan = coreFacade.duplication.scanProject(
149
149
  tracked,
150
150
  (relativePath) => {
151
151
  try {
152
- return readFileSync(`${root}/${relativePath}`, "utf8");
152
+ return readFileSync(`${shaRoot}/${relativePath}`, "utf8");
153
153
  } catch {
154
154
  return null;
155
155
  }
@@ -490,7 +490,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
490
490
  * says so once at the end instead of three times ([/decisions/ad-073.md](/decisions/ad-073.md)).
491
491
  */
492
492
  const deferred: string[] = [];
493
- const changedFiles = await listChangedRepoFiles(root, turnBase);
493
+ const changedFiles = await listChangedRepoFiles(shaRoot, turnBase);
494
494
  const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
495
495
  const testTargets = filterTestTargets(changedFiles);
496
496
  // why: the comment rail scopes by the syntax catalog (40+ languages), not `codeTargets`'s nine-extension
@@ -653,7 +653,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
653
653
  coreFacade.observe.shouldObserve(policy.observe, "comments", policy.comments.enabled)
654
654
  ) {
655
655
  const hits = await coreFacade.commentPolicy.scanAddedComments(
656
- root,
656
+ shaRoot,
657
657
  commentTargets,
658
658
  policy.comments.mode,
659
659
  turnBase,
@@ -680,7 +680,7 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
680
680
 
681
681
  if (policy.comments.enabled && policy.comments.onViolation === "followup" && commentTargets.length > 0) {
682
682
  const hits = await coreFacade.commentPolicy.scanAddedComments(
683
- root,
683
+ shaRoot,
684
684
  commentTargets,
685
685
  policy.comments.mode,
686
686
  turnBase,
@@ -708,13 +708,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
708
708
  if (policy.supplyChain.enabled && changedFiles.length > 0) {
709
709
  const manifests = changedFiles.filter((path) => coreFacade.supplyChain.isManifest(path));
710
710
  if (manifests.length > 0) {
711
- const added = await listAddedLines(root, manifests, turnBase);
711
+ const added = await listAddedLines(shaRoot, manifests, turnBase);
712
712
  const outcome = coreFacade.supplyChain.inspectSupplyChain({
713
713
  changedFiles,
714
714
  added,
715
715
  readManifest: (relativePath) => {
716
716
  try {
717
- return readFileSync(join(root, relativePath), "utf8");
717
+ return readFileSync(join(shaRoot, relativePath), "utf8");
718
718
  } catch {
719
719
  return null;
720
720
  }
@@ -742,13 +742,13 @@ export const stopHandler: Handler = async (event: HarnessEvent, ctx: HandlerCont
742
742
  * ([/decisions/ad-071.md](/decisions/ad-071.md)).
743
743
  */
744
744
  if (policy.duplication.enabled && codeTargets.length > 0) {
745
- const added = await listAddedLines(root, codeTargets, turnBase);
746
- const tracked = await listTrackedFiles(root);
745
+ const added = await listAddedLines(shaRoot, codeTargets, turnBase);
746
+ const tracked = await listTrackedFiles(shaRoot);
747
747
  const scan = coreFacade.duplication.scanProject(
748
748
  tracked,
749
749
  (relativePath) => {
750
750
  try {
751
- return readFileSync(join(root, relativePath), "utf8");
751
+ return readFileSync(join(shaRoot, relativePath), "utf8");
752
752
  } catch {
753
753
  return null;
754
754
  }
@@ -94,9 +94,13 @@ export type TurnScope = {
94
94
  *
95
95
  * invariant: `turn_base_sha` falls back to `HEAD` when the handoff is absent or its seal diverged —
96
96
  * identical to `stop.ts`'s own fallback.
97
+ * why: `root` is state (worktree-stable, AD-114); `gitRoot` is where the turn's files actually are
98
+ * (`shaScopeRoot(event)`) — diffing a worktree-valid sha from the wrong directory read a whole unrelated
99
+ * branch as added ([/decisions/ad-129.md](/decisions/ad-129.md)).
97
100
  */
98
101
  export async function computeTurnScope(
99
102
  root: string,
103
+ gitRoot: string,
100
104
  provider: string,
101
105
  sessionKey: string,
102
106
  policy: Pick<Policy, "codePaths">,
@@ -104,7 +108,7 @@ export async function computeTurnScope(
104
108
  const seal = coreFacade.handoff.handoffInjectable(root, sessionKey);
105
109
  const handoff = seal.ok ? coreFacade.handoff.readHandoff(root, provider, sessionKey) : undefined;
106
110
  const turnBase = handoff?.turn_base_sha ?? "HEAD";
107
- const changedFiles = await listChangedRepoFiles(root, turnBase);
111
+ const changedFiles = await listChangedRepoFiles(gitRoot, turnBase);
108
112
  const codeTargets = filterCodeTargets(changedFiles, policy.codePaths);
109
113
  const testTargets = filterTestTargets(changedFiles);
110
114
  const commentScope = changedFiles.filter((file) =>
@@ -128,6 +132,7 @@ export async function computeTurnScope(
128
132
  */
129
133
  export async function pendingCommentViolations(
130
134
  root: string,
135
+ gitRoot: string,
131
136
  provider: string,
132
137
  sessionKey: string,
133
138
  policy: Pick<Policy, "comments" | "codePaths">,
@@ -135,12 +140,12 @@ export async function pendingCommentViolations(
135
140
  if (!policy.comments.enabled || policy.comments.onViolation !== "followup") {
136
141
  return [];
137
142
  }
138
- const scope = await computeTurnScope(root, provider, sessionKey, policy);
143
+ const scope = await computeTurnScope(root, gitRoot, provider, sessionKey, policy);
139
144
  if (scope.commentTargets.length === 0) {
140
145
  return [];
141
146
  }
142
147
  return coreFacade.commentPolicy.scanAddedComments(
143
- root,
148
+ gitRoot,
144
149
  scope.commentTargets,
145
150
  policy.comments.mode,
146
151
  scope.turnBase,
@@ -26,7 +26,13 @@ async function commentGateBeforeCommit(event: HarnessEvent, ctx: HandlerContext)
26
26
  if (!coreFacade.rules.triggerMatches({ kind: "commit" }, context)) {
27
27
  return { kind: "abstain" };
28
28
  }
29
- const hits = await pendingCommentViolations(event.projectDir, event.provider, event.sessionKey, ctx.policy);
29
+ const hits = await pendingCommentViolations(
30
+ event.projectDir,
31
+ shaScopeRoot(event),
32
+ event.provider,
33
+ event.sessionKey,
34
+ ctx.policy,
35
+ );
30
36
  if (hits.length === 0) {
31
37
  return { kind: "abstain" };
32
38
  }