pi-aia-asf 0.17.11 → 0.17.12

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/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.12] - 2026-11-10
11
+
12
+ ### Fixed — the health gate measured the wrong project
13
+
14
+ `runRoot()` is the git root of the session's cwd. That is right for where run state
15
+ lives and wrong for what to measure, and a session launched from a home directory that
16
+ is itself a git repo (common: `pi` started from $HOME with checkouts underneath) had
17
+ every health gate measure the parent. It did: `asf_health` reported 363 model findings
18
+ from an unrelated project checked out under $HOME, and eslint, madge and jscpd each
19
+ timed out at 120s scanning a tree nobody was working in — a verdict of NOT healthy on a
20
+ repository that is, in fact, healthy.
21
+
22
+ A gate that measures the wrong directory is worse than no gate: it blocks on findings
23
+ in code the run must not touch, or waves through the one project that is actually
24
+ unhealthy. `measureRoot()` now answers from what the run already knows — the git root of
25
+ the most recently touched file — and is used by the three paths that measure
26
+ (`asf_health`, `/asf health`, `/asf verify` Gate 8). It is deliberately **not** used to
27
+ locate state: a root that moved mid-run would orphan the run's own ledger, so
28
+ `runRoot()` still follows the session for that.
29
+
30
+ A touched path outside the workspace (a scratch dir, an unrelated checkout) does not
31
+ move the measurement either — the session root is the safe answer. `test/test-measure-root.mjs`
32
+ pins both on throwaway repos rather than this machine's $HOME, and both guards are
33
+ mutation-caught.
34
+
10
35
  ## [0.17.11] - 2026-11-10
11
36
 
12
37
  ### Added — the coverage invariant is checkable from the installed package
@@ -7,7 +7,7 @@ import { projectName, runRoot } from "../paths.ts";
7
7
  import { loadState } from "../state.ts";
8
8
  import { ledger, authority, gateStats } from "../gate-state.ts";
9
9
  import { runtime } from "../runtime.ts";
10
- import { cardFor, collectMeasurements, extensionDir } from "../run.ts";
10
+ import { cardFor, collectMeasurements, extensionDir, measureRoot } from "../run.ts";
11
11
  import { renderLedger } from "../../gates/ledger.mjs";
12
12
  import { renderAuthority } from "../../gates/authority.mjs";
13
13
  import { renderCompletionGate } from "../../gates/settle.mjs";
