pi-aia-asf 0.17.6 → 0.17.7

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,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.7] - 2026-11-09
11
+
12
+ ### Fixed — "waiting" is now illegal as a disposition
13
+
14
+ An agent working a claimed ticket ended its run with this on the reviewer's card:
15
+ *"Left In Progress at testing — deliberately not resolved, because the user-visible
16
+ outcome still isn't confirmed, and not parked, since you told me not to ask you for
17
+ anything."* Nothing blocked it. The run closed and the work stopped. That is a stop
18
+ wearing a status report, and it is the failure this whole process exists to prevent.
19
+
20
+ The agent did not misbehave; it had no legal state to stand in. The stop policy names
21
+ four things that may halt a run — plan approval, an irreversible action with no safe
22
+ path, an unobtainable credential, a deliverable-shaping either/or — and **"the other
23
+ side has not replied" is none of them**. A contract that forbids stopping without
24
+ naming what to do instead does not produce persistence, it produces paragraphs. And
25
+ the agent had correctly noticed that parking *looks like* asking, so a "don't ask me"
26
+ directive seemed to forbid the one move that was legal.
27
+
28
+ - **New gate: `lib/gates/tracked.mjs`.** Claims, resolutions and parks are read off the
29
+ tool *name* (`bmx_claim`, `submit_resolution`, `request_input`, …) — never off shell
30
+ text, because `git commit` is not a claim. A claimed item with no disposition is a
31
+ gap that keeps the run open, and the agent's own progress text is read for the
32
+ confession ("deliberately not resolved", "not parked", "waiting for their reply",
33
+ "it flips the moment…"), which raises it on the spot.
34
+ - **The three dispositions, all of which the agent can take itself**: park it where a
35
+ reviewer sees it (parking is a *marker*, not a question, so "don't ask me" never
36
+ forbids it); take the next action you control; or, if you are genuinely waiting on
37
+ someone else, make the wait end in one step — the test that will confirm the outcome,
38
+ the draft mail — and keep going. The gap text offers all three.
39
+ - The gap is worded so it cannot route itself into the report: it names no slash
40
+ command and never says "ask the PO", because a gap that reads like a human action is
41
+ filtered out by `splitGaps` and would make the guard decorative. Pinned by a test.
42
+ - **Contract text now lives where the agent reads it.** The no-stops policy existed only
43
+ in a source docstring; `06i-completeness.md` (always-read) now carries the rule and the
44
+ DoD has a line for it, with the case study in `06f-stability.md` §6. The always-read
45
+ load stayed inside its budget (75,953 / 76,000) — detail moved out to 06f rather than
46
+ the budget being raised.
47
+
48
+ ### Fixed — declaration files that lie about their module
49
+
50
+ `lib/gates/blockers.d.mts` still declared `RULE_TEXT` / `refusalStage` /
51
+ `__resetRefusals` after they moved to `refusals.mjs` (0.17.5); `lib/gates/settle.d.mts`
52
+ still declared `MAX_IDENTICAL_STEERS` after the cap was deleted; `lib/activation.d.mts`
53
+ still declared `SOURCE_EXTENSIONS` after it was removed. A `.ts` importing any of those
54
+ typechecks clean and gets `undefined` at runtime — the compiler is the tool that is
55
+ wrong. New suite **`test/test-declarations.mjs`** loads every `.mjs` beside each `.d.mts`
56
+ and fails on any declared export the module does not have. It found the third case on
57
+ its first run.
58
+
59
+ ### Fixed — the test runner could not fail
60
+
61
+ `test/harness.mjs` carried `const ok = globalThis.__tools = tools` where it meant to
62
+ capture the suite's verdict: `ok` was the tools array — always truthy — and the return
63
+ value of `run()` was discarded. A suite reporting failure by returning false printed its
64
+ FAILs and was still counted green, which is precisely the bug the comment above it
65
+ claimed was fixed. The verdict is now captured, and the new declarations suite depends
66
+ on it.
67
+
68
+ ### Also
69
+
70
+ - `06i-completeness.md` still described the old steering rule ("a gap is steered once,
71
+ never twice") after 0.16.0 removed the cap.
72
+
10
73
  ## [0.17.6] - 2026-11-09
11
74
 
12
75
  ### Fixed — a gate that crashes is not a gate that passed
@@ -12,5 +12,4 @@ export function activationDecision(o?: {
12
12
  activeRun?: { phase?: string; runId?: string } | null;
13
13
  passiveRunId?: string | null;
14
14
  }): { activate: boolean; reason?: string };
15
- export const SOURCE_EXTENSIONS: string[];
16
15
  export function delegatesUnguardedWrites(cmd: unknown): boolean;
@@ -163,6 +163,7 @@ async function judgeAndSettle(ctx: SettleContext, priorCount: number): Promise<S
163
163
  seenSteers: [...new Set([...(state.seenSteers || []), ...mem.seen])],
164
164
  sourceMutated: !!state.sourceMutated,
165
165
  testRunRecorded,
166
+ tracked: runtime.tracked,
166
167
  });
