@tech-leads-club/harness-toolkit 0.10.0 → 0.10.2

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.
@@ -27,13 +27,24 @@ export type TriggerContext = {
27
27
  * every repository, or a rule copied between them silently stops firing. An operator who wants their own shape
28
28
  * writes `command(<pattern>)`.
29
29
  */
30
- const SHELL_SHAPES: Record<"pr-open" | "commit" | "push", readonly string[][]> = {
30
+ type ShellShape = {
31
+ readonly prefix: readonly string[];
32
+ /**
33
+ * why this exists only on `gh pr create`: a draft is not yet open for review, so a rule gating `pr-open`
34
+ * has no work to demand proof of. It is also the only way a proof that itself depends on the pull request
35
+ * existing — `gh pr view`, for one — can ever run: open the draft, produce the proof against it, then
36
+ * `gh pr ready`, which keeps its own gate ([/decisions/ad-118.md](/decisions/ad-118.md)).
37
+ */
38
+ readonly excludeIfAny?: readonly string[];
39
+ };
40
+
41
+ const SHELL_SHAPES: Record<"pr-open" | "commit" | "push", readonly ShellShape[]> = {
31
42
  "pr-open": [
32
- ["gh", "pr", "create"],
33
- ["gh", "pr", "ready"],
43
+ { prefix: ["gh", "pr", "create"], excludeIfAny: ["--draft", "-d"] },
44
+ { prefix: ["gh", "pr", "ready"] },
34
45
  ],
35
- commit: [["git", "commit"]],
36
- push: [["git", "push"]],
46
+ commit: [{ prefix: ["git", "commit"] }],
47
+ push: [{ prefix: ["git", "push"] }],
37
48
  };
38
49
 
39
50
  /**
@@ -53,8 +64,15 @@ function subCommands(command: string): string[][] {
53
64
  }
54
65
 
55
66
  /** why prefix rather than equality: `gh pr create --fill --base main` is the same act as `gh pr create`. */
56
- function startsWithShape(words: readonly string[], shape: readonly string[]): boolean {
57
- return shape.every((token, index) => words[index] === token);
67
+ function startsWithShape(words: readonly string[], prefix: readonly string[]): boolean {
68
+ return prefix.every((token, index) => words[index] === token);
69
+ }
70
+
71
+ function matchesShape(words: readonly string[], shape: ShellShape): boolean {
72
+ if (!startsWithShape(words, shape.prefix)) {
73
+ return false;
74
+ }
75
+ return !shape.excludeIfAny?.some((flag) => words.includes(flag));
58
76
  }
59
77
 
60
78
  /**
@@ -82,9 +100,7 @@ export function triggerMatches(trigger: RuleTrigger, context: TriggerContext): b
82
100
  return false;
83
101
  }
84
102
  const shapes = SHELL_SHAPES[trigger.kind];
85
- return subCommands(context.command).some((words) =>
86
- shapes.some((shape) => startsWithShape(words, shape)),
87
- );
103
+ return subCommands(context.command).some((words) => shapes.some((shape) => matchesShape(words, shape)));
88
104
  }
89
105
  default: {
90
106
  if (context.command === undefined) {
@@ -2,7 +2,7 @@ import type { 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 { currentGitSha, obsConfigFor } from "./support.ts";
5
+ import { currentGitSha, obsConfigFor, shaScopeRoot } from "./support.ts";
6
6
 
7
7
  export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: HandlerContext) => {
8
8
  coreFacade.observability.recordFromEvent(event.projectDir, obsConfigFor(ctx.policy), event);
@@ -11,7 +11,10 @@ export const promptSubmitHandler: Handler = async (event: HarnessEvent, ctx: Han
11
11
  coreFacade.untrusted.clearRecall(event.projectDir, event.sessionKey);
12
12
  // why: every stop-time gate diffs against this, not against HEAD. A turn that commits moves HEAD past its
13
13
  // own changes, and each gate then read an empty diff and skipped ([/decisions/ad-058.md](/decisions/ad-058.md)).
14
- const sha = await currentGitSha(event.projectDir);
14
+ // why: the exact AD-114 divergence, now against turn_base_sha instead of a rule's proof sha — a worktree
15
+ // session recorded the main checkout's HEAD as the turn's start, so every gate diffing against it saw a
16
+ // whole untouched file read as "added this turn" ([/decisions/ad-117.md](/decisions/ad-117.md)).
17
+ const sha = await currentGitSha(shaScopeRoot(event));
15
18
  if (sha) {
16
19
  await coreFacade.handoff.patchHandoff(event.projectDir, event.provider, {
17
20
  slice: { turn_base_sha: sha },