@@ -41,7 +41,7 @@ export async function cmdStatus(): Promise<string> {
41
41
  export async function cmdHealth(args: string[]): Promise<string> {
42
42
  const { runHealth } = await import("../../code-health/index.mjs");
43
43
  const { recordHealthVerdict, recordLine } = await import("../health-record.ts");
44
- const res = await runHealth(runRoot(), args.slice(1));
44
+ const res = await runHealth(await measureRoot(), args.slice(1));
45
45
  // The command records what it measured. Before this, only the asf_health TOOL
46
46
  // recorded: a human could run /asf health, read HEALTHY, and leave the
47
47
  // completion gate's health gap open with no trace that anything was measured.
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { projectName, runRoot } from "../paths.ts";
9
+ import { measureRoot } from "../run.ts";
9
10
  import { loadState } from "../state.ts";
10
11
  import { loadProjectSpecs, validateTrace } from "../specs.ts";
11
12
  import { QA_CHECKLIST, type SpecItem } from "../types.ts";
@@ -56,7 +57,7 @@ async function traceabilitySection(): Promise<string[]> {
56
57
  async function healthSection(): Promise<string[]> {
57
58
  const { runHealth } = await import("../../code-health/index.mjs");
58
59
  const { recordHealthVerdict, recordLine } = await import("../health-record.ts");
59
- const health = await runHealth(runRoot(), []);
60
+ const health = await runHealth(await measureRoot(), []);
60
61
  // Same rule as the tool and the slash command: a measurement that is produced
61
62
  // is a measurement that is recorded.
62
63
  const rec = recordHealthVerdict(health, { by: "verify" });
@@ -4,16 +4,16 @@
4
4
  */
5
5
 
6
6
  import { execFileSync } from "node:child_process";
7
+ import { runRoot, projectName } from "./paths.ts";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { dirname } from "node:path";
9
- import { runRoot } from "./paths.ts";
10
+ import { projectRoot as projectRootOf } from "../project.mjs";
10
11
  import { ensureHome } from "../home.mjs";
11
12
  import { cpus, loadavg } from "node:os";
12
13
  import { renderRunCard, cardSummary, CARD_CEILING_BYTES } from "../run-card.mjs";
13
14
  import { ledger, suiteRuns, gateStats } from "./gate-state.ts";
14
15
  import { appendEvidence } from "./evidence.ts";
15
16
  import { loadState, saveState } from "./state.ts";
16
- import { projectName } from "./paths.ts";
17
17
  import { runtime } from "./runtime.ts";
18
18
  import { __resetRefusals } from "../gates/refusals.mjs";
19
19
  import { cardIndex, registerAt } from "../decisions/consult.mjs";
@@ -118,6 +118,43 @@ export async function openAutoRun(trigger: string, path: string | null): Promise
118
118
  return state;
119
119
  }
120
120
 
121
+ /**
122
+ * The project the gates should MEASURE.
123
+ *
124
+ * `runRoot()` is the git root of the session's cwd, which is right for where state
125
+ * lives and wrong for what to measure: a session started in a parent repository
126
+ * (a home directory that is itself a git repo is the common case) would have every
127
+ * gate measure that parent. It did — `asf_health` reported 363 model findings from an
128
+ * unrelated project checked out underneath $HOME, and every measurement tool then
129
+ * timed out scanning a tree nobody was working in. A gate that measures the wrong
130
+ * directory is worse than no gate: it blocks on findings in code the run must not
131
+ * touch, or waves through the one project that is actually unhealthy.
132
+ *
133
+ * The run already knows what it changed, so the answer is taken from there: the git
134
+ * root of the most recently touched file wins. It is deliberately NOT used to locate
135
+ * state — a root that moved mid-run would orphan the run's own ledger — only to
136
+ * decide what a measurement looks at.
137
+ */
138
+ export async function measureRoot(): Promise<string> {
139
+ const fallback = runRoot();
140
+ try {
141
+ const state = await loadState(projectName());
142
+ const touched = (state?.current?.filesTouched || []) as string[];
143
+ for (let i = touched.length - 1; i >= 0; i--) {
144
+ const f = touched[i];
145
+ if (!f || !f.includes("/")) continue;
146
+ const root = projectRootOf(dirname(f));
147
+ // Only a root that CONTAINS the session root counts as a better answer: a
148
+ // stray touched path outside the workspace must not drag the measurement to
149
+ // someone's scratch directory.
150
+ if (root && (root === fallback || root.startsWith(fallback + "/"))) return root;
151
+ }
152
+ } catch {
153
+ /* measurement falls back to the session root; a broken guess is worse than none */
154
+ }
155
+ return fallback;
156
+ }
157
+
121
158
  /** Where this extension is installed — the declaration file ships beside it. */
122
159
 
123
160
  export function extensionDir(): string {
@@ -27,12 +27,12 @@ interface HealthResult {
27
27
 
28
28
  async function runHealthGate(scope: string): Promise<HealthResult> {
29
29
  const { runHealth } = await import("../../code-health/index.mjs");
30
- const { runRoot } = await import("../paths.ts");
30
+ const { measureRoot } = await import("../run.ts");
31
31
  const args = scope ? [`--scope=${scope}`] : [];
32
- // Measure the RUN's project, not process.cwd(): a session started in $HOME
33
- // (or any parent) would otherwise measure everything under it — 960 phantom
34
- // findings from site-packages, on a repo that is actually healthy.
35
- return (await runHealth(runRoot(), args)) as HealthResult;
32
+ // Measure the project the RUN is about, not process.cwd() and not the session
33
+ // cwd's git root: a session started in $HOME (which is itself a repo here) would
34
+ // otherwise measure every checkout underneath it.
35
+ return (await runHealth(await measureRoot(), args)) as HealthResult;
36
36
  }
37
37
 
38
38
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-aia-asf",
3
- "version": "0.17.11",
3
+ "version": "0.17.12",
4
4
  "description": "Ai Applied Agentic Software Factory — codifies the full software development flow: intake, research, spec capture, adversarial analysis, planning with approval gates, test-first implementation, and release. Requires pi-vigilant, pi-smart-web-search, pi-smart-fetch, and pi-aia-browser.",
5
5
  "keywords": [
6
6
  "pi-package",