167
168
  runtime.lastSettleDecision = decision;
168
169
  appendEvidence({ kind: "gate-verdict", gate: "completion", fired: decision.missing.length > 0, missing: decision.missing });
@@ -17,6 +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
21
  import { readProjectConfig } from "../../gates/config.mjs";
21
22
  import { projectName, runRoot } from "../paths.ts";
22
23
  import { loadState, saveState } from "../state.ts";
@@ -102,6 +103,28 @@ function observeWebTouch(input: Input): void {
102
103
  if (touched && /\.(tsx|jsx|html|vue|svelte|css)$/i.test(touched)) runtime.webTouched = true;
103
104
  }
104
105
 
106
+ /**
107
+ * What this call did to tracked work (a claimed ticket, an assigned issue).
108
+ *
109
+ * The gate needs to know that the session *owns* something, because the failure
110
+ * it prevents is invisible in the source tree: an agent that ends its run with a
111
+ * claimed item neither resolved nor parked has stopped, and written a paragraph
112
+ * about it instead. Claim / resolve / park are read off the tool's name; a
113
+ * narrated wait is read off the text the agent wrote for the reviewer.
114
+ */
115
+ function observeTrackedWork(toolName: string, input: Input): void {
116
+ const kind = noteTrackedCall(runtime.tracked, { toolName, input });
117
+ if (!kind) return;
118
+ appendEvidence({ kind: "tracked", action: kind, tool: toolName, item: String(input.ticket_id || input.ticketId || input.id || "") || null });
119
+ if (kind === "wait") {
120
+ queueSteer(
121
+ `ASF: you described tracked work as waiting — that is not a disposition ASF recognises, and the run cannot close on it. ` +
122
+ `Park it where the reviewer sees it (parking is a marker, not a question, so "do not ask me" never forbids it), take the next ` +
123
+ `action you control, or do the work that makes the wait end in one step and keep going.`,
124
+ );
125
+ }
126
+ }
127
+
105
128
  /** Gate switches the project turned off, as evaluateBlockers wants them. */
