pi-aia-asf 0.17.7 → 0.17.8
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,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.8] - 2026-11-09
|
|
11
|
+
|
|
12
|
+
### Added — the tracked-work vocabulary is the project's to widen
|
|
13
|
+
|
|
14
|
+
`gate.tracked.claim` / `.resolve` / `.park` / `.wait` in `.asf/config.json` add patterns
|
|
15
|
+
(string or array) to the built-in matcher set. They **widen, never replace**: a project
|
|
16
|
+
that typos `claim` would otherwise stop tracking its own claims, and a floor that a
|
|
17
|
+
stray character in a config file can switch off is not a floor. Non-string entries are
|
|
18
|
+
ignored rather than compiled in.
|
|
19
|
+
|
|
20
|
+
`lib/gates/tracked.mjs` now says plainly what it is — **a tripwire over tool names and
|
|
21
|
+
the agent's own prose, not an authority over ticket state**. It cannot see a tracker's
|
|
22
|
+
database and its vocabulary is a small illustrative set; a tool named `grabWorkItem` is
|
|
23
|
+
invisible until the project says so. The asymmetry is deliberate: a miss costs a run
|
|
24
|
+
that settles early, a false alarm costs one steer.
|
|
25
|
+
|
|
26
|
+
### Fixed — a matcher that matched everything
|
|
27
|
+
|
|
28
|
+
`matchersFromConfig()` built the wait matcher as `alternatives + "|" + extras` even when
|
|
29
|
+
`extras` was empty, leaving a trailing `|`. An empty alternative matches the empty
|
|
30
|
+
string, so **every progress message in every project would have been read as a
|
|
31
|
+
confessed wait** — a gate that trips on everything is as useless as one that trips on
|
|
32
|
+
nothing, and much harder to notice. Every union is now guarded by a length check, and a
|
|
33
|
+
test pins that no matcher matches `""`, `"anything at all"`, or a config entry of
|
|
34
|
+
`""`/`undefined`/`null`. Caught by the suite that checks a benign progress message is
|
|
35
|
+
not a wait, and mutation-verified: restoring the dangling `|` fails two checks.
|
|
36
|
+
|
|
10
37
|
## [0.17.7] - 2026-11-09
|
|
11
38
|
|
|
12
39
|
### Fixed — "waiting" is now illegal as a disposition
|
|
@@ -17,7 +17,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import { classifyToolCall, activationDecision } from "../../activation.mjs";
|
|
18
18
|
import { evaluateBlockers, BLOCKERS, failSafeBlock } from "../../gates/blockers.mjs";
|
|
19
19
|
import { deriveSuiteRun } from "../../gates/suite-runs.mjs";
|
|
20
|
-
import { noteTrackedCall } from "../../gates/tracked.mjs";
|
|
20
|
+
import { noteTrackedCall, matchersFromConfig } from "../../gates/tracked.mjs";
|
|
21
21
|
import { readProjectConfig } from "../../gates/config.mjs";
|
|
22
22
|
import { projectName, runRoot } from "../paths.ts";
|
|
23
23
|
import { loadState, saveState } from "../state.ts";
|
|
@@ -112,8 +112,8 @@ function observeWebTouch(input: Input): void {
|
|
|
112
112
|
* about it instead. Claim / resolve / park are read off the tool's name; a
|
|
113
113
|
* narrated wait is read off the text the agent wrote for the reviewer.
|
|
114
114
|
*/
|
|
115
|
-
function observeTrackedWork(toolName: string, input: Input): void {
|
|
116
|
-
const kind = noteTrackedCall(runtime.tracked, { toolName, input });
|
|
115
|
+
function observeTrackedWork(toolName: string, input: Input, config: Record<string, unknown>): void {
|
|
116
|
+
const kind = noteTrackedCall(runtime.tracked, { toolName, input, matchers: matchersFromConfig(config) });
|
|
117
117
|
if (!kind) return;
|
|
118
118
|
appendEvidence({ kind: "tracked", action: kind, tool: toolName, item: String(input.ticket_id || input.ticketId || input.id || "") || null });
|
|
119
119
|
if (kind === "wait") {
|
|
@@ -223,6 +223,9 @@ export function registerToolCallHandler(pi: ExtensionAPI): void {
|
|
|
223
223
|
const ev = event as Event;
|
|
224
224
|
const toolName = String(ev.toolName || "");
|
|
225
225
|
const input = ev.input || {};
|
|
226
|
+
// Read once, hand it to whoever needs it: the gate below and the tracked-work
|
|
227
|
+
// matcher both ask, and a second read is a second answer to the same question.
|
|
228
|
+
const config = readProjectConfig(runRoot());
|
|
226
229
|
|
|
227
230
|
// An observer failure here must not block the work, so it is fenced.
|
|
228
231
|
try {
|
|
@@ -232,9 +235,8 @@ export function registerToolCallHandler(pi: ExtensionAPI): void {
|
|
|
232
235
|
}
|
|
233
236
|
observeSuiteAttempt(input, ev);
|
|
234
237
|
observeWebTouch(input);
|
|
235
|
-
observeTrackedWork(toolName, input);
|
|
238
|
+
observeTrackedWork(toolName, input, config);
|
|
236
239
|
|
|
237
|
-
const config = readProjectConfig(runRoot());
|
|
238
240
|
const res = evaluateBlockers(toolName, input, {
|
|
239
241
|
ctx: {
|
|
240
242
|
suiteRuns,
|
package/lib/gates/tracked.d.mts
CHANGED
|
@@ -16,11 +16,21 @@ export interface TrackedState {
|
|
|
16
16
|
|
|
17
17
|
export declare const DISPOSITIONS: string[];
|
|
18
18
|
|
|
19
|
+
export interface TrackedMatchers {
|
|
20
|
+
claim: RegExp;
|
|
21
|
+
resolve: RegExp;
|
|
22
|
+
park: RegExp;
|
|
23
|
+
wait: RegExp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Built-in matchers, widened (never replaced) by `gate.tracked.*` in the project config. */
|
|
27
|
+
export function matchersFromConfig(config?: Record<string, unknown>): TrackedMatchers;
|
|
28
|
+
|
|
19
29
|
export function createTracked(): TrackedState;
|
|
20
30
|
|
|
21
31
|
export function noteTrackedCall(
|
|
22
32
|
tracked: TrackedState,
|
|
23
|
-
call: { toolName?: string; input?: Record<string, unknown
|
|
33
|
+
call: { toolName?: string; input?: Record<string, unknown>; matchers?: TrackedMatchers },
|
|
24
34
|
): "claim" | "resolve" | "park" | "wait" | null;
|
|
25
35
|
|
|
26
36
|
export function danglingGaps(tracked: TrackedState | undefined): string[];
|
package/lib/gates/tracked.mjs
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* So the illegal state is named, and the legal answers are exactly three. They
|
|
18
18
|
* are all actions, and the agent can take every one of them — which is what
|
|
19
19
|
* makes this gap steerable rather than the 2026-11-07 kind that loops forever.
|
|
20
|
+
*
|
|
21
|
+
* What this is: a **tripwire over tool names and the agent's own prose**, not an
|
|
22
|
+
* authority over ticket state. It cannot see a tracker's database, and its
|
|
23
|
+
* vocabulary is a small illustrative set, widened per project through
|
|
24
|
+
* `gate.tracked.*`. A tool named `grabWorkItem` is invisible to it until someone
|
|
25
|
+
* says so. That is acceptable because the cost of a miss is a run that settles
|
|
26
|
+
* early; the cost of a false alarm is one steer.
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
/** The only three things an agent may do with work it cannot finish right now. */
|
|
@@ -57,6 +64,44 @@ const WAITING_ALTERNATIVES = [
|
|
|
57
64
|
];
|
|
58
65
|
const WAITING_RE = new RegExp(WAITING_ALTERNATIVES.join("|"), "i");
|
|
59
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The matchers as one value, so a project can widen them.
|
|
69
|
+
*
|
|
70
|
+
* `gate.tracked.claim` / `.resolve` / `.park` / `.wait` in the project config each
|
|
71
|
+
* add patterns to the built-in set (a string, or an array of strings). They widen,
|
|
72
|
+
* never replace: a project that forgets `claim` would stop tracking its own claims,
|
|
73
|
+
* and a gate that can be switched off by a typo in a config file is not a floor.
|
|
74
|
+
*/
|
|
75
|
+
export function matchersFromConfig(config = {}) {
|
|
76
|
+
const extra = (key) => {
|
|
77
|
+
const v = config[`gate.tracked.${key}`];
|
|
78
|
+
const list = Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
|
|
79
|
+
return list.filter((x) => typeof x === "string" && x.trim()).map((x) => x.trim());
|
|
80
|
+
};
|
|
81
|
+
const build = (base, key) => {
|
|
82
|
+
const add = extra(key);
|
|
83
|
+
return add.length ? new RegExp(`${base.source}|${add.map(escapeRe).join("|")}`, "i") : base;
|
|
84
|
+
};
|
|
85
|
+
// The empty alternative is the trap here: `a|b|` matches the empty string, so a
|
|
86
|
+
// dangling `|` turns "does this text confess a wait" into "yes, always" — a gate
|
|
87
|
+
// that trips on everything is as useless as one that trips on nothing, and it is
|
|
88
|
+
// far harder to notice. Every union below is guarded by `add.length`.
|
|
89
|
+
const waitAdd = extra("wait");
|
|
90
|
+
const waitSource = waitAdd.length
|
|
91
|
+
? `${WAITING_ALTERNATIVES.join("|")}|${waitAdd.map(escapeRe).join("|")}`
|
|
92
|
+
: WAITING_ALTERNATIVES.join("|");
|
|
93
|
+
return {
|
|
94
|
+
claim: build(CLAIM_RE, "claim"),
|
|
95
|
+
resolve: build(RESOLVE_RE, "resolve"),
|
|
96
|
+
park: build(PARK_RE, "park"),
|
|
97
|
+
wait: new RegExp(waitSource, "i"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function escapeRe(source) {
|
|
102
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
/** Identify the item a call is about, so one ticket is one entry. */
|
|
61
106
|
function itemId(input) {
|
|
62
107
|
if (!input || typeof input !== "object") return null;
|
|
@@ -99,22 +144,23 @@ function closeItem(tracked, id) {
|
|
|
99
144
|
* Returns a short label for the evidence trail (`claim` / `resolve` / `park` /
|
|
100
145
|
* `wait` / null). Never throws on odd input: this is an observer on the hot path.
|
|
101
146
|
*/
|
|
102
|
-
export function noteTrackedCall(tracked, { toolName = "", input = {} } = {}) {
|
|
147
|
+
export function noteTrackedCall(tracked, { toolName = "", input = {}, matchers } = {}) {
|
|
148
|
+
const m = matchers || matchersFromConfig();
|
|
103
149
|
if (!tracked || !toolName) return null;
|
|
104
150
|
const id = itemId(input) || `tool:${toolName}`;
|
|
105
|
-
if (
|
|
151
|
+
if (m.park.test(toolName)) {
|
|
106
152
|
closeItem(tracked, id);
|
|
107
153
|
return "park";
|
|
108
154
|
}
|
|
109
|
-
if (
|
|
155
|
+
if (m.resolve.test(toolName)) {
|
|
110
156
|
closeItem(tracked, id);
|
|
111
157
|
return "resolve";
|
|
112
158
|
}
|
|
113
|
-
if (
|
|
159
|
+
if (m.claim.test(toolName)) {
|
|
114
160
|
openItem(tracked, id, toolName);
|
|
115
161
|
return "claim";
|
|
116
162
|
}
|
|
117
|
-
if (NARRATE_RE.test(toolName) &&
|
|
163
|
+
if (NARRATE_RE.test(toolName) && m.wait.test(textOf(input))) {
|
|
118
164
|
// Narrating a wait does not make it legal; it makes it visible to the gate.
|
|
119
165
|
openItem(tracked, id, toolName);
|
|
120
166
|
if (!tracked.waits.includes(id)) tracked.waits.push(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-aia-asf",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.8",
|
|
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",
|
|
@@ -140,6 +140,18 @@ Two things this is **not**: it is not a ban on saying "blocked" honestly (say it
|
|
|
140
140
|
report, with the disposition you took); and it is not a reason to resolve work that is
|
|
141
141
|
not done — a false `resolved` is the worse failure. Park it.
|
|
142
142
|
|
|
143
|
+
It is a **tripwire over tool names and the agent's own prose, not an authority over
|
|
144
|
+
ticket state** — it cannot see a tracker's database, and its vocabulary is a small
|
|
145
|
+
illustrative set. A project widens it (never narrows it) in `.asf/config.json`:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "gate.tracked.claim": "grabWorkItem", "gate.tracked.wait": ["still stuck on"] }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Keys: `gate.tracked.claim`, `.resolve`, `.park`, `.wait`. A miss costs a run that
|
|
152
|
+
settles early; a false alarm costs one steer. That asymmetry is why the defaults lean
|
|
153
|
+
toward catching.
|
|
154
|
+
|
|
143
155
|
## Anti-patterns
|
|
144
156
|
|
|
145
157
|
- ❌ Unbounded retries on non-idempotent writes
|