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

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/concepts.md CHANGED
@@ -336,6 +336,13 @@ subcommands are refused from inside a session.
336
336
  `since HEAD` compares the sha the observation was made against with the current one, so a review followed by
337
337
  another commit is stale. A project with no git checkout cannot satisfy `since HEAD` at all.
338
338
 
339
+ A denial says which of two things happened: nothing of that kind and value was ever observed here (a flat
340
+ `missing …`), or it was, just not inside the window (`missing … (ran, but at a different commit)` /
341
+ `(ran, but in a different session)` / `(ran, but this project is not a git checkout, so since HEAD can never
342
+ be satisfied)`). The first of those three is common with more than one branch checked out of the same working
343
+ directory in turn — the proof is real, it is just stamped against whichever commit was checked out when it
344
+ ran, not the one the current command has in mind.
345
+
339
346
  Each proof kind matches differently — picking the wrong one for what you actually want to check is the
340
347
  most common way a new rule reads as protection and is not:
341
348
 
package/docs/log.md CHANGED
@@ -20,6 +20,7 @@ newest first. For what landed in which npm release, see `CHANGELOG.md` at the re
20
20
  - **AD-116** — push and gh pr create run the stop-time battery before shipping, identically on every provider ([/decisions/ad-116.md](/decisions/ad-116.md))
21
21
  - **AD-117** — turn_base_sha resolves sha from the event's own working directory, not the project root ([/decisions/ad-117.md](/decisions/ad-117.md))
22
22
  - **AD-118** — pr-open does not fire on a draft, so a proof needing the pull request to exist has a way to run ([/decisions/ad-118.md](/decisions/ad-118.md))
23
+ - **AD-119** — A rule's denial says whether the proof never ran or ran outside the window ([/decisions/ad-119.md](/decisions/ad-119.md))
23
24
 
24
25
  ## 2026-08-26
25
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tech-leads-club/harness-toolkit",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "description": "Multi-provider agent steering: gates, follow-up, handoff, policy",
6
6
  "keywords": [
@@ -84,17 +84,43 @@ export function proofSatisfied(
84
84
  );
85
85
  }
86
86
 
87
+ /**
88
+ * why this exists at all: "missing" reads the same whether the thing never ran or it ran and the window rejected
89
+ * it, and only one of those is a config-side fact worth stating rather than a guess. The fact is cheap and always
90
+ * true — an observation of this kind and value exists — so it is stated whenever it holds
91
+ * ([/decisions/ad-060.md](/decisions/ad-060.md)'s own distinction between a fact and a diagnosis).
92
+ */
93
+ function staleReason(proof: RuleProof, context: ProofContext): string {
94
+ if (proof.since === "session") {
95
+ return "ran, but in a different session";
96
+ }
97
+ return context.sha === null
98
+ ? "ran, but this project is not a git checkout, so since HEAD can never be satisfied"
99
+ : "ran, but at a different commit";
100
+ }
101
+
102
+ export type MissingProof = { proof: RuleProof; reason: string | null };
103
+
87
104
  /** invariant: every proof must hold. The list is a conjunction, which is why there is no boolean algebra. */
88
105
  export function missingProofs(
89
106
  rule: Rule,
90
107
  observations: readonly Observation[],
91
108
  context: ProofContext,
92
- ): RuleProof[] {
93
- return rule.require.filter((proof) => !proofSatisfied(proof, observations, context));
109
+ ): MissingProof[] {
110
+ return rule.require
111
+ .filter((proof) => !proofSatisfied(proof, observations, context))
112
+ .map((proof) => ({
113
+ proof,
114
+ reason: observations.some((observation) => valueMatches(proof, observation))
115
+ ? staleReason(proof, context)
116
+ : null,
117
+ }));
94
118
  }
95
119
 
96
- export function proofLabel(proof: RuleProof): string {
97
- return `${proof.kind}(${proof.value}) since ${proof.since === "head" ? "HEAD" : "session"}`;
120
+ export function proofLabel(missing: MissingProof): string {
121
+ const { proof, reason } = missing;
122
+ const base = `${proof.kind}(${proof.value}) since ${proof.since === "head" ? "HEAD" : "session"}`;
123
+ return reason === null ? base : `${base} (${reason})`;
98
124
  }
99
125
 
100
126
  /**