pi-aia-asf 0.17.5 → 0.17.6
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 +48 -0
- package/lib/activation.mjs +0 -2
- package/lib/extension/evidence.ts +24 -0
- package/lib/extension/handlers/tool-call.ts +10 -6
- package/lib/extension/handlers/tool-result.ts +6 -2
- package/lib/gates/blockers.d.mts +2 -3
- package/lib/gates/blockers.mjs +24 -0
- package/package.json +1 -1
- package/skills/aia-asf/references/06i-completeness.md +1 -1
- package/skills/aia-asf/references/07-release.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.6] - 2026-11-09
|
|
11
|
+
|
|
12
|
+
### Fixed — a gate that crashes is not a gate that passed
|
|
13
|
+
|
|
14
|
+
Both tool handlers were fenced with `catch { return undefined }`, which had two consequences of
|
|
15
|
+
exactly the kind ASF bans:
|
|
16
|
+
|
|
17
|
+
- A failure inside the **suite recorder** lost the run silently, after which the publish and
|
|
18
|
+
completion floors demanded a run that could never register — an unclosable gap with no visible
|
|
19
|
+
cause.
|
|
20
|
+
- Because pi's own semantics for a crashing `tool_call` handler are *block*, returning "allow"
|
|
21
|
+
overrode them: anything that made the earlier code throw — a config read, the activation
|
|
22
|
+
observer — disabled the **irreversibility floor** for that call.
|
|
23
|
+
|
|
24
|
+
Now:
|
|
25
|
+
|
|
26
|
+
- `reportGateError(gate, e)` records a `gate-error` ledger entry and tells the agent once, as a
|
|
27
|
+
steer: *"That is not a pass — the check failed to execute."* Guidance, never a stop.
|
|
28
|
+
- When the gate dies **before** the blockers ran, `failSafeBlock(input)` re-runs just the rules
|
|
29
|
+
whose `failSafe` is `block`, from the raw tool input alone, with no context to fail on. A secret
|
|
30
|
+
in a tracked file is still refused when the gate itself is broken; ordinary work is never
|
|
31
|
+
stopped by an internal error.
|
|
32
|
+
- A fail-safe rule that *itself* throws refuses ("could not run") rather than reporting nothing
|
|
33
|
+
found.
|
|
34
|
+
|
|
35
|
+
Three assertions pin this, each mutation-tested: removing the report, the fail-closed call, or the
|
|
36
|
+
crash-refusal each fails a test.
|
|
37
|
+
|
|
38
|
+
- `lib/gates/blockers.d.mts` still declared `RULE_TEXT`, `refusalStage` and `__resetRefusals` after
|
|
39
|
+
they moved to `lib/gates/refusals.mjs` in 0.17.5 — a `.ts` could import them, typecheck, and get
|
|
40
|
+
`undefined` at runtime. The declarations now match the modules.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- Dead export `SOURCE_EXTENSIONS` from `lib/activation.mjs` (no importer).
|
|
45
|
+
|
|
46
|
+
### Documented
|
|
47
|
+
|
|
48
|
+
- **The loaded copy is not the shipped copy** (07-release.md step 7 + hygiene rule): `pi` loads
|
|
49
|
+
extensions once at process start, so a session open across a release keeps enforcing the previous
|
|
50
|
+
version and quoting text that no longer exists in the source. Confirm which build is loaded — a
|
|
51
|
+
string unique to the new release, and the process start time — before concluding a released gate
|
|
52
|
+
is broken. That is the difference between "the fix did not work" and "the fix is not running", and
|
|
53
|
+
it cost hours chasing a phantom bug in 0.17.5.
|
|
54
|
+
- `06i-completeness.md` pointed its drift example at `lib/gates/ledger.mjs` / `deriveSuiteRun()`;
|
|
55
|
+
those moved to `lib/gates/suite-runs.mjs` in 0.17.5, and the doc — which is about not trusting
|
|
56
|
+
stale references — was itself stale.
|
|
57
|
+
|
|
10
58
|
## [0.17.5] - 2026-11-08
|
|
11
59
|
|
|
12
60
|
### Fixed — the publish floor could be satisfied by a green that proved nothing
|
package/lib/activation.mjs
CHANGED
|
@@ -27,6 +27,30 @@ export function queueSteer(text: string) {
|
|
|
27
27
|
pendingSteers.push(text);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* An internal gate error must never be able to look like a pass.
|
|
32
|
+
*
|
|
33
|
+
* Every ASF handler is fenced, because an observer must not break the work it
|
|
34
|
+
* watches. Fencing *without* reporting is exactly how the 2026-11-07 loop became
|
|
35
|
+
* possible: a gate demanded something, the code that would have shown why
|
|
36
|
+
* swallowed its own error, and the agent was steered forever toward an action
|
|
37
|
+
* that could not register. So a swallowed error is (a) recorded as evidence and
|
|
38
|
+
* (b) told to the agent once, as a steer — guidance, never a stop.
|
|
39
|
+
*
|
|
40
|
+
* The settle gate has a stronger channel (it returns the entry inline, so the
|
|
41
|
+
* agent sees it in the same turn); the tool handlers do not, so they use this.
|
|
42
|
+
*/
|
|
43
|
+
export function reportGateError(gate: string, e: unknown) {
|
|
44
|
+
const message = String((e as Error | null)?.message || e || "unknown");
|
|
45
|
+
appendEvidence({ kind: "gate-error", gate, error: message.slice(0, 300) });
|
|
46
|
+
const cls = (e as { code?: string } | null)?.code || (e as Error | null)?.name || "unknown";
|
|
47
|
+
queueSteer(
|
|
48
|
+
`ASF: the ${gate} hit an internal error (${cls}: ${message.slice(0, 140)}) and did not run. ` +
|
|
49
|
+
`That is not a pass — the check failed to execute. Re-issue the action; if it repeats, ` +
|
|
50
|
+
`this is an ASF bug to report, not a rule to satisfy.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
|
|
31
55
|
export function appendEvidence(entry: Record<string, unknown>) {
|
|
32
56
|
const res = ledger.append(entry);
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { classifyToolCall, activationDecision } from "../../activation.mjs";
|
|
18
|
-
import { evaluateBlockers, BLOCKERS } from "../../gates/blockers.mjs";
|
|
18
|
+
import { evaluateBlockers, BLOCKERS, failSafeBlock } from "../../gates/blockers.mjs";
|
|
19
19
|
import { deriveSuiteRun } from "../../gates/suite-runs.mjs";
|
|
20
20
|
import { readProjectConfig } from "../../gates/config.mjs";
|
|
21
21
|
import { projectName, runRoot } from "../paths.ts";
|
|
22
22
|
import { loadState, saveState } from "../state.ts";
|
|
23
23
|
import { openAutoRun, gitDirty, numberConfig } from "../run.ts";
|
|
24
|
-
import { appendEvidence, queueSteer } from "../evidence.ts";
|
|
24
|
+
import { appendEvidence, queueSteer, reportGateError } from "../evidence.ts";
|
|
25
25
|
import { gateStats, pendingSuiteRuns, suiteRuns, recordVerdict } from "../gate-state.ts";
|
|
26
26
|
import { authority } from "../gate-state.ts";
|
|
27
27
|
import { runtime, BLIND_EDIT_NUDGE_AFTER } from "../runtime.ts";
|
|
@@ -230,10 +230,14 @@ export function registerToolCallHandler(pi: ExtensionAPI): void {
|
|
|
230
230
|
if (park) await parkRun(park);
|
|
231
231
|
if (!res.block) return undefined;
|
|
232
232
|
return await handleBlock({ pi, res: res as Refusal["res"], park, config });
|
|
233
|
-
} catch {
|
|
234
|
-
// An observer must never break the thing it observes
|
|
235
|
-
// the
|
|
236
|
-
|
|
233
|
+
} catch (e) {
|
|
234
|
+
// An observer must never break the thing it observes — so this does not
|
|
235
|
+
// stop the work. But it must not look like a clean pass either, and it must
|
|
236
|
+
// not hand anyone a way past the irreversibility floor: an error raised
|
|
237
|
+
// before the blockers ran is re-checked against the rules that guard
|
|
238
|
+
// irreversible actions, from the raw tool input alone.
|
|
239
|
+
reportGateError("tool_call gate", e);
|
|
240
|
+
return failSafeBlock((event as { input?: Input } | null)?.input || {});
|
|
237
241
|
}
|
|
238
242
|
});
|
|
239
243
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { deriveSuiteRun } from "../../gates/suite-runs.mjs";
|
|
12
|
-
import { appendEvidence } from "../evidence.ts";
|
|
12
|
+
import { appendEvidence, reportGateError } from "../evidence.ts";
|
|
13
13
|
import { pendingSuiteRuns, suiteRuns } from "../gate-state.ts";
|
|
14
14
|
import { runtime } from "../runtime.ts";
|
|
15
15
|
|
|
@@ -52,7 +52,11 @@ export function registerToolResultHandler(pi: ExtensionAPI): void {
|
|
|
52
52
|
pendingSuiteRuns.delete(id);
|
|
53
53
|
completeSuiteRun(ev, pending);
|
|
54
54
|
return undefined;
|
|
55
|
-
} catch {
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Swallowing this one silently is how the publish floor becomes an
|
|
57
|
+
// unclosable gap: the run happened, the recorder failed, and the gate then
|
|
58
|
+
// demands a run that can never register.
|
|
59
|
+
reportGateError("suite recorder", e);
|
|
56
60
|
return undefined;
|
|
57
61
|
}
|
|
58
62
|
});
|
package/lib/gates/blockers.d.mts
CHANGED
|
@@ -34,9 +34,8 @@ export function evaluateBlockers(
|
|
|
34
34
|
verdicts: BlockerVerdict[];
|
|
35
35
|
steers?: Array<{ gate: string; reason: string }>;
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
export function
|
|
39
|
-
export function __resetRefusals(): void;
|
|
37
|
+
/** Re-run the fail-safe-block rules from the raw input alone, when the gate itself could not run. */
|
|
38
|
+
export function failSafeBlock(input: Record<string, unknown>): { block: true; reason: string } | undefined;
|
|
40
39
|
export function blockSecretWrite(input: Record<string, unknown>, ctx?: Record<string, unknown>): BlockerVerdict;
|
|
41
40
|
export function blockTestSubset(input: Record<string, unknown>, ctx?: Record<string, unknown>): BlockerVerdict;
|
|
42
41
|
export function blockPushWithoutGreen(input: Record<string, unknown>, ctx?: Record<string, unknown>): BlockerVerdict;
|
package/lib/gates/blockers.mjs
CHANGED
|
@@ -160,6 +160,30 @@ export const BLOCKERS = [
|
|
|
160
160
|
{ name: "destructive-git-dirty-tree", run: blockDestructiveGitDirty, failSafe: "allow", switch: "gate.block.destructiveGit", criticality: "medium", bypassable: true, mode: "block" },
|
|
161
161
|
];
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* The floor that must not fail open.
|
|
165
|
+
*
|
|
166
|
+
* A fenced handler that returns "allow" on its own internal error hands anyone
|
|
167
|
+
* who can make the *other* code throw a way past the irreversibility floor — the
|
|
168
|
+
* secret rule would be disabled by a bug in a config read. So when the gate
|
|
169
|
+
* could not run, the rules whose `failSafe` is `block` are re-run from the raw
|
|
170
|
+
* tool input alone, with no context to fail on; and if even one of those throws,
|
|
171
|
+
* that is a refusal, not a pass.
|
|
172
|
+
*/
|
|
173
|
+
export function failSafeBlock(input) {
|
|
174
|
+
for (const b of BLOCKERS) {
|
|
175
|
+
if (b.failSafe !== "block") continue;
|
|
176
|
+
let res;
|
|
177
|
+
try {
|
|
178
|
+
res = b.run(input, {});
|
|
179
|
+
} catch {
|
|
180
|
+
res = { fired: true, reason: `${b.name} could not run, and it guards an irreversible action.` };
|
|
181
|
+
}
|
|
182
|
+
if (res?.fired) return { block: true, reason: `${b.name}: ${res.reason || "refused"}` };
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
163
187
|
function str(input, keys) {
|
|
164
188
|
if (!input || typeof input !== "object") return null;
|
|
165
189
|
for (const k of keys) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-aia-asf",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.6",
|
|
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",
|
|
@@ -60,7 +60,7 @@ goes in the final message in plain words — never discovered by the user throug
|
|
|
60
60
|
## Your own artifacts are part of the deliverable (task-scoped drift)
|
|
61
61
|
|
|
62
62
|
The plan and the spec tree are artifacts too, and they make checkable claims: a PLAN.md
|
|
63
|
-
that names `lib/gates/
|
|
63
|
+
that names `lib/gates/suite-runs.mjs` or `deriveSuiteRun()` is asserting those exist. When the
|
|
64
64
|
code has moved and the artifact has not, the delivery report describes something that is
|
|
65
65
|
not there — which is the same defect as a stub, just further from the user's eye.
|
|
66
66
|
|
|
@@ -63,6 +63,13 @@ in the absence of a user definition.
|
|
|
63
63
|
- **Deployed app:** smoke-test in **production** (health endpoint, key user journey) — a deploy that breaks prod is not a successful release.
|
|
64
64
|
- **Not applicable** (internal-only experiment, no runtime surface): say so explicitly in the report (06b Rule 11).
|
|
65
65
|
- If the published artifact is broken: **fix forward** (new patch), never pretend the release succeeded.
|
|
66
|
+
- **The copy you are testing may not be the copy you shipped.** `pi` loads extensions once at
|
|
67
|
+
process start: a session that was already open keeps enforcing the *previous* version's
|
|
68
|
+
gates no matter what you publish, and its messages quote text that no longer exists in the
|
|
69
|
+
source. Before concluding a released gate is broken, confirm which build is loaded — compare
|
|
70
|
+
a string unique to the new release against the behaviour you saw, and check the process
|
|
71
|
+
start time (`ps -o lstart -p <pid>`). `/reload`, or a new session, is what picks up the new
|
|
72
|
+
code. This is the difference between "the fix did not work" and "the fix is not running".
|
|
66
73
|
|
|
67
74
|
## CI/CD offer
|
|
68
75
|
|
|
@@ -79,3 +86,4 @@ After a successful release (or for a new project), **offer** to set up a CI/CD p
|
|
|
79
86
|
- The version in package.json, the git tag, and the npm release must match
|
|
80
87
|
- If publishing fails partway, fix forward; never publish a second version with the same number
|
|
81
88
|
- **Test on publish / in production, if applicable — always** (step 7)
|
|
89
|
+
- **Never diagnose a gate from a session that predates the release** — confirm the loaded build first (step 7)
|