106
129
  function blockerSwitches(config: Record<string, unknown>): Record<string, boolean> {
107
130
  const enabled: Record<string, boolean> = {};
@@ -209,6 +232,7 @@ export function registerToolCallHandler(pi: ExtensionAPI): void {
209
232
  }
210
233
  observeSuiteAttempt(input, ev);
211
234
  observeWebTouch(input);
235
+ observeTrackedWork(toolName, input);
212
236
 
213
237
  const config = readProjectConfig(runRoot());
214
238
  const res = evaluateBlockers(toolName, input, {
@@ -7,6 +7,8 @@
7
7
  * 2026-11-07 loop possible in the first place (state nobody owned).
8
8
  */
9
9
 
10
+ import { createTracked, type TrackedState } from "../gates/tracked.mjs";
11
+
10
12
  /** P5: machine turns before a re-anchor is due. */
11
13
  export const REANCHOR_AFTER_MACHINE_TURNS = 25;
12
14
  /** P5: and never more often than this, however many turns pass. */
@@ -40,6 +42,12 @@ export interface RuntimeState {
40
42
  lastScaleWasSmall: boolean;
41
43
  /** Re-anchoring is count-limited per run. */
42
44
  reanchorsThisRun: number;
45
+ /**
46
+ * Tracked work opened this session (a claimed ticket, an assigned issue) and
47
+ * whether it has a disposition. Session-scoped on purpose: a claim made before
48
+ * a restart is re-opened by the agent's own progress text if it is still dangling.
49
+ */
50
+ tracked: TrackedState;
43
51
  }
44
52
 
45
53
  export const runtime: RuntimeState = {
@@ -56,4 +64,5 @@ export const runtime: RuntimeState = {
56
64
  passiveHeartbeatWritten: false,
57
65
  lastScaleWasSmall: false,
58
66
  reanchorsThisRun: 0,
67
+ tracked: createTracked(),
59
68
  };
@@ -25,10 +25,11 @@ export function decideSettlement(
25
25
  testRunRecorded?: boolean;
26
26
  reportFields?: string[];
27
27
  untracedMet?: string[];
28
+ /** Tracked work opened this session; dangling items are a gap. */
29
+ tracked?: import("./tracked.mjs").TrackedState;
28
30
  },
29
31
  ): SettleDecision;
30
32
  export function renderCompletionGate(decision: { missing: string[]; exhausted?: boolean; park?: ParkRequest }): string;
31
33
  export function classifyPark(missing?: string[]): ParkRequest | null;
32
34
  export function steerFingerprint(missing?: string[]): string;
33
- export const MAX_IDENTICAL_STEERS: number;
34
35
  export const MAX_STEERS: number;
@@ -11,6 +11,14 @@
11
11
  * credential ASF cannot get, or a deliverable-shaping either/or that cannot be
12
12
  * inferred from the artifacts. Everything else is a steer.
13
13
  *
14
+ * **And a fifth thing that is NOT a halt (PO, 2026-11-09): someone else has not
15
+ * replied.** After the guard shipped, an agent left a claimed ticket In Progress,
16
+ * neither resolved nor parked, and explained it was waiting and had not parked
17
+ * because it had been told not to ask anything. Four named halts with no rule for
18
+ * "the other side is slow" does not produce persistence — it produces a run that
19
+ * closes on a paragraph. Tracked work left dangling is therefore a gap like any
20
+ * other, steered until it has one of the three dispositions in `tracked.mjs`.
21
+ *
14
22
  * **What replaced the cap.** The old rule was `MAX_CONTINUATIONS = 2`, then the
15
23
  * gate declined to ask again. A fixed cap is the wrong instrument under "no
16
24
  * stops": two is far too few for an honest implement → test → fix → verify loop,
@@ -27,6 +35,7 @@
27
35
  */
28
36
 
29
37
  import { sessionFacts } from "./ledger.mjs";
38
+ import { danglingGaps } from "./tracked.mjs";
30
39
 
31
40
  /**
32
41
  * A REPORTING number only — never a stop, and no longer referenced by any
@@ -187,6 +196,9 @@ function collectGaps(ledger, opts) {
187
196
  ...validationGap(ledger, opts),
188
197
  ...traceGap(opts.untracedMet),
189
198
  ...reportGaps(ledger, opts),
199
+ // "Waiting" is not a disposition: tracked work left neither resolved nor
200
+ // parked is a stop wearing a status report, and it is steered like one.
201
+ ...danglingGaps(opts.tracked),
190
202
  ];
191
203
  }
192
204
 
@@ -0,0 +1,28 @@
1
+ /** Type surface for the tracked-work gate (`tracked.mjs`). */
2
+
3
+ export interface TrackedItem {
4
+ at: number;
5
+ by: string;
6
+ }
7
+
8
+ export interface TrackedState {
9
+ /** Open items by id. A key here is work with no disposition yet. */
10
+ open: Record<string, TrackedItem>;
11
+ /** Ids whose agent has narrated a wait instead of acting. */
12
+ waits: string[];
13
+ /** Every id ever opened, for the evidence trail. */
14
+ seen: string[];
15
+ }
16
+
17
+ export declare const DISPOSITIONS: string[];
18
+
19
+ export function createTracked(): TrackedState;
20
+
21
+ export function noteTrackedCall(
22
+ tracked: TrackedState,
23
+ call: { toolName?: string; input?: Record<string, unknown> },
24
+ ): "claim" | "resolve" | "park" | "wait" | null;
25
+
26
+ export function danglingGaps(tracked: TrackedState | undefined): string[];
27
+
28
+ export function trackedClear(tracked: TrackedState | undefined): boolean;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * "Waiting" is not a disposition.
3
+ *
4
+ * On 2026-11-09 an agent working a claimed ticket ended its run with this on the
5
+ * card: *"Left In Progress at testing — deliberately not resolved, because the
6
+ * user-visible outcome still isn't confirmed, and not parked, since you told me
7
+ * not to ask you for anything."* Nothing was blocked, nothing was parked, the
8
+ * run closed, and the work stopped. That is a stop wearing a status report.
9
+ *
10
+ * It was ASF's fault, not the agent's. The contract names four things that may
11
+ * halt a run — plan approval, an irreversible action with no safe path, an
12
+ * unobtainable credential, a deliverable-shaping either/or. "The other side has
13
+ * not replied" is none of them, so the agent had no legal state to stand in and
14
+ * improvised an illegal one. A contract that forbids stopping without naming
15
+ * what to do instead does not produce persistence; it produces narratives.
16
+ *
17
+ * So the illegal state is named, and the legal answers are exactly three. They
18
+ * are all actions, and the agent can take every one of them — which is what
19
+ * makes this gap steerable rather than the 2026-11-07 kind that loops forever.
20
+ */
21
+
22
+ /** The only three things an agent may do with work it cannot finish right now. */
23
+ export const DISPOSITIONS = [
24
+ "park it where a reviewer sees it (the needs-input queue) — parking is a marker, not a question, so a 'do not ask me' instruction never forbids it",
25
+ "take the next action you control, now, in this run",
26
+ "if you are genuinely waiting on someone else, do the work that makes the wait end in one step — the test that will confirm the outcome, the draft, the verification you can run today — and then keep going",
27
+ ];
28
+
29
+ /**
30
+ * Tool names that open a tracked item. Matched on the tool name, never on shell
31
+ * text: `bmx_claim`, `linear_claim`, `ticket_assign`. A bash command is not a
32
+ * claim, because `git commit` is not one either.
33
+ */
34
+ const CLAIM_RE = /(^|[_-])(claim|assign|take|acquire)([_-]|$)/i;
35
+ /** Tool names that close one. `submit_resolution`, `resolve_ticket`, `finish`… */
36
+ const RESOLVE_RE = /(^|[_-])(resolve|resolution|finish|complete|close|deliver)([_-]|$)/i;
37
+ /** Tool names that park one where a human sees it. */
38
+ const PARK_RE = /(^|[_-])(park|escalate|needs?[_-]?input|request[_-]?input|request[_-]?review|block[_-]?review|hand[_-]?off)([_-]|$)/i;
39
+ /** Tool names whose text the agent writes for a human to read. */
40
+ const NARRATE_RE = /(^|[_-])(progress|status|update|comment|note|report)([_-]|$)/i;
41
+
42
+ /**
43
+ * The confession. An agent narrating a wait has usually said it out loud, in the
44
+ * very message meant for the reviewer. Deliberately narrow: it must describe
45
+ * leaving work unfinished, not merely mention a test or a queue.
46
+ */
47
+ const WAITING_ALTERNATIVES = [
48
+ "(left|stays?|kept|remain(?:s|ing)?)\\s+(?:it\\s+)?(in\\s+progress|in\\s+the\\s+queue|unresolved|not\\s+resolved|open\\s+deliberately)",
49
+ "deliberately\\s+not\\s+(resolved|closed|parked|finished)",
50
+ "not\\s+parked",
51
+ "waiting\\s+(for|on)\\s+(the|their|your|a\\s+reply|them)",
52
+ "as\\s+soon\\s+as\\s+(they|he|she|it)\\s+repl(?:y|ies|ied)",
53
+ "it\\s+flips\\s+(the\\s+moment|when|once)",
54
+ "pend(?:ing|s)\\s+(their|the|his|her)\\s+repl(?:y|ies)",
55
+ "until\\s+(they|he|she|it)\\s+(repl(?:y|ies|ied)|answers?|responds?)",
56
+ "resume[sd]?\\s+(when|once|the\\s+moment)",
57
+ ];
58
+ const WAITING_RE = new RegExp(WAITING_ALTERNATIVES.join("|"), "i");
59
+
60
+ /** Identify the item a call is about, so one ticket is one entry. */
61
+ function itemId(input) {
62
+ if (!input || typeof input !== "object") return null;
63
+ for (const k of ["ticket_id", "ticketId", "id", "issue", "issue_id", "work_item_id"]) {
64
+ const v = input[k];
65
+ if (typeof v === "string" && v.trim()) return v.trim().slice(0, 60);
66
+ if (typeof v === "number") return String(v);
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function textOf(input) {
72
+ if (!input || typeof input !== "object") return "";
73
+ const parts = [];
74
+ for (const k of ["text", "body", "comment", "message", "summary", "reason", "note", "status"]) {
75
+ const v = input[k];
76
+ if (typeof v === "string") parts.push(v);
77
+ }
78
+ return parts.join("\n").slice(0, 4000);
79
+ }
80
+
81
+ /** Fresh tracked-work state. `open` maps an item id to what opened it. */
82
+ export function createTracked() {
83
+ return { open: {}, waits: [], seen: [] };
84
+ }
85
+
86
+ function openItem(tracked, id, toolName) {
87
+ if (tracked.open[id]) return;
88
+ tracked.open[id] = { at: Date.now(), by: toolName };
89
+ tracked.seen.push(id);
90
+ }
91
+
92
+ function closeItem(tracked, id) {
93
+ delete tracked.open[id];
94
+ }
95
+
96
+ /**
97
+ * Record what a tool call did to tracked work.
98
+ *
99
+ * Returns a short label for the evidence trail (`claim` / `resolve` / `park` /
100
+ * `wait` / null). Never throws on odd input: this is an observer on the hot path.
101
+ */
102
+ export function noteTrackedCall(tracked, { toolName = "", input = {} } = {}) {
103
+ if (!tracked || !toolName) return null;
104
+ const id = itemId(input) || `tool:${toolName}`;
105
+ if (PARK_RE.test(toolName)) {
106
+ closeItem(tracked, id);
107
+ return "park";
108
+ }
109
+ if (RESOLVE_RE.test(toolName)) {
110
+ closeItem(tracked, id);
111
+ return "resolve";
112
+ }
113
+ if (CLAIM_RE.test(toolName)) {
114
+ openItem(tracked, id, toolName);
115
+ return "claim";
116
+ }
117
+ if (NARRATE_RE.test(toolName) && WAITING_RE.test(textOf(input))) {
118
+ // Narrating a wait does not make it legal; it makes it visible to the gate.
119
+ openItem(tracked, id, toolName);
120
+ if (!tracked.waits.includes(id)) tracked.waits.push(id);
121
+ return "wait";
122
+ }
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Gaps for work left dangling at settle time.
128
+ *
129
+ * Wording matters: these gaps must survive `splitGaps`, so they never say "ask
130
+ * the PO", "needs your input" or a slash command — a gap that reads like a human
131
+ * action is routed to the report and stops blocking, which would make this guard
132
+ * decorative. It names only moves the agent can make itself.
133
+ */
134
+ export function danglingGaps(tracked) {
135
+ const ids = Object.keys(tracked && tracked.open ? tracked.open : {});
136
+ if (!ids.length) return [];
137
+ const how = DISPOSITIONS.map((d, i) => `${i + 1}) ${d}`).join(" ");
138
+ return ids.map((id) => {
139
+ const waited = tracked.waits.includes(id);
140
+ return (
141
+ `tracked work "${id}" is still open and this run is ending with it neither resolved nor parked` +
142
+ (waited ? " — and the agent has just narrated waiting for it, which is not a state ASF recognises" : "") +
143
+ `. Nothing here stops you. Do one of: ${how}. A run does not close while tracked work dangles.`
144
+ );
145
+ });
146
+ }
147
+
148
+ /** True when nothing is dangling (used by tests and by the run card). */
149
+ export function trackedClear(tracked) {
150
+ return Object.keys(tracked && tracked.open ? tracked.open : {}).length === 0;
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-aia-asf",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
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",
@@ -86,6 +86,60 @@ correlation IDs, and a runbook exist."*
86
86
  unbounded wait, retry, or result set.
87
87
  - **Phase 7 (verification):** DoD includes the stability line above.
88
88
 
89
+ ## 5. Your own artifacts: the drift check
90
+
91
+ `checkDrift` in `lib/gates/drift.mjs` reads this task's own artifacts (PLAN.md, the
92
+ spec tree, a design doc) and checks the files and symbols they name still exist.
93
+ Invoke it before claiming done:
94
+
95
+ ```bash
96
+ node --input-type=module -e "
97
+ import {checkDrift, renderDrift} from './lib/gates/drift.mjs';
98
+ import fs from 'node:fs';
99
+ const arts = ['PLAN.md'].filter(f => fs.existsSync(f)).map(path => ({path, text: fs.readFileSync(path,'utf8')}));
100
+ console.log(renderDrift(checkDrift({artifacts: arts, repoRoot: process.cwd(), fs})));
101
+ "
102
+ ```
103
+
104
+ Three things it deliberately does **not** do:
105
+
106
+ - It does not scan the whole repo's documentation. Repo-wide docs drift presumes the
107
+ documentation exists and is maintained, and products legitimately evolve — a check
108
+ that produces permanent noise gets switched off, which is worse than never having it.
109
+ - It does not treat "0 unresolved" as a pass on its own. `status: "no-claims"` means
110
+ the artifact named no files or symbols at all, and that is reported as
111
+ examined-nothing, never as clean (06b Rule 10: `scanned: 0` is never a pass).
112
+ - It does not replace the traceability matrix. Drift says the artifact still describes
113
+ the code; the matrix says the spec was actually delivered.
114
+
115
+ ## 6. The stop that looked like a status report (2026-11-09)
116
+
117
+ An agent working a claimed ticket ended its run with this on the reviewer's card:
118
+
119
+ > *"Left In Progress at testing — deliberately not resolved, because the user-visible
120
+ > outcome still isn't confirmed, and not parked, since you told me not to ask you for
121
+ > anything. It flips the moment their thread replies: Resume succeeds and I resolve it
122
+ > with mail to ClearStory; fails and I re-diagnose from the factory's own sentence."*
123
+
124
+ Nothing blocked it. No gate fired. The run closed and the work stopped. The reasoning
125
+ was internally consistent and completely wrong: the agent had been told not to ask
126
+ questions, had correctly worked out that parking *looks like* asking, and so had
127
+ chosen a fifth state that the contract did not name — "in progress, waiting" — and
128
+ explained it. Every clause of that sentence is a stop.
129
+
130
+ Why it was ASF's bug, not the agent's: the stop policy names four things that may halt
131
+ a run, and "the other side has not replied" is none of them. A contract that forbids
132
+ stopping without naming what to do instead does not produce persistence, it produces
133
+ paragraphs. So the state is now named and illegal, the three legal dispositions are
134
+ listed (06i, "Waiting is not a disposition"), and the gate enforces it: `lib/gates/tracked.mjs`
135
+ records claims, resolutions and parks off the tool names, reads the agent's own progress
136
+ text for the confession, and a claimed item with no disposition is a gap that keeps the
137
+ run open — steered, never stopped, and closable by the agent alone.
138
+
139
+ Two things this is **not**: it is not a ban on saying "blocked" honestly (say it, in the
140
+ report, with the disposition you took); and it is not a reason to resolve work that is
141
+ not done — a false `resolved` is the worse failure. Park it.
142
+
89
143
  ## Anti-patterns
90
144
 
91
145
  - ❌ Unbounded retries on non-idempotent writes
@@ -4,10 +4,9 @@
4
4
  every deliverable with real behaviour. **Skip when:** never. Small work needs only
5
5
  the Core rule and the DoD checklist at the bottom.
6
6
 
7
- Newer models can produce work that *looks* finished: a function with the right name
8
- whose body is a placeholder, a screen populated with invented records, a report that
9
- cites a limit nobody checked. Green tests do not disprove it — the tests were written
10
- against the stub. This reference is the standing answer.
7
+ Work can *look* finished: the right function name over a placeholder body, a screen of
8
+ invented records, a report citing a limit nobody checked. Green tests do not disprove
9
+ it — they were written against the stub.
11
10
 
12
11
  ## Core rule
13
12
 
@@ -60,31 +59,16 @@ goes in the final message in plain words — never discovered by the user throug
60
59
  ## Your own artifacts are part of the deliverable (task-scoped drift)
61
60
 
62
61
  The plan and the spec tree are artifacts too, and they make checkable claims: a PLAN.md
63
- that names `lib/gates/suite-runs.mjs` or `deriveSuiteRun()` is asserting those exist. When the
64
- code has moved and the artifact has not, the delivery report describes something that is
65
- not there — which is the same defect as a stub, just further from the user's eye.
66
-
67
- Before claiming done, run the drift check against **this task's own artifacts**:
68
-
69
- ```bash
70
- node --input-type=module -e "
71
- import {checkDrift, renderDrift} from './lib/gates/drift.mjs';
72
- import fs from 'node:fs';
73
- const arts = ['PLAN.md'].filter(f => fs.existsSync(f)).map(path => ({path, text: fs.readFileSync(path,'utf8')}));
74
- console.log(renderDrift(checkDrift({artifacts: arts, repoRoot: process.cwd(), fs})));
75
- "
76
- ```
77
-
78
- Three things this deliberately does **not** do:
79
-
80
- - It does not scan the whole repo's documentation. Repo-wide docs drift presumes the
81
- documentation exists and is maintained, and products legitimately evolve — a check that
82
- produces permanent noise gets switched off, which is worse than never having it.
83
- - It does not treat "0 unresolved" as a pass on its own. `status: "no-claims"` means the
84
- artifact named no files or symbols at all, and that is reported as examined-nothing,
85
- never as clean (06b Rule 10: `scanned: 0` is never a pass).
86
- - It does not replace the traceability matrix. Drift says the artifact still describes
87
- the code; the matrix says the spec was actually delivered.
62
+ naming `lib/gates/suite-runs.mjs` asserts that file exists. When the code moved and the
63
+ artifact did not, the delivery report describes something that is not there — the same
64
+ defect as a stub, further from the user's eye.
65
+
66
+ Before claiming done, run `checkDrift` from `lib/gates/drift.mjs` against **this
67
+ task's own artifacts** (PLAN.md, spec tree, design doc) and put its status in the
68
+ delivery report; the invocation is in 06f, section 5.
69
+
70
+ What the drift check deliberately does **not** do (and why a check that produces
71
+ permanent noise gets switched off) is in 06f, section 5.
88
72
 
89
73
  ## Active process (enforced by the harness)
90
74
 
@@ -93,14 +77,36 @@ Three things this deliberately does **not** do:
93
77
  scratch do not.
94
78
  - **Floor:** a passing test run that started after the last change (a lint is not
95
79
  a test); a spec met this run needs `trace.outcome` + `trace.codePath`.
96
- - A gap is steered once, never twice; unclosable gaps are reported **unverified**.
97
- Park only for: plan approval, irreversible step, credential, deliverable choice.
80
+ - A **warranted** gap (one you can close) is re-raised until closed — no ceiling on
81
+ real work. An **unwarranted** one (only a human, or a pi slash command, can close
82
+ it) is reported **unverified** and never blocks: that was the 2026-11-07 loop.
98
83
  - Always refused, even under `/asf passive`: secret in a tracked file, push without
99
84
  full-suite green, destructive git on a dirty tree.
100
85
  - `/asf passive` = this run only. `/asf abort` ends a run; deleting state does not.
101
86
 
87
+ ## Waiting is not a disposition
88
+
89
+ Four things may halt a run: plan approval, an irreversible step, a credential, a
90
+ deliverable choice. **"The other side has not replied" is not one of them.** Never
91
+ leave tracked work — a claimed ticket, an assigned issue — in an intermediate state
92
+ with an explanation of why it is stuck: that is a stop wearing a status report, and
93
+ the run may not close on it (06f carries the case that proved it).
94
+
95
+ Three dispositions, all yours to take:
96
+
97
+ 1. **Park it where a reviewer sees it** (needs-input). Parking is a *marker*, not a
98
+ question — "don't ask me anything" never forbids it.
99
+ 2. **Take the next action you control, now.** There is always one.
100
+ 3. **Waiting on someone else? Make the wait end in one step** — the test that will
101
+ confirm the outcome, the draft mail, the check you can run today — then keep going.
102
+
103
+ A claimed item with no disposition is a gap like any other; narrating a wait in a
104
+ progress message raises it on the spot.
105
+
102
106
  ## Definition of Done — completeness
103
107
 
108
+ - [ ] No claimed/tracked item left without a disposition: resolved, parked, or acted on.
109
+
104
110
  - [ ] No stub, unimplemented path, placeholder or fabricated data remains on a
105
111
  user-reachable path — or it is declared to the user with the reason.
106
112
  - [ ] Every part of the ask is reachable from where the user enters it; the delivery