@gobing-ai/spur 0.3.76 → 0.3.78
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/.claude-plugin/marketplace.json +1 -1
- package/config/plugin-scripts.json +4 -0
- package/config/rules/strict/runtime-boundaries.yaml +1 -0
- package/config/workflows/task-pipeline.yaml +19 -6
- package/package.json +9 -9
- package/plugins/sp/commands/dev-refineall.md +1 -0
- package/plugins/sp/commands/dev-run.md +1 -0
- package/plugins/sp/commands/dev-runall.md +1 -0
- package/plugins/sp/commands/dev-verifyall.md +1 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +173 -2
- package/plugins/sp/scripts/batch-preflight.ts +257 -2
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +139 -26
- package/plugins/sp/skills/code-verification/SKILL.md +4 -2
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -2
- package/plugins/sp/skills/dogfood-testing/SKILL.md +41 -36
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +34 -10
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +10 -6
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +33 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +17 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +133 -16
- package/spur.js +10328 -9186
|
@@ -257,29 +257,129 @@ function extractRequirementIds(taskContent: string): string[] {
|
|
|
257
257
|
return [...ids];
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
// ─── Canonical AC identity resolution (task 0804 R4) ─────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Strip the tolerated AC-id wrappers (bracket tags, `Scenario:` prefix) until
|
|
264
|
+
* fixpoint — the shared first step of `normalizeAcTitle` and the `AC-N` alias
|
|
265
|
+
* path in `resolveAcIdentity`.
|
|
266
|
+
*/
|
|
267
|
+
function stripAcWrappers(title: string): string {
|
|
268
|
+
let out = title.trim();
|
|
269
|
+
let prev: string;
|
|
270
|
+
do {
|
|
271
|
+
prev = out;
|
|
272
|
+
out = out
|
|
273
|
+
.replace(/^\[[^\]]*\]\s*/, '')
|
|
274
|
+
.replace(/\s*\[[^\]]*\]\s*$/, '')
|
|
275
|
+
.replace(/^Scenario:\s*/i, '')
|
|
276
|
+
.trim();
|
|
277
|
+
} while (out !== prev);
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Normalize an AC identity to its canonical key, mirroring the documented
|
|
283
|
+
* matching behavior of feature-check `rowMatchesScenario` + ac-style-guide
|
|
284
|
+
* "Four accepted id forms" (exact/bare title, `Scenario:` prefix, bracket
|
|
285
|
+
* tags, `AC-N` ordinal) without importing that private matcher or adopting
|
|
286
|
+
* its permissive trailing-Gherkin fallback (0804 R4). Comparison is
|
|
287
|
+
* case/quote/whitespace-insensitive. A paraphrase normalizes differently
|
|
288
|
+
* and still fails.
|
|
289
|
+
*/
|
|
290
|
+
function normalizeAcTitle(title: string): string {
|
|
291
|
+
return (
|
|
292
|
+
stripAcWrappers(title)
|
|
293
|
+
.replace(/^R\d+\s*[:\-—]?\s*/, '')
|
|
294
|
+
.toLowerCase()
|
|
295
|
+
// Exact pre-refactor removal set (0809 R5): ASCII apostrophe + the four curly
|
|
296
|
+
// quotes. Escaped form keeps U+0027 visible next to lookalike curly glyphs;
|
|
297
|
+
// U+02BC stays a meaningful character, never removable punctuation.
|
|
298
|
+
.replace(/[\u0027\u2018\u2019\u201c\u201d]/g, '')
|
|
299
|
+
.replace(/\s+/g, ' ')
|
|
300
|
+
.trim()
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Declared AC identities keyed by canonical normalized title, plus the two
|
|
306
|
+
* AC-N ordinal sources (task scenario list and linked-feature scenario list).
|
|
307
|
+
* A checklist-declared spelling always wins over the positional alias.
|
|
308
|
+
*/
|
|
309
|
+
interface AcIdentityIndex {
|
|
310
|
+
/** normalized canonical title → a declared spelling (label, token, or title). */
|
|
311
|
+
readonly byTitle: Map<string, string>;
|
|
312
|
+
/** AC-N → task scenario title at that 1-based ordinal. */
|
|
313
|
+
readonly taskScenarios: string[];
|
|
314
|
+
/** AC-N → feature scenario title at that 1-based ordinal. */
|
|
315
|
+
readonly featureScenarios: string[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildAcIdentityIndex(taskContent: string, featureContent: string | null): AcIdentityIndex {
|
|
319
|
+
const byTitle = new Map<string, string>();
|
|
320
|
+
// Not named `declare`: Bun's TS transpiler treats a call to an identifier
|
|
321
|
+
// named `declare` as an ambient-declaration modifier and drops it.
|
|
322
|
+
const declareIdentity = (spelling: string): void => {
|
|
323
|
+
const key = normalizeAcTitle(spelling);
|
|
324
|
+
if (key !== '' && !byTitle.has(key)) byTitle.set(key, spelling);
|
|
325
|
+
};
|
|
262
326
|
const section = sectionBetween(taskContent, 'Acceptance Criteria');
|
|
263
|
-
// Checkbox labels (`- [x] AC1 (R1): …`, 0726) and plain bullets (`- AC1: Given …`,
|
|
264
|
-
// 0713/0727) both yield the label text up to `:` plus its leading token.
|
|
265
327
|
for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?(.+?)\s*(?::|$)/gm)) {
|
|
266
328
|
const label = (m[1] ?? '').trim();
|
|
267
329
|
if (!label) continue;
|
|
268
|
-
|
|
330
|
+
declareIdentity(label);
|
|
269
331
|
const leading = label.split(/\s+/)[0] ?? '';
|
|
270
|
-
if (leading && leading !== label)
|
|
332
|
+
if (leading && leading !== label) declareIdentity(leading);
|
|
271
333
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
334
|
+
const scenarioTitles = (content: string): string[] =>
|
|
335
|
+
[...content.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)].map((m) => (m[1] ?? '').trim()).filter((t) => t !== '');
|
|
336
|
+
const taskScenarios = scenarioTitles(sectionBetween(taskContent, 'Acceptance Criteria'));
|
|
337
|
+
const featureScenarios = featureContent !== null ? scenarioTitles(featureContent) : [];
|
|
338
|
+
for (const title of [...taskScenarios, ...featureScenarios]) declareIdentity(title);
|
|
339
|
+
return { byTitle, taskScenarios, featureScenarios };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Resolution outcome for one AC row id. */
|
|
343
|
+
type AcIdentityResolution = { ok: true; canonical: string } | { ok: false; error: string };
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve an answer-file AC row id to one canonical task identity (0804 R4):
|
|
347
|
+
* exact/declared title forms first, then the documented `AC-N` positional
|
|
348
|
+
* alias — accepted only against a real scenario ordinal, and refused with an
|
|
349
|
+
* actionable diagnostic when task and feature ordinals disagree. Undeclared
|
|
350
|
+
* `ACn` tokens, paraphrases and invented ordinals never resolve.
|
|
351
|
+
*/
|
|
352
|
+
function resolveAcIdentity(rowId: string, index: AcIdentityIndex): AcIdentityResolution {
|
|
353
|
+
const canonical = index.byTitle.get(normalizeAcTitle(rowId));
|
|
354
|
+
if (canonical !== undefined) return { ok: true, canonical };
|
|
355
|
+
// Strip the tolerated wrappers, then try the documented `AC-N` alias.
|
|
356
|
+
const stripped = stripAcWrappers(rowId);
|
|
357
|
+
const ordinal = /^AC-(\d+)$/i.exec(stripped);
|
|
358
|
+
if (ordinal !== null) {
|
|
359
|
+
const n = Number(ordinal[1]);
|
|
360
|
+
const taskTitle = index.taskScenarios[n - 1];
|
|
361
|
+
const featureTitle = index.featureScenarios[n - 1];
|
|
362
|
+
const candidates = [...new Set([taskTitle, featureTitle].filter((t): t is string => t !== undefined))];
|
|
363
|
+
if (candidates.length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error:
|
|
367
|
+
`AC id "${rowId}" uses the AC-${n} positional alias but no scenario exists at that ordinal ` +
|
|
368
|
+
'(task scenario list and linked-feature scenario list) — cite the exact scenario title or checklist label',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (candidates.length > 1) {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
error:
|
|
375
|
+
`AC id "${rowId}" is ambiguous: task AC #${n} ("${taskTitle}") and feature scenario #${n} ` +
|
|
376
|
+
`("${featureTitle}") are different scenarios with different ordering — cite the exact title`,
|
|
377
|
+
};
|
|
280
378
|
}
|
|
379
|
+
const resolved = index.byTitle.get(normalizeAcTitle(candidates[0] ?? ''));
|
|
380
|
+
if (resolved !== undefined) return { ok: true, canonical: resolved };
|
|
281
381
|
}
|
|
282
|
-
return
|
|
382
|
+
return { ok: false, error: '' };
|
|
283
383
|
}
|
|
284
384
|
|
|
285
385
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
@@ -341,7 +441,7 @@ function main(): void {
|
|
|
341
441
|
}
|
|
342
442
|
|
|
343
443
|
const reqIds = extractRequirementIds(taskContent);
|
|
344
|
-
const
|
|
444
|
+
const acIndex = buildAcIdentityIndex(taskContent, featureContent);
|
|
345
445
|
|
|
346
446
|
// Requirement rows: completeness, no unknowns, no duplicates, valid status, non-empty evidence.
|
|
347
447
|
const seenReq = new Set<string>();
|
|
@@ -358,19 +458,31 @@ function main(): void {
|
|
|
358
458
|
if (!seenReq.has(id)) add(`missing requirement row for "${id}"`);
|
|
359
459
|
}
|
|
360
460
|
|
|
361
|
-
// AC rows: identity must
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
461
|
+
// AC rows: identity must resolve to ONE canonical task AC identity — a
|
|
462
|
+
// checklist label/token or a scenario title in any ac-style-guide form
|
|
463
|
+
// (exact/bare title, `Scenario:` prefix, bracket tags, declared AC-N
|
|
464
|
+
// alias; 0804 R4). Alias-equivalent spellings of the same identity are
|
|
465
|
+
// duplicates even when the raw strings differ. Status and evidence type
|
|
466
|
+
// must normalize; evidence non-empty. AC completeness is the verifier's
|
|
467
|
+
// authoring contract, not a lint rejection class (0726 R3).
|
|
468
|
+
const seenAc = new Map<string, string>(); // canonical key → first raw row id
|
|
365
469
|
for (const row of tables.acs) {
|
|
366
|
-
|
|
470
|
+
const resolution = resolveAcIdentity(row.id, acIndex);
|
|
471
|
+
const canonicalKey = resolution.ok ? normalizeAcTitle(resolution.canonical) : null;
|
|
472
|
+
if (!resolution.ok) {
|
|
473
|
+
if (resolution.error !== '') add(`line ${row.line}: ${resolution.error}`);
|
|
474
|
+
else
|
|
475
|
+
add(
|
|
476
|
+
`line ${row.line}: AC ID "${row.id.slice(0, 60)}" matches no task AC checklist label or scenario title ` +
|
|
477
|
+
'(accepted forms: exact title, bare title, `Scenario:` prefix, bracket tags, declared AC-N alias)',
|
|
478
|
+
);
|
|
479
|
+
} else if (canonicalKey !== null && seenAc.has(canonicalKey)) {
|
|
480
|
+
const first = seenAc.get(canonicalKey) ?? '';
|
|
367
481
|
add(
|
|
368
|
-
`line ${row.line}: AC
|
|
482
|
+
`line ${row.line}: duplicate AC row "${row.id.slice(0, 60)}" — alias-equivalent to "${first.slice(0, 60)}"`,
|
|
369
483
|
);
|
|
370
|
-
} else if (seenAc.has(row.id)) {
|
|
371
|
-
add(`line ${row.line}: duplicate AC row "${row.id.slice(0, 60)}"`);
|
|
372
484
|
}
|
|
373
|
-
seenAc.
|
|
485
|
+
if (canonicalKey !== null && !seenAc.has(canonicalKey)) seenAc.set(canonicalKey, row.id);
|
|
374
486
|
if (normalizeAcStatus(row.status) === null)
|
|
375
487
|
add(`line ${row.line}: invalid AC status "${row.status}" (MET | PARTIAL | UNMET | N/A)`);
|
|
376
488
|
if (normalizeEvidenceType(row.evidenceType) === null)
|
|
@@ -396,4 +508,5 @@ function main(): void {
|
|
|
396
508
|
process.exit(0);
|
|
397
509
|
}
|
|
398
510
|
|
|
399
|
-
|
|
511
|
+
// CLI entry (guarded so the helpers stay importable for focused tests).
|
|
512
|
+
if (import.meta.main) main();
|
|
@@ -117,8 +117,8 @@ assign a per-requirement status:
|
|
|
117
117
|
| **PARTIAL** | Evidence for part of the requirement only |
|
|
118
118
|
| **UNMET** | No implementation evidence found |
|
|
119
119
|
|
|
120
|
-
Record the evidence string (repo-relative
|
|
121
|
-
in `## Testing`.
|
|
120
|
+
Record the evidence string (repo-relative `file:line`, command, or test name) per requirement —
|
|
121
|
+
this lands in `## Testing`.
|
|
122
122
|
|
|
123
123
|
**Line-anchor verification (anti-stale-citation rule).** Every `file:line` evidence citation
|
|
124
124
|
written into the Testing table MUST be re-read at the cited lines this run, and the re-read content
|
|
@@ -142,6 +142,8 @@ classifies it as external and never raises `L4.stale-line-anchor` for it (R1). D
|
|
|
142
142
|
that lives in this repo — in-repo evidence MUST use the repo-relative backtick form
|
|
143
143
|
`` `path:line` `` / `` `path:start-end` ``, and citing it in the external form still reports (R2).
|
|
144
144
|
|
|
145
|
+
**Concrete anchors (0804 R9):** cite existing `file:line`s, never globs — `references/verdict-schema.md`.
|
|
146
|
+
|
|
145
147
|
### Step 5 — Acceptance Criteria guard
|
|
146
148
|
|
|
147
149
|
If the task has a non-empty Acceptance Criteria section, evaluate every checklist item and every
|
|
@@ -110,12 +110,21 @@ For answer files, emit a matching parseable table:
|
|
|
110
110
|
`Verdict: PARTIAL` first, append one complete row at a time, and replace the first verdict line only
|
|
111
111
|
after every row is certified. `verify-answer-lint.ts` gates the file before `spur task verdict
|
|
112
112
|
--from-answer` and rejects, with row-level diagnostics: missing/duplicate/unknown requirement IDs,
|
|
113
|
-
AC ids that do not
|
|
114
|
-
linked feature scenario title,
|
|
113
|
+
AC ids that do not resolve to one accepted identity — a task AC checklist label or its declared
|
|
114
|
+
`AC-N`/checklist-token alias, or a linked feature scenario title, in the ac-style-guide forms
|
|
115
|
+
(exact/bare title, `Scenario:` prefix, bracket tags, `AC-N`); paraphrases and ambiguous aliases
|
|
116
|
+
fail — invalid status (`MET | PARTIAL | UNMET` for requirements;
|
|
115
117
|
`N/A` additionally allowed for AC), invalid evidence type (`test | command | static-ref |
|
|
116
118
|
manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Interrupted runs keep the
|
|
117
119
|
rows that pass the lint and complete only the missing IDs on retry.
|
|
118
120
|
|
|
121
|
+
**Concrete anchors only (task 0804 R9).** An evidence anchor must be a concrete existing `file:line`
|
|
122
|
+
(or `file:start-end`) path. A glob or directory summary (`src/services/*.ts`, `the retry
|
|
123
|
+
classifiers in task-pipeline.yaml`) is not an anchor: expand it into the specific cited files/ranges
|
|
124
|
+
the run actually verified. Since 0804 R9 the checker ignores complete parsed citation spans before
|
|
125
|
+
scanning for subjects, so a citation's filename (including snake_case paths) can never become a
|
|
126
|
+
false subject — a real absent symbol, nonexistent file, or invalid range still reports.
|
|
127
|
+
|
|
119
128
|
## Checks evidence
|
|
120
129
|
|
|
121
130
|
Wave C verification can emit the following additive `checks[]` rows:
|
|
@@ -39,7 +39,8 @@ sinks; this skill owns the protocol.
|
|
|
39
39
|
testee (a /sp:... command, Skill(...), or shell CLI invocation)
|
|
40
40
|
→ PLAN classify + derive steps + open dual artifacts (live + docs/dogfood) with status:running
|
|
41
41
|
→ EXECUTE run each step as a user; on failure, bounded diagnose→fix→re-run (or observe-only)
|
|
42
|
-
→ MONITOR
|
|
42
|
+
→ MONITOR live-ledger row to disk every step resolve; mirror frozen inside a proof window —
|
|
43
|
+
never reconstruct from memory
|
|
43
44
|
→ REPORT finalize-or-abort (non-skippable): status complete|aborted, Cost block, both paths, footer
|
|
44
45
|
```
|
|
45
46
|
|
|
@@ -105,9 +106,8 @@ The command forwards these via `$ARGUMENTS`:
|
|
|
105
106
|
```
|
|
106
107
|
|
|
107
108
|
- Exit **2** → print the stdout refuse line and **stop** (do not plan). The CLI refuses on either
|
|
108
|
-
of two independent mutation sources (task 0293);
|
|
109
|
-
|
|
110
|
-
- mutating `--fix`: `⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)`.
|
|
109
|
+
of two independent mutation sources (task 0293); its two refuse lines are the ones quoted
|
|
110
|
+
verbatim in the repo-mutation warning above.
|
|
111
111
|
- Exit **0** → proceed. Do not auto-substitute `--max-retry 0`.
|
|
112
112
|
- The matcher contract is unit-checked by `tests/dogfood-testing/pipeline-detect.test.ts`.
|
|
113
113
|
See [§Pipeline-driving word-boundary contract](#pipeline-driving-word-boundary-contract) and
|
|
@@ -186,17 +186,18 @@ report — the report is assembled from the files, not from memory.
|
|
|
186
186
|
On **every** step resolve:
|
|
187
187
|
|
|
188
188
|
1. Append/update the ledger row on the **live** file first.
|
|
189
|
-
2. Mirror the same row to the **report** path under `docs/dogfood
|
|
189
|
+
2. Mirror the same row to the **report** path under `docs/dogfood/` — unconditional **outside** a
|
|
190
|
+
pipeline proof window; **inside** one the mirror stays **frozen** (live rows only) until the
|
|
191
|
+
window closes, then sync/validate with live-based recovery (task 0804 R2 —
|
|
192
|
+
[monitor-ledger.md](references/monitor-ledger.md) live-ledger rule 3).
|
|
190
193
|
3. Do **not** batch rows until Phase 4.
|
|
191
194
|
|
|
192
|
-
The final report MUST include a `### 3. Monitor Ledger` section containing those rows
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
**[monitor-ledger.md](references/monitor-ledger.md)**. Apply the conservation discipline while
|
|
199
|
-
monitoring — low cache% is usually the driver re-fetching data it already holds.
|
|
195
|
+
The final report MUST include a `### 3. Monitor Ledger` section containing those rows (cardinality:
|
|
196
|
+
row count == the declared executed steps). Cardinality, full methodology, column contract,
|
|
197
|
+
token/cache estimation, multi-source Cost honesty, the cache-health finding rule, and the
|
|
198
|
+
**cache-conservation discipline** live in
|
|
199
|
+
**[monitor-ledger.md](references/monitor-ledger.md)** — apply conservation while monitoring; low
|
|
200
|
+
cache% is usually the driver re-fetching data it already holds.
|
|
200
201
|
|
|
201
202
|
## Phase 4 — Report (finalize-or-abort — non-skippable)
|
|
202
203
|
|
|
@@ -214,8 +215,9 @@ contract violation**.
|
|
|
214
215
|
declared in §2 (N/A steps documented explicitly as rows). A mismatch refuses `complete`.
|
|
215
216
|
4. Write the **Cost** block under §2 (ledger `~estimate` + Method + confidence; `Meter: n/a` or
|
|
216
217
|
optional ccusage/agent usage when real). For any `chained:<step>` ledger row whose meter is not
|
|
217
|
-
observable, Fresh/Cached MUST be `~unknown`
|
|
218
|
-
|
|
218
|
+
observable, Fresh/Cached MUST be `~unknown` — excluded from the cache% aggregate (or surfaced as
|
|
219
|
+
a separate unknown bucket), never counted as `Cached ~0` — **and** emit finding
|
|
220
|
+
`P3 — chained-step cost not observable`; never invent chained totals.
|
|
219
221
|
5. **R2 drift check at finalize.** If a workspace fingerprint was recorded in Phase 1, re-take
|
|
220
222
|
the snapshot and diff against baseline minus the run's own touched files. If drift is detected,
|
|
221
223
|
append a `drift:external` warning row to the ledger and emit a mandatory P2 report finding
|
|
@@ -292,13 +294,15 @@ Do **not** use this skill for:
|
|
|
292
294
|
mutating pipeline — pass `--max-retry 0` first and inspect the findings before letting it apply
|
|
293
295
|
fixes.
|
|
294
296
|
2. **The ledger is live on disk, not reconstructed.** Honest fixed-vs-unresolved accounting depends
|
|
295
|
-
on
|
|
296
|
-
fiction. Working-memory-only ledgers are a contract
|
|
297
|
+
on writing each step *as it happens* to the live file (mirror per Phase 3 — frozen inside a proof
|
|
298
|
+
window). Reconstructing at the end produces fiction. Working-memory-only ledgers are a contract
|
|
299
|
+
violation.
|
|
297
300
|
3. **A hiding fix is a finding.** If "fixing" a step would mask the bug, log it as a finding and
|
|
298
301
|
leave the step unresolved.
|
|
299
302
|
4. **Token numbers are estimates, but cache math is not free-form.** A skill cannot read its own
|
|
300
303
|
exact token meter, so label numbers `~estimate` and put Method + confidence in the Cost block;
|
|
301
|
-
however, cache% must be recomputable from Monitor Ledger row sums
|
|
304
|
+
however, cache% must be recomputable from Monitor Ledger row sums of observable rows (`~unknown`
|
|
305
|
+
rows are excluded from the aggregate, never counted as `~0` cached). Never invent or reuse a fixed
|
|
302
306
|
percentage. Optional meters (`ccusage`, agent usage) are session/day scope — never fake per-step.
|
|
303
307
|
5. **Testee-scoped `--agent`.** Don't confuse the driver agent (always current) with the testee
|
|
304
308
|
agent (the forwarded value).
|
|
@@ -431,11 +435,10 @@ baseline is drift.
|
|
|
431
435
|
### Worktree advisory (mutating dogfoods)
|
|
432
436
|
|
|
433
437
|
For fix-mode dogfoods of **pipeline-driving** or **mutating-`--fix`** testees (the two refuse-gate
|
|
434
|
-
cases above),
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
preferred setup for mutating dogfoods where the operator cares about clean attribution.
|
|
438
|
+
cases above), run in an **isolated `git worktree`** — **advisory, not a hard gate**. Phase 1 must
|
|
439
|
+
print this advisory whenever the driver or testee may mutate and the tree is dirty, **or** the
|
|
440
|
+
testee drives a pipeline (incl. observe-only over a mutating testee). Rationale, trigger matrix
|
|
441
|
+
and wording: `references/monitor-ledger.md`.
|
|
439
442
|
|
|
440
443
|
## Step-splitting recipe (implement-heavy pipeline dogfoods)
|
|
441
444
|
|
|
@@ -486,8 +489,9 @@ Rules:
|
|
|
486
489
|
2. When the chained step ran in a subagent or session whose usage data the driver cannot read, label
|
|
487
490
|
the chained row `~unknown` and emit a **P3** finding: "chained-step cost not observable — candidate
|
|
488
491
|
for surfacing subagent usage in the driver context." Do not invent a number.
|
|
489
|
-
3.
|
|
490
|
-
|
|
492
|
+
3. An observable chained row counts toward the aggregate cache%. A `~unknown` chained row is
|
|
493
|
+
**excluded** from the aggregate (or surfaced as a separate unknown bucket) — never folded in as
|
|
494
|
+
`Cached = ~0` — per the anti-fiction rule in
|
|
491
495
|
[monitor-ledger.md](references/monitor-ledger.md).
|
|
492
496
|
|
|
493
497
|
## `--next` chain stop-at-testing
|
|
@@ -518,18 +522,17 @@ Do NOT:
|
|
|
518
522
|
driver permission to read the chained leg's named artifacts (`.spur/run/<wbs>-verdict.json`,
|
|
519
523
|
task-file section diffs, review tables) after the leg completes and attribute normally. The flag
|
|
520
524
|
licenses **reading** chained-leg evidence that already exists — it does NOT license the driver to
|
|
521
|
-
execute the chained leg itself. The legacy "operator may direct" prose direction
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
[§Arguments](#arguments).
|
|
525
|
+
execute the chained leg itself. The legacy "operator may direct" prose direction stays honored for
|
|
526
|
+
back-compat; omitting the flag keeps stop-at-testing as the **default**. The flag is a driver
|
|
527
|
+
attribute only — it does not change `detect-pipeline-driving` gate semantics (it is not a testee
|
|
528
|
+
mutation source). See [§Arguments](#arguments).
|
|
526
529
|
|
|
527
530
|
## Additional Resources
|
|
528
531
|
|
|
529
|
-
- [references/report-template.md](references/report-template.md) —
|
|
530
|
-
mandatory
|
|
531
|
-
- [references/monitor-ledger.md](references/monitor-ledger.md) —
|
|
532
|
-
token/cache estimation
|
|
532
|
+
- [references/report-template.md](references/report-template.md) — report section contract,
|
|
533
|
+
mandatory footer, task-sink L3 rule.
|
|
534
|
+
- [references/monitor-ledger.md](references/monitor-ledger.md) — live-ledger column contract,
|
|
535
|
+
token/cache estimation, cache-health finding rule.
|
|
533
536
|
|
|
534
537
|
## Platform Notes
|
|
535
538
|
|
|
@@ -565,8 +568,10 @@ shape just because `report-template.md` wasn't auto-loaded.
|
|
|
565
568
|
- Report: `docs/dogfood/YYYY-MM-DD-<testee-slug>-dogfood.md`
|
|
566
569
|
|
|
567
570
|
Both start with YAML frontmatter including `status: running | aborted | complete`, `run_id`,
|
|
568
|
-
`protocol: sp:dogfood-testing@1.2`, and paths.
|
|
569
|
-
resolve
|
|
571
|
+
`protocol: sp:dogfood-testing@1.2`, and paths. Write each ledger row to the live file on every step
|
|
572
|
+
resolve; mirror it to the report only outside a proof window — frozen inside one, synced at
|
|
573
|
+
finalize (Phase 3). On stop, set `status` to `complete` or
|
|
574
|
+
`aborted` (finalize-or-abort — non-skippable).
|
|
570
575
|
|
|
571
576
|
**The six mandatory section headings** (in order, each report MUST contain all six):
|
|
572
577
|
|
|
@@ -28,9 +28,14 @@ artifacts):
|
|
|
28
28
|
1. **Open both artifacts in Phase 1**, before the first step runs (frontmatter `status: running` +
|
|
29
29
|
empty ledger table in each).
|
|
30
30
|
2. **Write a row the instant a step resolves** (pass, fixed, unresolved, or N/A) — not after the run.
|
|
31
|
-
3. **Dual-write every step
|
|
32
|
-
**
|
|
33
|
-
|
|
31
|
+
3. **Dual-write every step — EXCEPT inside a proof window (task 0804 R2).** Append/update the row
|
|
32
|
+
on the **live** file first; the live file is SSOT. While a pipeline proof window is open (from
|
|
33
|
+
the first proof capture until the final proof-sensitive action, including done/provenance
|
|
34
|
+
checks), the tracked report mirror stays **frozen**: append each observation to the live ledger
|
|
35
|
+
only, and sync/validate the mirror after the window closes (or after abort). Tracked reports are
|
|
36
|
+
proof-input fingerprint inputs — writing them mid-window churns the fingerprint and voids the
|
|
37
|
+
proof. If the report write fails at finalize, recover by recreating the mirror from the valid
|
|
38
|
+
live content and re-validate; missing live evidence cannot manufacture complete.
|
|
34
39
|
4. **The report reads the on-disk ledger, not your memory.** Every number in the report traces to a
|
|
35
40
|
ledger row on disk. If it is not in the ledger file, it does not go in the report.
|
|
36
41
|
5. **Cardinality (@1.2).** The ledger's data-row count MUST equal the `**Steps:** N derived, N executed` declared
|
|
@@ -44,6 +49,19 @@ artifacts):
|
|
|
44
49
|
a `FIXED` / `PASS` outcome — it is purely documentary. Cache columns carry `—` (not estimated).
|
|
45
50
|
See [SKILL.md §Workspace-drift guard](../SKILL.md#workspace-drift-guard-r2--task-0296).
|
|
46
51
|
|
|
52
|
+
### Worktree advisory — planning-time surfacing (task 0804 R5)
|
|
53
|
+
|
|
54
|
+
The worktree advisory (SKILL.md §Worktree advisory) is surfaced at planning time, not only when
|
|
55
|
+
drift is detected. Phase 1 prints it whenever the **driver or the testee may mutate** and the tree
|
|
56
|
+
is dirty (`git status --porcelain` non-empty) **or** the testee itself drives a pipeline —
|
|
57
|
+
including observe-only driver mode over a mutating testee. The advisory is **not a hard gate** (the
|
|
58
|
+
refuse-gate semantics from task 0293 are unchanged); the isolated checkout is simply the preferred
|
|
59
|
+
setup for mutating dogfoods because no concurrent writer can reach it, so attribution stays clean
|
|
60
|
+
— which is why the §Mutating `--fix` mode contract recommends it for those two refuse-gate cases.
|
|
61
|
+
Dirtiness alone is not proof of a concurrent writer and must not be reported as one; known
|
|
62
|
+
concurrent writes still follow the project's one-writer rule. A clean, read-only run adds no
|
|
63
|
+
warning.
|
|
64
|
+
|
|
47
65
|
### Fast-run exemption (task 0294 R6a)
|
|
48
66
|
|
|
49
67
|
The per-step live-write mandate (rules 1–4) exists to bound information loss when a mid-run crash
|
|
@@ -81,7 +99,7 @@ in the report's §6 Findings (no exemption applies).
|
|
|
81
99
|
| `Finding` | One-line finding surfaced at this step, or `—`. A finding does **not** change `Outcome`. |
|
|
82
100
|
| `Fresh Tokens` | Estimated fresh context for the step. Prefix with `~`. |
|
|
83
101
|
| `Cached Tokens` | Estimated reused context for the step. Prefix with `~`. |
|
|
84
|
-
| `Cache %` | `Cached Tokens / (Fresh Tokens + Cached Tokens)`, rounded to the nearest whole percent. |
|
|
102
|
+
| `Cache %` | `Cached Tokens / (Fresh Tokens + Cached Tokens)`, rounded to the nearest whole percent. An `~unknown` row carries `—`, never `0%` — unknown basis is not an observed zero. |
|
|
85
103
|
| `Basis` | Observable basis for the estimate: command output, prior file read reused, generated text, etc. |
|
|
86
104
|
| `Wall-clock` | Elapsed time for the step. |
|
|
87
105
|
|
|
@@ -98,8 +116,10 @@ A skill **cannot read its own exact token meter** — derive an estimate and lab
|
|
|
98
116
|
reused by reference in this step. Use the same `ceil(characters / 4)` basis and round to the
|
|
99
117
|
nearest 100. Do not count fresh command output, newly read files, or regenerated scaffolding as
|
|
100
118
|
cached.
|
|
101
|
-
3. Compute each row: `Cache % = round(Cached Tokens / (Fresh Tokens + Cached Tokens) * 100)`.
|
|
102
|
-
|
|
119
|
+
3. Compute each row: `Cache % = round(Cached Tokens / (Fresh Tokens + Cached Tokens) * 100)`. A row
|
|
120
|
+
whose basis is unknown carries `—`, never `0%`.
|
|
121
|
+
4. Compute the report aggregate from **observable rows only** — `~unknown` rows are excluded from
|
|
122
|
+
both sums (or surfaced as a separate unknown bucket), never folded in as `Cached ~0`:
|
|
103
123
|
`aggregate cache% = round(sum(Cached Tokens) / sum(Fresh Tokens + Cached Tokens) * 100)`.
|
|
104
124
|
|
|
105
125
|
The **trend across runs** is the signal, not the absolute value: rising cache% = the testee is
|
|
@@ -132,8 +152,10 @@ step stays on the driver's own row.
|
|
|
132
152
|
- Observable chained usage (subagent output in driver context, or the operator explicitly provided
|
|
133
153
|
the artifact) → estimate Fresh/Cached from that output normally.
|
|
134
154
|
- Unobservable chained usage (subagent ran in a different session, usage data never surfaced) →
|
|
135
|
-
label Fresh `~unknown`, Cached `~
|
|
136
|
-
|
|
155
|
+
label Fresh `~unknown`, Cached `~unknown`, Basis `chained-leg usage not observable from driver`,
|
|
156
|
+
and **exclude the row from the aggregate cache%** (or surface it as a separate unknown bucket) —
|
|
157
|
+
unknown cache use is not an observed zero. **MUST** emit a P3 finding: `P3 — chained-step cost
|
|
158
|
+
not observable` (task 0278 R3). Do not invent totals.
|
|
137
159
|
|
|
138
160
|
Never fold a chained row into the driver's row; the whole point of dogfooding a pipeline-driving
|
|
139
161
|
testee is to see the testee's own cost separately from the driver's monitoring cost. See
|
|
@@ -142,8 +164,10 @@ testee is to see the testee's own cost separately from the driver's monitoring c
|
|
|
142
164
|
## Anti-fiction rule
|
|
143
165
|
|
|
144
166
|
Never reuse a convenient cache percentage such as `45%` because it "feels right." A cache percentage
|
|
145
|
-
is valid only when it can be recomputed from the ledger row sums. If the basis is
|
|
146
|
-
|
|
167
|
+
is valid only when it can be recomputed from the ledger row sums of observable rows. If the basis is
|
|
168
|
+
missing, mark the row `~unknown`, exclude it from the aggregate (or surface it as a separate unknown
|
|
169
|
+
bucket), and explain the missing basis — never fold it in as `Cached Tokens = ~0`: unknown cache use
|
|
170
|
+
is not an observed zero-percent hit rate, and a low-cache diagnosis needs observed data.
|
|
147
171
|
|
|
148
172
|
## Cache-health finding rule
|
|
149
173
|
|
|
@@ -27,7 +27,7 @@ Every dogfood run **always** writes **two** files — with or without `--save`:
|
|
|
27
27
|
| Artifact | Path | Role |
|
|
28
28
|
| ---------- | ------ | ------ |
|
|
29
29
|
| **Live** | `.spur/run/dogfood/<run_id>.md` | Mid-run SSOT; opened in Phase 1; ledger rows appended on every step resolve |
|
|
30
|
-
| **Report** | `docs/dogfood/YYYY-MM-DD-<testee-slug>-dogfood.md` | Operator artifact; same content promoted on open + every step + finalize |
|
|
30
|
+
| **Report** | `docs/dogfood/YYYY-MM-DD-<testee-slug>-dogfood.md` | Operator artifact; same content promoted on open + every step + finalize — **except inside a pipeline proof window (task 0804 R2): the mirror stays frozen (live ledger only) until the window closes, then sync/validate, recovering from live if the write failed** — [monitor-ledger.md](monitor-ledger.md) → live-ledger rule 3 |
|
|
31
31
|
|
|
32
32
|
`--save` is **back-compat no-op** for delivery: it still documents/prints the report path but is
|
|
33
33
|
**not required** to create the file. A run that ends with no file under `docs/dogfood/` (and no live
|
|
@@ -158,14 +158,17 @@ it is the audit trail for step outcomes, fix attempts, findings, and cache math.
|
|
|
158
158
|
|------|----------|---------|-------------|---------|--------------|---------------|---------|-------|------------|
|
|
159
159
|
| resolve | 1 | PASS | — | — | ~800 | ~300 | 27% | 1 command + reused task summary | ~3s |
|
|
160
160
|
|
|
161
|
-
**Cache calculation:** aggregate cache% = round((sum(Cached Tokens) / sum(Fresh Tokens + Cached Tokens)) * 100)
|
|
161
|
+
**Cache calculation:** aggregate cache% = round((sum(Cached Tokens) / sum(Fresh Tokens + Cached Tokens)) * 100),
|
|
162
|
+
computed over **observable rows only** — `~unknown` rows are excluded from both sums (or surfaced as
|
|
163
|
+
a separate unknown bucket), never counted as `~0` cached.
|
|
162
164
|
```
|
|
163
165
|
|
|
164
166
|
Ledger rules:
|
|
165
167
|
|
|
166
168
|
- Every executed step gets exactly one row, recorded when the step resolves (**on disk**, both files).
|
|
167
|
-
- `Fresh Tokens` and `Cached Tokens` must be numbers
|
|
168
|
-
from
|
|
169
|
+
- `Fresh Tokens` and `Cached Tokens` must be `~`-prefixed numbers, or `~unknown` when the basis is
|
|
170
|
+
unobservable (that row is then excluded from the aggregate, never counted as `~0` cached);
|
|
171
|
+
`Cache %` must be computed from those two cells, not guessed.
|
|
169
172
|
- `Basis` is mandatory. It names the observable inputs used for the estimate: command output,
|
|
170
173
|
previously-read file reused from context, generated report text, or similar.
|
|
171
174
|
- The aggregate cache line in `#### Cost` under §2 must equal the ledger formula above. If it
|
|
@@ -173,8 +176,9 @@ Ledger rules:
|
|
|
173
176
|
- **Cardinality (@1.2):** the number of ledger data rows MUST equal the `**Steps:** N derived, N executed`
|
|
174
177
|
declared in §2. Steps marked N/A are documented explicitly as their own rows (`Outcome: N/A`);
|
|
175
178
|
an unaccounted step or an extra row refuses `status: complete` at finalize.
|
|
176
|
-
- If the driver cannot make a defensible estimate for a row, write `~
|
|
177
|
-
|
|
179
|
+
- If the driver cannot make a defensible estimate for a row, write `~unknown`, exclude it from the
|
|
180
|
+
aggregate cache% (or surface it as a separate unknown bucket), and explain the missing basis in
|
|
181
|
+
`Basis`; do not fold it in as `~0` cached or invent a stable percentage.
|
|
178
182
|
|
|
179
183
|
### 4. What We Did
|
|
180
184
|
|
|
@@ -209,6 +209,39 @@ executor is the current coding agent. Interactive pipelines retain a run log and
|
|
|
209
209
|
through the inline driver; task pipelines additionally record a task run-link. If process isolation or an independently killable
|
|
210
210
|
stage is required, select the subprocess path (`--agent auto` or `--agent <name>`).
|
|
211
211
|
|
|
212
|
+
## Shared startup contract (task 0814 R1/R3/R4/R6/R7/R8)
|
|
213
|
+
|
|
214
|
+
The workflow-backed dev commands (`dev-run`, `dev-runall`, `dev-refineall`, `dev-verifyall`) share one
|
|
215
|
+
startup order. The order is load-bearing and applies on both the inline driver and the subprocess
|
|
216
|
+
path; skill-only operations (refine/verify batches with no nested workflow) display their owned
|
|
217
|
+
procedure and do not fabricate a workflow YAML.
|
|
218
|
+
|
|
219
|
+
1. **Publish a compact bootstrap checklist immediately** (host-preparation rows, never copied
|
|
220
|
+
workflow states): `A, Quick readiness` · `B, Prepare Git` · `C, Publish workflow plan` ·
|
|
221
|
+
`D, Comprehensive checking`.
|
|
222
|
+
2. **Quick deterministic readiness (R2), before isolation.** Evaluate `quickReadiness`
|
|
223
|
+
(`plugins/sp/scripts/batch-preflight.ts`) with the operation, status, filtered-set size, and the
|
|
224
|
+
selected matrix required/present sections + content-policy findings. This is an admission decision
|
|
225
|
+
(runnable / needs-refinement / blocked / skipped / invalid), never an implementation certificate.
|
|
226
|
+
3. **Isolation (R3), only when `--worktree` is valid.** After quick readiness and the required Git
|
|
227
|
+
safety checks, create/adopt and switch to the execution tree; confirm absolute cwd, branch, base
|
|
228
|
+
SHA, and ownership. An invalid/empty target, unsupported mode, ambiguous ownership, or stale target
|
|
229
|
+
stops without creating a tree or discarding work. All subsequent tools, agents, corpus writes, and
|
|
230
|
+
run artifacts use the confirmed execution tree.
|
|
231
|
+
4. **Publish the workflow inventory (R4), before reading the YAML.** `spur workflow show
|
|
232
|
+
<resolved-file> --no-logo --format todo --json`; validate with `parseWorkflowInventory` and bind to
|
|
233
|
+
the run's `__definitionDigest` with `assertInventoryIdentity`. Drift or projection failure stops the
|
|
234
|
+
run before any comprehensive/model work — never execute with a misleading plan.
|
|
235
|
+
5. **Load execution detail and run comprehensive checks (R7).** Only after the plan is visible (and
|
|
236
|
+
after isolation when requested) load the full YAML for the active stage and run the owning
|
|
237
|
+
comprehensive gates at their boundaries. Prefer deterministic checks; invoke semantic model work
|
|
238
|
+
only for an identified unresolved requirement/design/evidence question and record its reason.
|
|
239
|
+
|
|
240
|
+
Quick readiness and plan projection dispatch zero models and execute zero workflow actions. Record a
|
|
241
|
+
timestamped event trace under `.spur/run/<run-id>-event-trace.md` (R8) — event ordering,
|
|
242
|
+
time-to-first-visible-checklist, time-to-workflow-inventory, confirmed cwd, invocation counts — and
|
|
243
|
+
record unavailable measurements as `unknown`, never as invented savings.
|
|
244
|
+
|
|
212
245
|
## Every write is CLI-gated
|
|
213
246
|
|
|
214
247
|
Never edit a task or feature file directly. Every mutation goes through:
|
|
@@ -446,6 +446,23 @@ isolated git worktree instead of the operator's working directory. This section
|
|
|
446
446
|
lifecycle for the sequential batch loop. Per-task worktrees and `--mode parallel` isolation stay out
|
|
447
447
|
of scope (task 0142 Slice A); `--worktree --mode parallel` is rejected.
|
|
448
448
|
|
|
449
|
+
**Startup ordering (task 0814 R3).** Resolve the selector/status filter and run the quick
|
|
450
|
+
command-aware readiness (the `quickReadiness` contract in `batch-preflight.ts`) **before** creating
|
|
451
|
+
or adopting the tree. The admission decision is what determines whether a tree should be cut at all;
|
|
452
|
+
all subsequent tools, agents, task/feature writes, and run artifacts use the confirmed execution
|
|
453
|
+
tree's cwd. A stale or empty selector, an unsupported mode, or an invalid target creates no tree and
|
|
454
|
+
no marker (WT-2/WT-7), and the required Git safety checks (WT-1) still precede creation.
|
|
455
|
+
|
|
456
|
+
> **Command wiring (task 0814 R3).** The four worktree-capable commands (`dev-run`, `dev-runall`,
|
|
457
|
+
> `dev-refineall`, `dev-verifyall`) each call `quickReadiness` with their operation (`run`/`refine`/
|
|
458
|
+
> `verify`), the resolved selector/status, and the filtered-set size **before** WT-1/WT-2. The
|
|
459
|
+
> admission outcome gates the tree: an invalid/empty selector, unsupported mode, or a target that
|
|
460
|
+
> quickReadiness marks `blocked`/`invalid` creates no tree and no marker (WT-2/WT-7); a
|
|
461
|
+
> `needs-refinement` refine batch is still work to do (the tree is created, the gaps are the work).
|
|
462
|
+
> The required Git safety checks (WT-1) still precede creation, and ownership/identity is confirmed
|
|
463
|
+
> before any tool, agent, corpus write, or run artifact. A later failure retains the tree with
|
|
464
|
+
> recovery information (WT-5).
|
|
465
|
+
|
|
449
466
|
**Single-task `dev-run` (batch of one).** `/sp:dev-run <wbs> --worktree [<name>]` runs this same
|
|
450
467
|
lifecycle with a one-task loop: WT-1…WT-6 apply unchanged, the marker's `command` is `dev-run` and
|
|
451
468
|
its `selector` is the `<wbs>` (so WT-6's command+selector fallback resolves the resume), and the
|