pi-aia-asf 0.17.13 → 0.18.0
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 +56 -0
- package/index.ts +2 -0
- package/lib/extension/handlers/settle.ts +31 -7
- package/lib/extension/tools/decisions.ts +15 -15
- package/lib/extension/tools/run-complete.ts +95 -0
- package/lib/home.mjs +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.18.0] - 2026-11-10
|
|
11
|
+
|
|
12
|
+
### Added — a run can be completed, and tests can no longer corrupt one
|
|
13
|
+
|
|
14
|
+
Two defects from the same blind spot: ASF tracked runs but had no honest way to finish
|
|
15
|
+
one, and nothing stopped a test suite from writing into the run state a real user reads.
|
|
16
|
+
|
|
17
|
+
**`asf_run_complete`.** A run used to close only when the completion gate happened to
|
|
18
|
+
pass at settle time, or when a human typed `/asf abort`. There was no action the agent
|
|
19
|
+
could take, so a run that had genuinely finished stayed open and swallowed the next
|
|
20
|
+
piece of work. This project carried a single run opened three days earlier that had
|
|
21
|
+
absorbed several releases and unrelated changes in its `filesTouched` — the gate was
|
|
22
|
+
judging today's work against Monday's evidence, so nothing settled and the loop never
|
|
23
|
+
closed. The user's instruction was to "properly complete and mark as completed every
|
|
24
|
+
run, so you judge the next run separately again".
|
|
25
|
+
|
|
26
|
+
- The tool asks the **same question the gate asks**: `evaluateRun()` is now the single
|
|
27
|
+
judge, called by both `judgeAndSettle` and the tool. Two implementations of "is this
|
|
28
|
+
run done" drift, and the drift reads as a run the agent closed that the gate still
|
|
29
|
+
counts as open.
|
|
30
|
+
- It **fails closed**. An unfinished run is refused with its gaps listed and stays open
|
|
31
|
+
with its evidence. A tool that closed runs on request would be an exit around the
|
|
32
|
+
completion gate, which is worse than the bug it fixes.
|
|
33
|
+
- `closeRun()` is the only writer of run history and the only clearer of `current`, and
|
|
34
|
+
it records `by` — `auto` from the gate, `asf_run_complete` from the agent — so a close
|
|
35
|
+
that happened outside the gate is visible in evidence.
|
|
36
|
+
|
|
37
|
+
### Changed — the decision register follows the code
|
|
38
|
+
|
|
39
|
+
`asf_decisions` wrote to the session workspace. Working in a repository opened from a
|
|
40
|
+
parent directory (a home directory that is itself a checkout is the common case) put the
|
|
41
|
+
ADRs one level above the project they describe, where nothing would ever read them. The
|
|
42
|
+
register is durable knowledge *about* the code, so it now resolves through `measureRoot()`
|
|
43
|
+
like every other measurement. `currentRegister()` was deleted: it announced itself as
|
|
44
|
+
"exposed for the handlers" and had no callers.
|
|
45
|
+
|
|
46
|
+
### Fixed — a test suite could not touch a live run
|
|
47
|
+
|
|
48
|
+
A suite that forgot to `chdir` wrote into the developer's real run state: `/etc/passwd`
|
|
49
|
+
and a scratch path appeared in the live run's `filesTouched`, and the reviewer saw them
|
|
50
|
+
in the run card. State is the one thing a test corrupts with no visible failure — the
|
|
51
|
+
assertion passes and the user's screen is wrong.
|
|
52
|
+
|
|
53
|
+
- `homeStateDir()` honours `ASF_STATE_ROOT`, and `test/harness.mjs` sets it to a fresh
|
|
54
|
+
temp directory before the extension loads. The whole suite is now provably inert
|
|
55
|
+
against real state, verified by diffing the state tree across a full `run-all`.
|
|
56
|
+
- `test-decisions.mjs` pins **both** behaviours — the real `.asf/state/<key>` layout when
|
|
57
|
+
unset, the override when set — so the isolation cannot silently defeat the assertion
|
|
58
|
+
it was added next to.
|
|
59
|
+
|
|
60
|
+
`scripts/verify-published.mjs` also states its own boundary: `scripts/` does not ship, so
|
|
61
|
+
a user cannot run it, which is fine only because every assertion it makes delegates to
|
|
62
|
+
shipped code (`lib/gates/evidence-coverage.mjs`, the same module behind
|
|
63
|
+
`asf_evidence_check`). A check added there that cannot be reproduced from an install is a
|
|
64
|
+
check only a releaser can run.
|
|
65
|
+
|
|
10
66
|
## [0.17.13] - 2026-11-10
|
|
11
67
|
|
|
12
68
|
### Changed — no scale means small (PO policy)
|
package/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { registerHealthTool } from "./lib/extension/tools/health.ts";
|
|
|
20
20
|
import { registerDecisionsTool } from "./lib/extension/tools/decisions.ts";
|
|
21
21
|
import { registerBrowserTools } from "./lib/extension/tools/browser.ts";
|
|
22
22
|
import { registerEvidenceCheckTool } from "./lib/extension/tools/evidence-check.ts";
|
|
23
|
+
import { registerRunCompleteTool } from "./lib/extension/tools/run-complete.ts";
|
|
23
24
|
import { registerCommands } from "./lib/extension/commands/index.ts";
|
|
24
25
|
import { registerToolCallHandler } from "./lib/extension/handlers/tool-call.ts";
|
|
25
26
|
import { registerToolResultHandler } from "./lib/extension/handlers/tool-result.ts";
|
|
@@ -34,6 +35,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
34
35
|
registerDecisionsTool(pi);
|
|
35
36
|
registerBrowserTools(pi);
|
|
36
37
|
registerEvidenceCheckTool(pi);
|
|
38
|
+
registerRunCompleteTool(pi);
|
|
37
39
|
registerCommands(pi);
|
|
38
40
|
registerToolCallHandler(pi);
|
|
39
41
|
registerToolResultHandler(pi);
|
|
@@ -135,10 +135,21 @@ async function handleBlock(
|
|
|
135
135
|
return { entries, continue: true };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Close a run: archive it to history and clear `current`.
|
|
140
|
+
*
|
|
141
|
+
* The next piece of work then opens its own run and is judged on its own facts. A run
|
|
142
|
+
* that is never closed accretes everything the session does — this project once carried
|
|
143
|
+
* a single run opened three days earlier, with four releases and several unrelated
|
|
144
|
+
* changes inside its `filesTouched`, so the gate was judging today's work against
|
|
145
|
+
* Monday's evidence and nothing could settle.
|
|
146
|
+
*
|
|
147
|
+
* `by` records who closed it: the gate itself (`auto`) or the agent asking
|
|
148
|
+
* (`asf_run_complete`), so a run that closed without the gate passing is visible.
|
|
149
|
+
*/
|
|
150
|
+
export async function closeRun(state: AsfState, file: ProjectStateFile, { project, by = "auto" }: { project: string; by?: string }): Promise<void> {
|
|
140
151
|
file.history.push({ workType: state.workType || "auto", phase: state.phase, startedAt: state.startedAt || state.updatedAt, endedAt: new Date().toISOString() });
|
|
141
|
-
appendEvidence({ kind: "run-close", runId: state.runId, auto:
|
|
152
|
+
appendEvidence({ kind: "run-close", runId: state.runId, auto: by === "auto", by });
|
|
142
153
|
file.current = null;
|
|
143
154
|
await saveState(project, file);
|
|
144
155
|
}
|
|
@@ -179,8 +190,15 @@ function decisionFacts(state: AsfState) {
|
|
|
179
190
|
}
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Judge a run: the same decision the completion gate makes, in one place.
|
|
195
|
+
*
|
|
196
|
+
* Exported because the agent needs to ask the same question before it claims a run is
|
|
197
|
+
* finished (`asf_run_complete`). Two implementations of "is this run done" drift, and
|
|
198
|
+
* the drift shows up as a run the agent closed and the gate still thinks is open —
|
|
199
|
+
* which is how a run from three days ago was still being carried into new work.
|
|
200
|
+
*/
|
|
201
|
+
export async function evaluateRun(project: string, state: AsfState) {
|
|
184
202
|
// Only a passing test that STARTED after the last source change validates it.
|
|
185
203
|
const since = typeof state.lastMutationAt === "number" ? state.lastMutationAt : 0;
|
|
186
204
|
const testRunRecorded = suiteRuns.some((r) => r.exitCode === 0 && (r.at ?? 0) >= since);
|
|
@@ -189,7 +207,7 @@ async function judgeAndSettle(ctx: SettleContext, priorCount: number): Promise<S
|
|
|
189
207
|
// stale, raced or hand-edited. max() means a counter can never go down.
|
|
190
208
|
const mem = steerLedgerFor(`${project}|${state.runId || "run"}`);
|
|
191
209
|
sanitizeSteerFields(state);
|
|
192
|
-
|
|
210
|
+
return decideSettlement(ledger, {
|
|
193
211
|
untracedMet,
|
|
194
212
|
scale: currentScale(state),
|
|
195
213
|
webTouched: runtime.webTouched,
|
|
@@ -200,12 +218,18 @@ async function judgeAndSettle(ctx: SettleContext, priorCount: number): Promise<S
|
|
|
200
218
|
tracked: runtime.tracked,
|
|
201
219
|
decisionGaps: decisionFacts(state),
|
|
202
220
|
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function judgeAndSettle(ctx: SettleContext, priorCount: number): Promise<SettleResult> {
|
|
224
|
+
const { project, file, state, entries } = ctx;
|
|
225
|
+
const mem = steerLedgerFor(`${project}|${state.runId || "run"}`);
|
|
226
|
+
const decision = await evaluateRun(project, state);
|
|
203
227
|
runtime.lastSettleDecision = decision;
|
|
204
228
|
appendEvidence({ kind: "gate-verdict", gate: "completion", fired: decision.missing.length > 0, missing: decision.missing });
|
|
205
229
|
|
|
206
230
|
if (decision.park) return await handlePark(ctx, decision.park as { reason: string; question: string });
|
|
207
231
|
if (decision.block) return await handleBlock({ ...ctx, mem }, decision as BlockRequest);
|
|
208
|
-
if (decision.missing.length === 0 && state.auto) await
|
|
232
|
+
if (decision.missing.length === 0 && state.auto) await closeRun(state, file, { project });
|
|
209
233
|
// A continuation for guidance alone is also bounded: an agent that keeps
|
|
210
234
|
// producing new distinct steers must not be kept alive by its own advice.
|
|
211
235
|
if (entries.length > priorCount && steerContinuationsAllowed(mem)) return { entries, continue: true };
|
|
@@ -21,6 +21,7 @@ import { registerAt, addDecision, supersedeDecision, relativeTo } from "../../de
|
|
|
21
21
|
import { matchesAny } from "../../decisions/glob.mjs";
|
|
22
22
|
import { readProjectConfig } from "../../gates/config.mjs";
|
|
23
23
|
import { runRoot, projectName } from "../paths.ts";
|
|
24
|
+
import { measureRoot } from "../run.ts";
|
|
24
25
|
import { loadState } from "../state.ts";
|
|
25
26
|
import { runtime } from "../runtime.ts";
|
|
26
27
|
import { appendEvidence } from "../evidence.ts";
|
|
@@ -43,9 +44,12 @@ async function planApproved(): Promise<boolean> {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function config(): Record<string, unknown> {
|
|
47
|
+
function config(root: string): Record<string, unknown> {
|
|
47
48
|
try {
|
|
48
|
-
|
|
49
|
+
// The register is durable knowledge ABOUT the code, so it follows the code the
|
|
50
|
+
// run is changing — not the session workspace, which for a repo opened from a
|
|
51
|
+
// parent directory would put the ADRs one level above the project they describe.
|
|
52
|
+
return readProjectConfig(root);
|
|
49
53
|
} catch {
|
|
50
54
|
return {};
|
|
51
55
|
}
|
|
@@ -88,14 +92,14 @@ function renderProblems(problems: { severity: string; file?: string; field?: str
|
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
async function listAction(root: string): Promise<ActionOut> {
|
|
91
|
-
const { dir, records, problems } = registerAt(root, config());
|
|
95
|
+
const { dir, records, problems } = registerAt(root, config(root));
|
|
92
96
|
const errors = problems.filter((p) => p.severity === "error");
|
|
93
97
|
const text = `${dir}\n\n${renderList(records)}${errors.length ? `\n\n${renderProblems(errors)}` : ""}`;
|
|
94
98
|
return { text, ok: errors.length === 0, count: records.length };
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
async function getAction(root: string, id: string): Promise<ActionOut> {
|
|
98
|
-
const { records } = registerAt(root, config());
|
|
102
|
+
const { records } = registerAt(root, config(root));
|
|
99
103
|
const found = records.find((r) => String(r.id).toLowerCase() === id.toLowerCase());
|
|
100
104
|
if (!found) return { text: `No ${id} in the register.`, ok: false, count: 0 };
|
|
101
105
|
if (!runtime.decisionsRead.includes(String(found.id))) runtime.decisionsRead.push(String(found.id));
|
|
@@ -106,7 +110,7 @@ async function getAction(root: string, id: string): Promise<ActionOut> {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
async function matchAction(root: string, filePath: string): Promise<ActionOut> {
|
|
109
|
-
const { records } = registerAt(root, config());
|
|
113
|
+
const { records } = registerAt(root, config(root));
|
|
110
114
|
const hit = records.filter((r) => r.status === "accepted" && matches(r, filePath));
|
|
111
115
|
const text = hit.length
|
|
112
116
|
? `${filePath} is governed by:\n${renderList(hit)}\n\nRead them with action=get before changing the file.`
|
|
@@ -121,11 +125,11 @@ function matches(r: DecisionRecord, filePath: string): boolean {
|
|
|
121
125
|
|
|
122
126
|
async function recordAction(root: string, p: Record<string, unknown>): Promise<ActionOut> {
|
|
123
127
|
const approved = await planApproved();
|
|
124
|
-
const { records } = registerAt(root, config());
|
|
128
|
+
const { records } = registerAt(root, config(root));
|
|
125
129
|
const out = addDecision(root, {
|
|
126
130
|
record: recordFrom(p),
|
|
127
131
|
body: str(p.body),
|
|
128
|
-
config: config(),
|
|
132
|
+
config: config(root),
|
|
129
133
|
planApproved: approved,
|
|
130
134
|
read: records,
|
|
131
135
|
});
|
|
@@ -140,7 +144,7 @@ async function recordAction(root: string, p: Record<string, unknown>): Promise<A
|
|
|
140
144
|
|
|
141
145
|
async function supersedeAction(root: string, p: Record<string, unknown>): Promise<ActionOut> {
|
|
142
146
|
const approved = await planApproved();
|
|
143
|
-
const { records } = registerAt(root, config());
|
|
147
|
+
const { records } = registerAt(root, config(root));
|
|
144
148
|
// The `id` parameter names the decision being REPLACED; the record built from
|
|
145
149
|
// the same params must not carry it, or the new decision is written with the old
|
|
146
150
|
// decision's id and the register ends up with two claimants for one number.
|
|
@@ -150,7 +154,7 @@ async function supersedeAction(root: string, p: Record<string, unknown>): Promis
|
|
|
150
154
|
by: "agent",
|
|
151
155
|
record: recordFrom(recordParams),
|
|
152
156
|
body: str(p.body),
|
|
153
|
-
config: config(),
|
|
157
|
+
config: config(root),
|
|
154
158
|
planApproved: approved,
|
|
155
159
|
read: records,
|
|
156
160
|
});
|
|
@@ -161,7 +165,7 @@ async function supersedeAction(root: string, p: Record<string, unknown>): Promis
|
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
async function checkAction(root: string): Promise<ActionOut> {
|
|
164
|
-
const { dir, records, problems } = registerAt(root, config());
|
|
168
|
+
const { dir, records, problems } = registerAt(root, config(root));
|
|
165
169
|
const errors = problems.filter((p) => p.severity === "error");
|
|
166
170
|
const notices = problems.filter((p) => p.severity === "notice");
|
|
167
171
|
// What the completion floor believes about THIS run. Without it the citation rules
|
|
@@ -228,7 +232,7 @@ export function registerDecisionsTool(pi: ExtensionAPI): void {
|
|
|
228
232
|
promptSnippet: "Consult or record the project's architecture decisions",
|
|
229
233
|
parameters: Type.Object(DECISION_PARAMS),
|
|
230
234
|
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
231
|
-
const root =
|
|
235
|
+
const root = await measureRoot();
|
|
232
236
|
const action = str(params?.action);
|
|
233
237
|
try {
|
|
234
238
|
return toolResult(action, await runAction(root, action, params));
|
|
@@ -245,7 +249,3 @@ export function registerDecisionsTool(pi: ExtensionAPI): void {
|
|
|
245
249
|
});
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
/** Exposed for the handlers: the register as the rest of ASF sees it. */
|
|
249
|
-
export function currentRegister() {
|
|
250
|
-
return registerAt(runRoot(), config());
|
|
251
|
-
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* asf_run_complete — the agent's way to finish a run.
|
|
3
|
+
*
|
|
4
|
+
* A run used to close only when the completion gate happened to pass at settle time,
|
|
5
|
+
* or when a human typed `/asf abort`. Neither is an action the agent can take, so a run
|
|
6
|
+
* that had genuinely finished stayed open and swallowed the next piece of work: this
|
|
7
|
+
* project carried one run opened three days earlier, holding four releases and several
|
|
8
|
+
* unrelated changes in its `filesTouched`, and the gate judged today's work against
|
|
9
|
+
* Monday's evidence. Nothing settled, and the loop never closed.
|
|
10
|
+
*
|
|
11
|
+
* It asks the SAME question the gate asks (`evaluateRun`), so the tool and the gate
|
|
12
|
+
* cannot disagree about what "done" means. And it fails closed: an unfinished run is
|
|
13
|
+
* refused with the gaps listed. A tool that closed runs on request would be an exit
|
|
14
|
+
* around the completion gate, which is worse than the bug it fixes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { projectName } from "../paths.ts";
|
|
20
|
+
import { loadState } from "../state.ts";
|
|
21
|
+
import { evaluateRun, closeRun } from "../handlers/settle.ts";
|
|
22
|
+
|
|
23
|
+
/** One details shape for every branch: branches that disagree about which fields
|
|
24
|
+
* exist make the host read `error` as required and reject the tool. */
|
|
25
|
+
function details() {
|
|
26
|
+
return {
|
|
27
|
+
action: "run-complete",
|
|
28
|
+
ok: false,
|
|
29
|
+
closed: false,
|
|
30
|
+
error: "",
|
|
31
|
+
runId: "",
|
|
32
|
+
note: "",
|
|
33
|
+
missing: [] as string[],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function completeRun(note: string) {
|
|
38
|
+
const project = projectName();
|
|
39
|
+
const file = await loadState(project);
|
|
40
|
+
const state = file.current;
|
|
41
|
+
if (!state || state.phase === "none") {
|
|
42
|
+
return {
|
|
43
|
+
text: "No active ASF run to complete — the next piece of work opens a fresh one.",
|
|
44
|
+
res: { ...details(), error: "no-run" },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const decision = await evaluateRun(project, state);
|
|
49
|
+
const missing = decision.missing || [];
|
|
50
|
+
if (missing.length > 0) {
|
|
51
|
+
// Refusing is the whole point: this is the gate's own verdict, not a formality.
|
|
52
|
+
return {
|
|
53
|
+
text:
|
|
54
|
+
`Run ${state.runId} is NOT complete — the definition of done still has ${missing.length} gap(s):\n` +
|
|
55
|
+
missing.map((m: string) => `- ${m}`).join("\n") +
|
|
56
|
+
`\n\nClose those and call it again. The run stays open and keeps its evidence.`,
|
|
57
|
+
res: { ...details(), runId: state.runId || "", missing },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const runId = state.runId || "run";
|
|
62
|
+
await closeRun(state, file, { project, by: "asf_run_complete" });
|
|
63
|
+
return {
|
|
64
|
+
text:
|
|
65
|
+
`Run ${runId} complete (${state.phase}${state.scale ? `, ${state.scale}` : ""})` +
|
|
66
|
+
`${note ? ` — ${note}` : ""}. It is archived to history and the next piece of work ` +
|
|
67
|
+
`opens a fresh run, judged on its own facts.`,
|
|
68
|
+
res: { ...details(), ok: true, closed: true, runId, note },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerRunCompleteTool(pi: ExtensionAPI): void {
|
|
73
|
+
pi.registerTool({
|
|
74
|
+
name: "asf_run_complete",
|
|
75
|
+
label: "ASF Complete Run",
|
|
76
|
+
description:
|
|
77
|
+
"Finish the current ASF run so the NEXT piece of work is judged on its own facts. " +
|
|
78
|
+
"Call it when the work this run was opened for is genuinely done — tests run, evidence " +
|
|
79
|
+
"recorded, released if it was a release. It asks the same question the completion gate " +
|
|
80
|
+
"asks, so it cannot be used to get around the gate: an unfinished run is refused with " +
|
|
81
|
+
"the gaps listed, and the run stays open.",
|
|
82
|
+
promptSnippet: "Mark the current run complete (only if its definition of done is met)",
|
|
83
|
+
parameters: Type.Object({
|
|
84
|
+
note: Type.Optional(
|
|
85
|
+
Type.String({ description: "One line on what the run delivered; recorded with the closure." }),
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
async execute(_toolCallId: string, params: { note?: unknown }) {
|
|
90
|
+
const note = typeof params?.note === "string" ? params.note.trim() : "";
|
|
91
|
+
const { text, res } = await completeRun(note);
|
|
92
|
+
return { content: [{ type: "text" as const, text }], details: res };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
package/lib/home.mjs
CHANGED
|
@@ -49,8 +49,20 @@ export function asfDir(root) {
|
|
|
49
49
|
return path.join(root, ASF_DIR);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Where the run state lives now. `key` is the project key (git-root based).
|
|
54
|
+
*
|
|
55
|
+
* `ASF_STATE_ROOT` redirects the whole tree. It exists for one reason: a suite that
|
|
56
|
+
* forgets to `chdir` was writing into the developer's LIVE run state, and the garbage
|
|
57
|
+
* (a `/etc/passwd` entry in `filesTouched`) showed up in the run card the reviewer
|
|
58
|
+
* reads. State is the one piece of ASF that a test can corrupt without noticing, so
|
|
59
|
+
* the test harness points it at a temp directory and no suite can reach the real one.
|
|
60
|
+
*/
|
|
53
61
|
export function homeStateDir(root, key) {
|
|
62
|
+
const override = process.env.ASF_STATE_ROOT;
|
|
63
|
+
// The key is derived from the git root, so it is already unique per project;
|
|
64
|
+
// nesting the root under the override again would only make the path unreadable.
|
|
65
|
+
if (typeof override === "string" && override.trim()) return path.join(path.resolve(override), key);
|
|
54
66
|
return path.join(asfDir(root), STATE_SUBDIR, key);
|
|
55
67
|
}
|
|
56
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-aia-asf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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",
|