pi-gauntlet 7.17.1 → 7.18.1
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
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v7.18.1 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- **`start implement` no longer warns about a missing `promptCache` lifetime.** The warning fired on every non-Anthropic model (pi's catalog declares `promptCache` only for direct `anthropic` Claude entries) and read as an instruction to edit `models.json`, which stalled auto-chaining into implement until the user typed "continue"; the per-call cache telemetry from #67 stays and is the real signal for cache hits.
|
|
8
|
+
|
|
9
|
+
## v7.18.0 - 2026-10-19
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Brainstorming questions list one option per line.** Each `A)`/`B)` option and the `Recommendation:` line sit on their own physical line instead of one paragraph; wording and the framing rules are unchanged. (#70)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Telemetry counts the agents of a failed dispatch.** A `subagent` call that ended in error (a failed chain step, a sibling that exited non-zero) still records its retained per-persona results; spec-round and conformance bookkeeping stay on the success path. `package.json` now declares pi-cohort >= 7.3.1 as a peer dependency, matching the README requirement. (#70)
|
|
18
|
+
|
|
3
19
|
## v7.17.1 - 2026-10-18
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
@@ -69,7 +69,7 @@ const resumedBranch = (rest: Partial<Record<Phase, Status>>) => [
|
|
|
69
69
|
phaseResult("complete", phases({ brainstorm: "skipped", ...rest })),
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
-
type TestModel = { provider: string; id: string
|
|
72
|
+
type TestModel = { provider: string; id: string };
|
|
73
73
|
|
|
74
74
|
function harness(options: { cwd?: string; branch?: unknown[]; idle?: boolean; beforeSettled?: (setIdle: (idle: boolean) => void) => void; sendThrows?: boolean; model?: TestModel; thinkingLevel?: string; registry?: Record<string, TestModel>; setModelResult?: boolean | Error } = {}) {
|
|
75
75
|
const handlers = new Map<string, ((event: unknown, ctx: unknown) => unknown)[]>();
|
|
@@ -136,7 +136,7 @@ const startPhase = async (h: ReturnType<typeof harness>, phase: string) => {
|
|
|
136
136
|
return { text: r.content.map((c) => c.text).join("\n"), status: r.details.phases[phase].status };
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
const M = (id: string
|
|
139
|
+
const M = (id: string) => ({ provider: "p", id });
|
|
140
140
|
|
|
141
141
|
const preparePhase = async (h: ReturnType<typeof harness>, phase = "plan") => {
|
|
142
142
|
await settle(h);
|
|
@@ -223,46 +223,6 @@ test("phaseModels: resolver warnings appear only for their phase", async () => {
|
|
|
223
223
|
assert.deepEqual(h.pi.thinkingCalls, []);
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
for (const row of [
|
|
227
|
-
{ name: "missing short", phase: "implement", cache: undefined, retention: undefined, tier: "short" },
|
|
228
|
-
{ name: "declared short", phase: "implement", cache: { short: 300 }, retention: undefined, tier: undefined },
|
|
229
|
-
{ name: "missing long", phase: "implement", cache: { short: 300 }, retention: "long", tier: "long" },
|
|
230
|
-
{ name: "plan ignores cache", phase: "plan", cache: undefined, retention: undefined, tier: undefined },
|
|
231
|
-
]) {
|
|
232
|
-
test(`phaseModels: cache warning ${row.name}`, async () => {
|
|
233
|
-
const retention = process.env.PI_CACHE_RETENTION;
|
|
234
|
-
if (row.retention === undefined) delete process.env.PI_CACHE_RETENTION;
|
|
235
|
-
else process.env.PI_CACHE_RETENTION = row.retention;
|
|
236
|
-
try {
|
|
237
|
-
const h = harness({ cwd: tempCwd({ piGauntlet: { flowGuards: { enforce: false } } }), model: M("m", row.cache) });
|
|
238
|
-
await preparePhase(h, row.phase);
|
|
239
|
-
const r = await startPhase(h, row.phase);
|
|
240
|
-
if (row.tier) {
|
|
241
|
-
assert.ok(r.text.includes(`p/m declares no promptCache.${row.tier} lifetime, so pi's cache warmer skips it`));
|
|
242
|
-
assert.ok(r.text.includes("providers.p.modelOverrides.m.promptCache"));
|
|
243
|
-
assert.ok(r.text.includes("providers.p.models[].promptCache"));
|
|
244
|
-
} else assert.doesNotMatch(r.text, /cache warmer/);
|
|
245
|
-
assert.deepEqual(h.pi.setModelCalls, []);
|
|
246
|
-
assert.deepEqual(h.pi.thinkingCalls, []);
|
|
247
|
-
assert.equal(r.status, "in_progress");
|
|
248
|
-
} finally {
|
|
249
|
-
if (retention === undefined) delete process.env.PI_CACHE_RETENTION;
|
|
250
|
-
else process.env.PI_CACHE_RETENTION = retention;
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
test("phaseModels: cache warning checks the switched model", async () => {
|
|
256
|
-
const h = harness({ cwd: tempCwd({ piGauntlet: { flowGuards: { enforce: false }, phaseModels: { implement: "p/warm" } } }), model: M("old"), registry: { "p/warm": M("warm", { short: 300, long: 3600 }) } });
|
|
257
|
-
await preparePhase(h, "implement");
|
|
258
|
-
const r = await startPhase(h, "implement");
|
|
259
|
-
assert.equal(h.pi.setModelCalls.length, 1);
|
|
260
|
-
assert.deepEqual(h.pi.thinkingCalls, []);
|
|
261
|
-
assert.equal(h.ctx.model?.id, "warm");
|
|
262
|
-
assert.doesNotMatch(r.text, /cache warmer/);
|
|
263
|
-
assert.equal(r.status, "in_progress");
|
|
264
|
-
});
|
|
265
|
-
|
|
266
226
|
test("agent_settled nudges each exact edge with persisted details", async () => {
|
|
267
227
|
for (const [state, edge, skill] of [
|
|
268
228
|
[phases({ brainstorm: "complete", plan: "complete" }), "plan-implement", "/skill:subagent-driven-development"],
|
|
@@ -1031,17 +1031,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1031
1031
|
for (const w of resolved.warning?.split("; ") ?? []) if (w.includes(`phaseModels.${phase} `)) startLines.push(`\u26a0\ufe0f ${w}`);
|
|
1032
1032
|
const target = resolved.phaseModels[phase];
|
|
1033
1033
|
if (target) startLines.push(...(await applyPhaseModel(pi, ctx, phase, target)));
|
|
1034
|
-
if (phase === "implement") {
|
|
1035
|
-
const tier = process.env.PI_CACHE_RETENTION === "long" ? "long" : "short";
|
|
1036
|
-
const m = ctx.model;
|
|
1037
|
-
if (m && m.promptCache?.[tier] === undefined) {
|
|
1038
|
-
startLines.push(
|
|
1039
|
-
`\u26a0\ufe0f ${m.provider}/${m.id} declares no promptCache.${tier} lifetime, so pi's cache warmer skips it. ` +
|
|
1040
|
-
`Declare it in <agent-dir>/models.json (default ~/.pi/agent): providers.${m.provider}.modelOverrides.${m.id}.promptCache for a built-in model, ` +
|
|
1041
|
-
`or providers.${m.provider}.models[].promptCache for a custom one; the lifetime is required by the warmer but not sufficient - see doc/configuration.md#phase-models.`,
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
1034
|
}
|
|
1046
1035
|
phases = { ...phases, [params.phase]: transitionPhaseState("in_progress") as PhaseState };
|
|
1047
1036
|
// A rewind must not inherit the prior verify's conformance latch.
|
|
@@ -588,7 +588,7 @@
|
|
|
588
588
|
assert.deepEqual(h.readRecord().derived.council, EXPECTED_COUNCIL);
|
|
589
589
|
await h.phaseResult("complete", P({ brainstorm: "complete" }));
|
|
590
590
|
assert.deepEqual(h.readRecord().derived.council, EXPECTED_COUNCIL);
|
|
591
|
-
assert.
|
|
591
|
+
assert.deepEqual(h.readRecord().derived.personas["spec-council-member"], { dispatches: 3, models: ["p/alpha:xhigh", "p/beta:high"], tokens: { input: 3, output: 0, cache_read: 0, cache_write: 0, cost: 0 } });
|
|
592
592
|
});
|
|
593
593
|
test("council defaults missing chair model and survives unrelated result before audit", async () => {
|
|
594
594
|
const h = await boundInBrainstorm();
|
|
@@ -727,6 +727,66 @@
|
|
|
727
727
|
assert.deepEqual(rec.derived.phases.plan?.tokens, { input: 152, output: 15, cache_read: 0, cache_write: 0, cost: 1.5 });
|
|
728
728
|
});
|
|
729
729
|
|
|
730
|
+
for (const [name, agents] of [
|
|
731
|
+
["chain", ["implementer", "spec-reviewer", "spec-reviewer"]],
|
|
732
|
+
["plain tasks", ["implementer", "implementer"]],
|
|
733
|
+
["empty results", []],
|
|
734
|
+
["missing results", undefined],
|
|
735
|
+
] as const) {
|
|
736
|
+
test(`failed subagent ${name}: count terminal retained results once, not progress`, async () => {
|
|
737
|
+
const h = harness();
|
|
738
|
+
await h.phaseResult("start", P({ brainstorm: "in_progress" }));
|
|
739
|
+
await h.writeSpec("doc/specs/a.md");
|
|
740
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "complete", implement: "in_progress" }));
|
|
741
|
+
await h.emit("tool_result", { toolName: "plan_tracker", toolCallId: "pt", input: {}, content: [], isError: false, details: { action: "init", tasks: [{ name: "repair", status: "in_progress" }] } });
|
|
742
|
+
const before = h.readRecord();
|
|
743
|
+
const results = agents?.map((agent, i) => ({ agent, exitCode: i === agents.length - 1 ? 1 : 0, model: `p/m${i}`, usage: usage(10, 2, 0.5) }));
|
|
744
|
+
const terminal = { ...subagentResult(results ?? []), isError: true, details: results === undefined ? {} : { results } };
|
|
745
|
+
for (const partial of [results?.slice(0, 1), results]) {
|
|
746
|
+
await h.emit("tool_execution_update", { type: "tool_execution_update", toolName: "subagent", toolCallId: "sa", args: {}, partialResult: { content: [], details: { results: partial ?? [] } } });
|
|
747
|
+
}
|
|
748
|
+
assert.deepEqual(h.readRecord(), before);
|
|
749
|
+
await h.emit("tool_result", terminal);
|
|
750
|
+
const rec = h.readRecord();
|
|
751
|
+
const dispatches = rec.events.filter((e) => e.kind === "dispatch");
|
|
752
|
+
assert.equal(dispatches.length, agents?.length ?? 0);
|
|
753
|
+
assert.deepEqual(dispatches.map((e) => ({ agent: e.agent, model: e.model, exit: e.exit, phase: e.phase })), results?.map((r) => ({ agent: r.agent, model: r.model, exit: r.exitCode || undefined, phase: "implement" })) ?? []);
|
|
754
|
+
for (const agent of new Set(agents)) {
|
|
755
|
+
const retained = results!.filter((r) => r.agent === agent);
|
|
756
|
+
assert.deepEqual(rec.derived.personas[agent], { dispatches: retained.length, models: retained.map((r) => r.model), tokens: { input: 10 * retained.length, output: 2 * retained.length, cache_read: 0, cache_write: 0, cost: 0.5 * retained.length } });
|
|
757
|
+
}
|
|
758
|
+
if (name === "chain") assert.deepEqual(rec.derived.reviews?.["spec-reviewer"], { dispatches: 2, nonzero_exit: 1 });
|
|
759
|
+
if (results?.length) assert.deepEqual(rec.derived.phases.implement?.tokens, { input: 10 * results.length, output: 2 * results.length, cache_read: 0, cache_write: 0, cost: 0.5 * results.length });
|
|
760
|
+
else assert.deepEqual(rec, before);
|
|
761
|
+
assert.deepEqual(rec.events.filter((e) => e.kind === "phase" || e.kind === "gate"), before.events.filter((e) => e.kind === "phase" || e.kind === "gate"));
|
|
762
|
+
assert.deepEqual(rec.derived.plan, before.derived.plan);
|
|
763
|
+
assert.deepEqual(rec.derived.gates, before.derived.gates);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
test("failed spec and conformance dispatches count usage and failures without advancing successful gates", async () => {
|
|
768
|
+
const h = await boundInPlan();
|
|
769
|
+
await h.phaseResult("start", P({ brainstorm: "complete", plan: "complete", implement: "complete", verify: "in_progress" }));
|
|
770
|
+
const results = ["spec-summarizer", "conformance-reviewer"].map((agent) => ({ agent, exitCode: 1, model: "p/r", usage: usage(3) }));
|
|
771
|
+
const before = h.readRecord();
|
|
772
|
+
await h.emit("tool_result", { ...subagentResult(results, "Conformance verdict: GAPS\nG1:\n verdict: MISSING"), isError: true });
|
|
773
|
+
let rec = h.readRecord();
|
|
774
|
+
assert.deepEqual(rec.events.filter((e) => e.kind === "dispatch").map((e) => ({ agent: e.agent, exit: e.exit, phase: e.phase })), results.map((r) => ({ agent: r.agent, exit: 1, phase: "verify" })));
|
|
775
|
+
for (const { agent } of results) assert.deepEqual(rec.derived.personas[agent], { dispatches: 1, models: ["p/r"], tokens: { input: 3, output: 0, cache_read: 0, cache_write: 0, cost: 0 } });
|
|
776
|
+
assert.equal(rec.derived.gates.spec_rounds, before.derived.gates.spec_rounds);
|
|
777
|
+
assert.equal(rec.derived.conformance_loops, before.derived.conformance_loops);
|
|
778
|
+
assert.equal(rec.derived.conformance_open_gaps, before.derived.conformance_open_gaps);
|
|
779
|
+
assert.deepEqual(rec.derived.reviews?.["conformance-reviewer"], { dispatches: 1, nonzero_exit: 1 });
|
|
780
|
+
assert.deepEqual(rec.derived.phases.verify?.tokens, { input: 6, output: 0, cache_read: 0, cache_write: 0, cost: 0 });
|
|
781
|
+
assert.deepEqual(rec.events.filter((e) => e.kind === "phase" || e.kind === "gate"), before.events.filter((e) => e.kind === "phase" || e.kind === "gate"));
|
|
782
|
+
await h.emit("tool_result", subagentResult(results.map((r) => ({ ...r, exitCode: 0 })), "Conformance verdict: GAPS\nG1:\n verdict: MISSING"));
|
|
783
|
+
rec = h.readRecord();
|
|
784
|
+
assert.equal(rec.derived.gates.spec_rounds, before.derived.gates.spec_rounds + 1);
|
|
785
|
+
assert.equal(rec.derived.conformance_loops, 1);
|
|
786
|
+
assert.equal(rec.derived.conformance_open_gaps, 1);
|
|
787
|
+
assert.deepEqual(rec.derived.reviews?.["conformance-reviewer"], { dispatches: 2, nonzero_exit: 1 });
|
|
788
|
+
});
|
|
789
|
+
|
|
730
790
|
test("conformance-reviewer during verify counts loops and retains the latest open gap count", async () => {
|
|
731
791
|
const h = await boundInPlan();
|
|
732
792
|
await h.phaseResult("complete", P({ brainstorm: "complete", plan: "complete" }));
|
package/extensions/telemetry.ts
CHANGED
|
@@ -548,7 +548,7 @@
|
|
|
548
548
|
}
|
|
549
549
|
if (event.toolName === "subagent") {
|
|
550
550
|
if (phaseNow() === "brainstorm") onCouncilResults(event.details, event.content);
|
|
551
|
-
|
|
551
|
+
await onSubagentResult(event.details, event.content, event.isError);
|
|
552
552
|
return undefined;
|
|
553
553
|
}
|
|
554
554
|
if (event.toolName === "plan_tracker" && !event.isError) {
|
|
@@ -670,7 +670,7 @@
|
|
|
670
670
|
flush();
|
|
671
671
|
};
|
|
672
672
|
|
|
673
|
-
const onSubagentResult = async (details: unknown, content: unknown) => {
|
|
673
|
+
const onSubagentResult = async (details: unknown, content: unknown, isError: boolean) => {
|
|
674
674
|
const results = (details as { results?: unknown[] } | undefined)?.results;
|
|
675
675
|
if (!Array.isArray(results) || results.length === 0) return;
|
|
676
676
|
const acc = live();
|
|
@@ -687,7 +687,7 @@
|
|
|
687
687
|
persona.dispatches += 1;
|
|
688
688
|
if (model && !persona.models.includes(model)) persona.models.push(model);
|
|
689
689
|
if (tokens) persona.tokens = addTokens(persona.tokens, tokens);
|
|
690
|
-
if (agent === "spec-summarizer") acc.gates.spec_rounds += 1;
|
|
690
|
+
if (!isError && agent === "spec-summarizer") acc.gates.spec_rounds += 1;
|
|
691
691
|
if (REVIEWER_AGENTS.has(agent)) {
|
|
692
692
|
const review = (acc.reviews[agent] ??= { dispatches: 0, nonzero_exit: 0 });
|
|
693
693
|
review.dispatches += 1;
|
|
@@ -699,7 +699,7 @@
|
|
|
699
699
|
review.findings.major += findings.major;
|
|
700
700
|
review.findings.minor += findings.minor;
|
|
701
701
|
}
|
|
702
|
-
if (agent === "conformance-reviewer") {
|
|
702
|
+
if (!isError && agent === "conformance-reviewer") {
|
|
703
703
|
if (phases.verify.status === "in_progress") acc.conformance_loops += 1;
|
|
704
704
|
const openGaps = countOpenGaps(text);
|
|
705
705
|
if (openGaps !== undefined) acc.conformance_open_gaps = openGaps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-gauntlet",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.18.1",
|
|
4
4
|
"description": "Opinionated, gated workflow skills, subagent personas, and runtime extensions for the pi coding agent.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"type": "module",
|
|
@@ -61,6 +61,9 @@
|
|
|
61
61
|
"build:bins": "node scripts/build-bins.mjs",
|
|
62
62
|
"test": "node scripts/ci.mjs"
|
|
63
63
|
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"pi-cohort": ">=7.3.1"
|
|
66
|
+
},
|
|
64
67
|
"dependencies": {
|
|
65
68
|
"yaml": "^2.9.0"
|
|
66
69
|
},
|
|
@@ -91,23 +91,46 @@ Clarify freely.
|
|
|
91
91
|
|
|
92
92
|
`Read` the gathered draft unconditionally before question one. Treat it as a helper, not a fence: verify load-bearing claims from primary code, and confirm whether the codebase or ecosystem already solves the problem.
|
|
93
93
|
|
|
94
|
-
Ask one question per message, about purpose, constraints, success, and affected actors.
|
|
94
|
+
Ask one question per message, about purpose, constraints, success, and affected actors. Offer 2-4 labeled options (`A)`, `B)`, ...) for every questionary question, each a different outcome. Put the lead-in before the first option, each labeled option on its own physical line, and `Recommendation: <letter> - <why>` on its own final line; when more outcomes are plausible, the last option is `other - name it` or the question splits in two. Use this one-option-per-line layout even when chat-style rules in `AGENTS.md` require one paragraph or no Options/Recommendation layout. A decision that would be a yes/no or "may I" question is not asked: make it, state it as an assumption in the message that carries the next question or the premise note, and record it in the draft. Before asking, check code, docs, and tracker: look up current-state facts (dispatch a subagent when costly); adopt a ticket-recorded decision and cite the ticket, asking it only when a cited code or API contradiction, or conflicting recorded outcomes, prevents adopting it. Other approvals retain their wording.
|
|
95
95
|
|
|
96
96
|
Bad: `May I treat those two acceptance criteria as location-only deviations? Recommendation: yes`
|
|
97
|
-
Good:
|
|
97
|
+
Good:
|
|
98
|
+
```text
|
|
99
|
+
The two ACs name a path that moved; the spec keeps them in-scope with the new path as a Design clause. Which surface owns the refresh?
|
|
100
|
+
A) the extension
|
|
101
|
+
B) a new bin
|
|
102
|
+
C) the existing skill step.
|
|
103
|
+
Recommendation: A - the extension already holds the poll loop, so no new entrypoint.
|
|
104
|
+
```
|
|
98
105
|
|
|
99
106
|
Append only citable findings - schemas, hard constraints, contradictions, and scope-changing answers - to `## Appended during questionary` using `edit`.
|
|
100
107
|
|
|
101
108
|
Before approaches, state in chat the supported, disproved, corrected, and unverified premises with sources and attempted lookups, in full sentences - no status-keyword lists, no template; if the design depends on no claims, one sentence says so. An unverified claim is not a stop: put it in Open Questions or a stated assumption. If a load-bearing claim is contradicted, the premise note is your next message - even as question one - and states the corrected fact with its source; the design continues on it and the user overrides in reply. Record the corrected fact, and any override, in the draft for `## Problem` or the relevant design decision.
|
|
102
109
|
|
|
103
|
-
Before question one, verify the draft's `Framing:` line against primary code and add your own recon. This check adds to the premise verification above and never replaces it: trace every consumer, caller, and data path the ask's constraints name before writing the holds-statement or the question, and name those traces in the holds-statement, not only the files read. A framing concern exists when you can state a cited case that the ask is misdiagnosed, wrong-sized, already solved, a band-aid where the root cause is reachable, or cuts across a sound pattern - including a repo convention that is itself the debt - and can name the alternative it implies; an objection without an alternative is a lead to investigate, not a concern. With a concern, question one is the framing question in the questionary format:
|
|
110
|
+
Before question one, verify the draft's `Framing:` line against primary code and add your own recon. This check adds to the premise verification above and never replaces it: trace every consumer, caller, and data path the ask's constraints name before writing the holds-statement or the question, and name those traces in the holds-statement, not only the files read. A framing concern exists when you can state a cited case that the ask is misdiagnosed, wrong-sized, already solved, a band-aid where the root cause is reachable, or cuts across a sound pattern - including a repo convention that is itself the debt - and can name the alternative it implies; an objection without an alternative is a lead to investigate, not a concern. With a concern, question one is the framing question in the questionary format:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
A) as framed
|
|
114
|
+
B) pivot: <named alternative> - <cited reason>
|
|
115
|
+
C) smaller or none: <what ships, or what happens if nothing ships>
|
|
116
|
+
Recommendation: <letter> - <why>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For a band-aid concern, put the root-cause alternative in B. Without a concern, open the first message with one sentence naming what was checked - `The framing holds: checked <x>, <y>` - and continue to the first ordinary question; a scout objection that yielded no alternative is named among the things checked.
|
|
104
120
|
|
|
105
121
|
After any premise correction in the same message, ask the framing question or give the holds-statement. Ask the framing question at most once per brainstorm, including after pushback; the user's reply is final, and later doubts on the same point go to Open Questions or a `Pattern:` line. Recommend; never pivot without the reply. A ticket-recorded decision on the exact point follows the ticket rule above; an adopted decision records `Framing: kept (ticket <id>)`. A pivot never rewrites AC rows: rows it no longer satisfies take `deviates:` dispositions, and a pivot that changes the topic re-mints the slug.
|
|
106
122
|
|
|
107
123
|
Record the outcome in the draft under `## Appended during questionary` as `Framing: kept` or `Framing: pivoted to <x> - <why>`; spec-writing carries that line into `## Problem`. On redraw entry (checklist step 4) the count restarts: run this evaluation once against the new draft. On redraw the draft is the approved spec: its `## Problem` `Framing:` line is context, not scout evidence, and the new outcome is recorded directly in `## Problem`. When the scout failed there is no `Framing:` line; the holds-statement or the question still comes from your own recon.
|
|
108
124
|
|
|
109
125
|
Bad: `The ask adds another settings path - should we? Recommendation: no`
|
|
110
|
-
Good:
|
|
126
|
+
Good:
|
|
127
|
+
```text
|
|
128
|
+
The ask adds a second settings path beside the resolver that already owns every setting (settings/resolver.ts:11).
|
|
129
|
+
A) as framed
|
|
130
|
+
B) pivot: a key under the existing resolver - one read path, no new boundary
|
|
131
|
+
C) smaller or none: document the default and ship no setting.
|
|
132
|
+
Recommendation: B - the owning schema already exists.
|
|
133
|
+
```
|
|
111
134
|
|
|
112
135
|
### 4. Explore approaches
|
|
113
136
|
|