shapeup-sdlc 3.1.2 → 3.2.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/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -1
- package/commands/ship.md +5 -0
- package/hooks/gate-intake.mjs +1 -1
- package/kernel/init/run.mjs +122 -6
- package/kernel/lib/breadboard.mjs +165 -0
- package/kernel/lib/paths.mjs +3 -1
- package/kernel/probe/resume.mjs +7 -1
- package/kernel/verify/spec.mjs +190 -2
- package/package.json +1 -1
- package/skills/ba-pitch-analyzer/SKILL.md +11 -5
- package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +2 -1
- package/skills/ba-pitch-analyzer/assets/templates/ux-behavior.tmpl.md +12 -2
- package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -0
- package/skills/ba-pitch-analyzer/references/ux-behavior-patterns.md +9 -0
- package/skills/orient/SKILL.md +7 -4
- package/skills/scope-architect/SKILL.md +4 -0
- package/skills/solution-architect/SKILL.md +4 -1
- package/skills/tech-lead/SKILL.md +6 -6
- package/skills/tech-lead/references/gates.md +26 -12
- package/skills/tech-lead/references/protocol.md +8 -7
- package/skills/tech-lead/schemas/domain.schema.json +28 -2
- package/skills/tech-lead/workflows/shapeup-run.js +13 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shapeup-sdlc-plugin",
|
|
3
3
|
"displayName": "ShapeUp SDLC Plugin",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"description": "Shape Up SDLC harness for Claude Code: shaping, intake, orient, scope-mapping, building (T0-verified, sandboxed, scope-contracted), evaluation and QA skills orchestrated by a tech-lead.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Liberty Nguyen",
|
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,7 @@ Skills and commands are named short throughout this file; every one of them reso
|
|
|
18
18
|
1. Set Boundaries → `/shapeup shaping`
|
|
19
19
|
2. Find the Elements → `/shapeup breadboarding`
|
|
20
20
|
3. Risks & Rabbit Holes → `/shapeup spike`
|
|
21
|
-
(The completed pitch is formed by `shaping.md` + `breadboard.md`)
|
|
21
|
+
(The completed pitch is formed by `shaping.md` + `breadboard.md`. A run takes both: `/ship` finds the breadboard beside the pitch or in `shaping/`, takes one named with `--breadboard`, or reads it inline in a single pitch file, and hands it to every planning worker. Each breadboard Place with UI affordances becomes its own screen in the spec — spec-lint stops the run at L1b when one is missing or folded into another screen — and a Place the shape will not build is deferred there, with the PO's yes.)
|
|
22
22
|
|
|
23
23
|
### Phase 2 — Betting (PO governance, no skill)
|
|
24
24
|
Betting Table: PO decides; rejected pitches loop back to raw idea.
|
package/commands/ship.md
CHANGED
|
@@ -71,6 +71,11 @@ Only run headless/auto if the user explicitly asks for it in their message:
|
|
|
71
71
|
tiny, it will say so and recommend the full lane.
|
|
72
72
|
|
|
73
73
|
Additional flags, pass through to `tech-lead` only when the user names them:
|
|
74
|
+
- `--breadboard <path>` → the pitch's breadboard, when it is not beside the pitch. A pitch shaped
|
|
75
|
+
with `/shapeup` is `shaping.md` + `breadboard.md`; the run finds a `breadboard.md` in the same
|
|
76
|
+
folder (or in `shaping/`) on its own, and reads one written inline in a single `pitch.md`. Name
|
|
77
|
+
it only when it lives somewhere else. Its Places become the spec's screens, and spec-lint stops
|
|
78
|
+
the run at GATE L1b when one is missing or folded into another screen.
|
|
74
79
|
- `--gate-answers <ci|guarded|interactive|path.json>` → the pre-recorded PO decisions this run
|
|
75
80
|
crosses its gates with. Gates still emit their blocks and still record a decision; the
|
|
76
81
|
decision's **source** becomes the answer set instead of a live human, and the ledger says so.
|
package/hooks/gate-intake.mjs
CHANGED
|
@@ -73,7 +73,7 @@ const hasResume = /--from\s+\S/.test(args); // resuming an existing run has its
|
|
|
73
73
|
// the spec.
|
|
74
74
|
// Adding a valued flag anywhere in the harness means adding it here, and structural test §39
|
|
75
75
|
// enforces exactly that against commands/ship.md.
|
|
76
|
-
const VALUED_FLAGS = /--(pitch|spec|from|lens|rounds|attempts|parallel-scopes|orch-model|exec-model|eval-model|qa-model|feature|task|gate-answers|wall-clock-budget|slug|auto-level|max-rounds|intake-file|intake-text|spec-folder|cwd|out|by|preset|file|order)\s+\S+/g;
|
|
76
|
+
const VALUED_FLAGS = /--(pitch|spec|from|lens|rounds|attempts|parallel-scopes|orch-model|exec-model|eval-model|qa-model|feature|task|gate-answers|wall-clock-budget|slug|auto-level|max-rounds|intake-file|intake-text|spec-folder|cwd|out|by|preset|file|order|breadboard)\s+\S+/g;
|
|
77
77
|
const BARE_FLAGS = /--[a-z0-9-]+/g;
|
|
78
78
|
const freeText = args.replace(VALUED_FLAGS, " ").replace(BARE_FLAGS, " ").trim();
|
|
79
79
|
|
package/kernel/init/run.mjs
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
// --spec-folder SHARED spec deliverable path (default: shapeup/<slug>/spec/)
|
|
55
55
|
// --dimensions comma-separated eval dimensions (default: spec-conformance)
|
|
56
56
|
// --gate-answers path | preset name (see `harness gate`; recorded, not read)
|
|
57
|
+
// --breadboard path to the pitch's breadboard (default: found — see resolveBreadboard())
|
|
57
58
|
// --wall-clock-budget N deadline breaker, seconds (off by default; see `harness verify budget`)
|
|
58
59
|
// --cwd project root (default: process.cwd())
|
|
59
60
|
// --force re-init over an existing run receipt
|
|
@@ -68,8 +69,8 @@
|
|
|
68
69
|
// that takes a phase, not an init-run flag that takes a slug: the one instruction available at the
|
|
69
70
|
// one moment it mattered named a mechanism that does not parse.
|
|
70
71
|
|
|
71
|
-
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, copyFileSync, rmSync } from "node:fs";
|
|
72
|
-
import { join, dirname, resolve } from "node:path";
|
|
72
|
+
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, copyFileSync, rmSync, statSync } from "node:fs";
|
|
73
|
+
import { join, dirname, resolve, relative, sep } from "node:path";
|
|
73
74
|
import { createHash } from "node:crypto";
|
|
74
75
|
import { decideLane, treeSize } from "./fit.mjs";
|
|
75
76
|
import { runArgs } from "../lib/argv.mjs";
|
|
@@ -78,8 +79,9 @@ import { deriveSnapshot } from "../reduce/snapshot.mjs";
|
|
|
78
79
|
import { mintRunId } from "../lib/paths.mjs";
|
|
79
80
|
import {
|
|
80
81
|
localRoot, activeScope, activeOrder, globLocal, globShared, ordersDir, resultsDir,
|
|
81
|
-
workflowsStage, globWorkflowsStage,
|
|
82
|
+
workflowsStage, globWorkflowsStage, sharedRoot, shapingDir, breadboard as stagedBreadboard,
|
|
82
83
|
} from "../lib/paths.mjs";
|
|
84
|
+
import { parseBreadboard, hasBreadboardTables, idCounts } from "../lib/breadboard.mjs";
|
|
83
85
|
import { resolveWorkers } from "../verify/skills.mjs";
|
|
84
86
|
|
|
85
87
|
export const RECEIPT_VERSION = 1;
|
|
@@ -132,8 +134,19 @@ export function digest(text) {
|
|
|
132
134
|
/**
|
|
133
135
|
* Build the receipt record. Pure — takes resolved inputs, returns the object that gets written.
|
|
134
136
|
* Kept separate from I/O so the structural tests can assert its shape without a filesystem.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} o - Resolved inputs (destructured).
|
|
139
|
+
* @param {string} o.slug - Feature slug.
|
|
140
|
+
* @param {string} o.intake - The intake text, verbatim.
|
|
141
|
+
* @param {object} o.config - The pinned run config.
|
|
142
|
+
* @param {string} o.startedAt - ISO start time.
|
|
143
|
+
* @param {(object|null)} [o.plugin] - The plugin copy that answered.
|
|
144
|
+
* @param {(string|null)} [o.intakeSource] - Repo-relative `--intake-file` path, or "text" / "stdin".
|
|
145
|
+
* @param {({source: string, path: (string|null), text: string, translated?: boolean}|null)} [o.breadboard] -
|
|
146
|
+
* The resolved breadboard, `path` repo-relative (null when embedded in the intake), or null.
|
|
147
|
+
* @returns {object} The receipt.
|
|
135
148
|
*/
|
|
136
|
-
export function buildReceipt({ slug, intake, config, startedAt, plugin = null }) {
|
|
149
|
+
export function buildReceipt({ slug, intake, config, startedAt, plugin = null, intakeSource = null, breadboard = null }) {
|
|
137
150
|
const intakeText = String(intake ?? "");
|
|
138
151
|
const intakeSha256 = digest(intakeText);
|
|
139
152
|
return {
|
|
@@ -156,6 +169,23 @@ export function buildReceipt({ slug, intake, config, startedAt, plugin = null })
|
|
|
156
169
|
intake_sha256: intakeSha256,
|
|
157
170
|
intake_chars: intakeText.length,
|
|
158
171
|
intake_lines: intakeText ? intakeText.split("\n").length : 0,
|
|
172
|
+
// WHERE THE INTAKE CAME FROM. The copy above is all the run reads, so without its origin nothing
|
|
173
|
+
// on disk says which file the pitch was — or that it had a second half beside it.
|
|
174
|
+
intake_source: intakeSource,
|
|
175
|
+
// THE PITCH'S OTHER HALF. A `/shapeup` pitch is shaping.md + breadboard.md, and a Place that only
|
|
176
|
+
// the breadboard names reaches no planning worker unless the run carries it. Recorded here, at
|
|
177
|
+
// t=0, rather than left to a worker to note: the kernel knows whether a breadboard exists before
|
|
178
|
+
// anything is dispatched, and a worker's prose rule for exactly this case did not hold.
|
|
179
|
+
breadboard: breadboard
|
|
180
|
+
? {
|
|
181
|
+
source: breadboard.source,
|
|
182
|
+
path: breadboard.path ?? null,
|
|
183
|
+
sha256: digest(breadboard.text),
|
|
184
|
+
chars: String(breadboard.text ?? "").length,
|
|
185
|
+
ids: idCounts(parseBreadboard(breadboard.text)),
|
|
186
|
+
...(typeof breadboard.translated === "boolean" ? { translated: breadboard.translated } : {}),
|
|
187
|
+
}
|
|
188
|
+
: null,
|
|
159
189
|
// The single fact that separates "the harness ran" from "the harness described itself".
|
|
160
190
|
// Written before any gate, so its ABSENCE at Stop is unambiguous.
|
|
161
191
|
started: true,
|
|
@@ -312,6 +342,70 @@ export function stageWorkflows(cwd, pluginRoot, { refresh = true } = {}) {
|
|
|
312
342
|
return { ok: true, dir, staged: names };
|
|
313
343
|
}
|
|
314
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Find the breadboard that belongs to this run's pitch. First hit wins:
|
|
347
|
+
*
|
|
348
|
+
* 1. `flag` — `--breadboard <path>`, resolved against cwd; a missing file is an error.
|
|
349
|
+
* 2. `sibling` — `breadboard.md` in the folder of `--intake-file` (not for text or stdin).
|
|
350
|
+
* 3. `shaping-dir` — `shapeup/<slug>/shaping/breadboard.md`, the documented location.
|
|
351
|
+
* 4. `shared-root` — `shapeup/<slug>/breadboard.md`, the flat layout real projects use.
|
|
352
|
+
* 5. `embedded` — the intake itself, when it carries Places and UI tables; nothing is staged.
|
|
353
|
+
*
|
|
354
|
+
* THE INTAKE'S OWN FOLDER COMES FIRST, not the documented location, because the only consumer that
|
|
355
|
+
* hit this keeps its pitch flat in `shapeup/<slug>/`: a lookup that reads only the shaping folder
|
|
356
|
+
* passes every fixture built to the documented layout and misses the real one.
|
|
357
|
+
*
|
|
358
|
+
* A discovered candidate that is the intake file itself, or the run's own staged copy, is skipped:
|
|
359
|
+
* the staged copy is this function's OUTPUT, and re-opening a run from its staged intake must not
|
|
360
|
+
* inherit the breadboard of the run it replaces.
|
|
361
|
+
*
|
|
362
|
+
* A TRANSLATED PITCH GETS ITS TRANSLATED BREADBOARD. When the intake is a translator output
|
|
363
|
+
* (`<name>.en.md`), each folder is tried for `breadboard.en.md` before `breadboard.md`, and the
|
|
364
|
+
* result says whether it is translated — an English spec planned from a source-language breadboard
|
|
365
|
+
* is a mismatch the caller reports, not one it may silently stage.
|
|
366
|
+
*
|
|
367
|
+
* @param {object} o - Inputs (destructured).
|
|
368
|
+
* @param {string} o.cwd - Project root.
|
|
369
|
+
* @param {string} o.slug - Feature slug.
|
|
370
|
+
* @param {(string|null)} [o.intakeFile] - The `--intake-file` value as given ("-" = stdin).
|
|
371
|
+
* @param {(string|null)} [o.flag] - The `--breadboard` value as given.
|
|
372
|
+
* @param {string} [o.intake] - The intake text, for the embedded case.
|
|
373
|
+
* @returns {({source: string, path: (string|null), text: string, translated?: boolean}|null)} The
|
|
374
|
+
* breadboard, its absolute path (null when embedded), and its text — plus `translated` when the
|
|
375
|
+
* intake is a `.en.md` translation; or null when the pitch has none.
|
|
376
|
+
* @throws {Error} If `--breadboard` names a file that does not exist.
|
|
377
|
+
*/
|
|
378
|
+
export function resolveBreadboard({ cwd, slug, intakeFile = null, flag = null, intake = "" }) {
|
|
379
|
+
const self = intakeFile && intakeFile !== "-" ? resolve(cwd, intakeFile) : null;
|
|
380
|
+
const english = Boolean(self && self.endsWith(".en.md"));
|
|
381
|
+
/** Tag a hit with whether it is translated — only meaningful when the intake is. */
|
|
382
|
+
const hit = (source, p, text) => ({ source, path: p, text, ...(english ? { translated: p === null || p.endsWith(".en.md") } : {}) });
|
|
383
|
+
if (flag) {
|
|
384
|
+
const p = resolve(cwd, flag);
|
|
385
|
+
if (!existsSync(p) || !statSync(p).isFile()) throw new Error(`--breadboard not found: ${p}`);
|
|
386
|
+
return hit("flag", p, readFileSync(p, "utf8"));
|
|
387
|
+
}
|
|
388
|
+
const staged = stagedBreadboard(cwd, slug);
|
|
389
|
+
const names = english ? ["breadboard.en.md", "breadboard.md"] : ["breadboard.md"];
|
|
390
|
+
const folders = [
|
|
391
|
+
...(self ? [["sibling", dirname(self)]] : []),
|
|
392
|
+
["shaping-dir", shapingDir(cwd, slug)],
|
|
393
|
+
["shared-root", sharedRoot(cwd, slug)],
|
|
394
|
+
];
|
|
395
|
+
for (const [source, dir] of folders) {
|
|
396
|
+
for (const n of names) {
|
|
397
|
+
const p = join(dir, n);
|
|
398
|
+
if (p === self || p === staged) continue;
|
|
399
|
+
if (existsSync(p) && statSync(p).isFile()) return hit(source, p, readFileSync(p, "utf8"));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (hasBreadboardTables(intake)) return hit("embedded", null, String(intake));
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** A path relative to the project root, `/`-joined on every platform — the form a receipt records. */
|
|
407
|
+
const repoRel = (cwd, p) => relative(cwd, p).split(sep).join("/");
|
|
408
|
+
|
|
315
409
|
// ---- CLI -------------------------------------------------------------------
|
|
316
410
|
|
|
317
411
|
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
@@ -319,7 +413,7 @@ export const ARGV_SPEC = {
|
|
|
319
413
|
usage: 'harness.mjs init run (--intake-file <path> | --intake-text "<req>" | --intake-stdin) ' +
|
|
320
414
|
"[--slug <slug>] [--auto-level interactive|auto|unattended] [--lens <lens>] " +
|
|
321
415
|
"[--max-rounds N] [--attempts N] [--spec-folder <dir>] [--dimensions <a,b>] " +
|
|
322
|
-
"[--gate-answers <preset|path>] " +
|
|
416
|
+
"[--gate-answers <preset|path>] [--breadboard <path>] " +
|
|
323
417
|
"[--lane full|tiny] [--tiny] [--wall-clock-budget <seconds>] [--cwd <dir>] " +
|
|
324
418
|
"[--plugin-root <dir>] [--force]",
|
|
325
419
|
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
@@ -335,6 +429,7 @@ export const ARGV_SPEC = {
|
|
|
335
429
|
"spec-folder": { type: "path" },
|
|
336
430
|
dimensions: { type: "str" },
|
|
337
431
|
"gate-answers": { type: "str" },
|
|
432
|
+
breadboard: { type: "path" },
|
|
338
433
|
lane: { type: "str" },
|
|
339
434
|
tiny: { type: "flag" },
|
|
340
435
|
"wall-clock-budget": { type: "int", min: 1 },
|
|
@@ -423,6 +518,16 @@ export function cli(rawArgv) {
|
|
|
423
518
|
let eval_dimensions;
|
|
424
519
|
try { eval_dimensions = parseDimensions(args.dimensions ?? null); }
|
|
425
520
|
catch (e) { fail(2, e.message); }
|
|
521
|
+
// The pitch's second half, resolved before anything is written so a bad `--breadboard` is a usage
|
|
522
|
+
// error that ran nothing. Read-only here; staged below, once the run is actually opened.
|
|
523
|
+
let bb = null;
|
|
524
|
+
try { bb = resolveBreadboard({ cwd, slug, intakeFile, flag: args.breadboard ?? null, intake }); }
|
|
525
|
+
catch (e) { fail(2, e.message); }
|
|
526
|
+
if (bb?.translated === false) {
|
|
527
|
+
console.error(`⚠ init-run: the intake is a translation but its breadboard is not — staging ${repoRel(cwd, bb.path)} as found.`);
|
|
528
|
+
console.error(" Translate the breadboard too (translator), and name the .en.md with --breadboard, or the spec is planned from two languages.");
|
|
529
|
+
}
|
|
530
|
+
const intakeSource = args.intakeStdin || intakeFile === "-" ? "stdin" : intakeFile ? repoRel(cwd, resolve(cwd, intakeFile)) : "text";
|
|
426
531
|
|
|
427
532
|
const config = {
|
|
428
533
|
auto_level,
|
|
@@ -532,7 +637,10 @@ export function cli(rawArgv) {
|
|
|
532
637
|
}
|
|
533
638
|
|
|
534
639
|
const startedAt = new Date().toISOString();
|
|
535
|
-
const receipt = buildReceipt({
|
|
640
|
+
const receipt = buildReceipt({
|
|
641
|
+
slug, intake, config, startedAt, plugin, intakeSource,
|
|
642
|
+
breadboard: bb ? { ...bb, path: bb.path ? repoRel(cwd, bb.path) : null } : null,
|
|
643
|
+
});
|
|
536
644
|
|
|
537
645
|
mkdirSync(runRoot, { recursive: true });
|
|
538
646
|
mkdirSync(join(runRoot, "orders"), { recursive: true });
|
|
@@ -541,6 +649,10 @@ export function cli(rawArgv) {
|
|
|
541
649
|
|
|
542
650
|
// The intake, verbatim. So "the spec was dropped on the hand-off" is a checkable claim.
|
|
543
651
|
writeFileSync(join(runRoot, "intake.md"), intake.endsWith("\n") ? intake : intake + "\n", "utf8");
|
|
652
|
+
// The breadboard, verbatim — byte for byte, so the receipt's digest is the file's. A copy left by a
|
|
653
|
+
// run this one replaces goes first: a re-open without a breadboard must not inherit the last one.
|
|
654
|
+
rmSync(stagedBreadboard(cwd, slug), { force: true });
|
|
655
|
+
if (bb?.path) writeFileSync(stagedBreadboard(cwd, slug), bb.text, "utf8");
|
|
544
656
|
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + "\n", "utf8");
|
|
545
657
|
writeFileSync(join(runRoot, "harness-run.md"), runFrontmatter({ slug, config, startedAt }), "utf8");
|
|
546
658
|
|
|
@@ -558,6 +670,10 @@ export function cli(rawArgv) {
|
|
|
558
670
|
receipt: globLocal(slug, "receipt.json"),
|
|
559
671
|
intake_sha256: receipt.intake_sha256,
|
|
560
672
|
intake_chars: receipt.intake_chars,
|
|
673
|
+
// The staged breadboard the planning dispatches are handed, or null when the pitch has none
|
|
674
|
+
// separate (`breadboard_source` then says "embedded" or null).
|
|
675
|
+
breadboard: bb?.path ? globLocal(slug, "breadboard.md") : null,
|
|
676
|
+
breadboard_source: bb?.source ?? null,
|
|
561
677
|
config,
|
|
562
678
|
// What the launch names. Project-local by necessity, not by preference — see stageWorkflows().
|
|
563
679
|
workflow_script: staged.ok ? globWorkflowsStage("shapeup-run.js") : null,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// breadboard — read a `/shapeup` breadboard's element ids out of its markdown tables.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. A pitch is two files: `shaping.md` (problem, requirements, parts) and
|
|
4
|
+
// `breadboard.md` (Places, affordances, slices). The run used to take only the first, so a Place
|
|
5
|
+
// that existed only in the breadboard — a new sheet, a new modal — reached no planning worker, and
|
|
6
|
+
// the spec folded it into whichever screen the prose happened to mention. The ids read here are what
|
|
7
|
+
// the run pins at open (the receipt's counts) and what spec-lint checks placement against.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IT READS, and nothing more. An element id is the FIRST cell of a markdown table row —
|
|
10
|
+
// `P1`, `P2.1`, `U3`, `N1b`, `S2`, `V4` — after stripping `**` and backticks, so both table layouts
|
|
11
|
+
// the breadboarding guide ships parse the same way: a `#` first column and an `ID` first column. A
|
|
12
|
+
// row's Places come from its `Place` column, every `P#` token in the cell (`P1 / P3`, `P2/P1`); a
|
|
13
|
+
// table with no Place column gives its rows no Places. Places may also be listed as prose under a
|
|
14
|
+
// `## Places` heading (`P1: Composer — …`), which is the guide's own output template.
|
|
15
|
+
//
|
|
16
|
+
// It skips fenced code blocks: a table inside a fence is an example of a breadboard, not this one.
|
|
17
|
+
//
|
|
18
|
+
// Tolerant by design. A layout this reader cannot parse yields zero ids, and every caller treats
|
|
19
|
+
// zero ids as "nothing to check" plus a warning — never as a hard stop. Zero dependencies.
|
|
20
|
+
|
|
21
|
+
/** A first-cell element id: a Place (`P1`, `P2.1`), or a UI/code affordance, store or slice. */
|
|
22
|
+
export const ID_PATTERN = /^(P\d+(?:\.\d+)*|[UNSV]\d+[a-z]?)$/;
|
|
23
|
+
|
|
24
|
+
/** Every Place reference inside a cell. Global — use with `match`, never `test`. */
|
|
25
|
+
const PLACE_REF = /\bP\d+(?:\.\d+)*\b/g;
|
|
26
|
+
|
|
27
|
+
/** A prose Place line under `## Places`: `P1: …`, `- P1 — …`, `**P2** – …`. */
|
|
28
|
+
const PROSE_PLACE = /^\s*(?:[-*+]\s+)?\**(P\d+(?:\.\d+)*)\**\s*[:—–-]\s*(.*)$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Split one markdown table row into trimmed cells.
|
|
32
|
+
* @param {string} line - A line that starts with `|`.
|
|
33
|
+
* @returns {string[]} The cells between the outer pipes.
|
|
34
|
+
*/
|
|
35
|
+
function cells(line) {
|
|
36
|
+
const t = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
37
|
+
return t.split("|").map((c) => c.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Strip the emphasis and code marks a breadboard author puts around an id.
|
|
42
|
+
* @param {string} cell - A raw table cell.
|
|
43
|
+
* @returns {string} The cell's text without `**` and backticks.
|
|
44
|
+
*/
|
|
45
|
+
function bare(cell) {
|
|
46
|
+
return String(cell ?? "").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Is this line a table's header separator (`|---|:--:|`)?
|
|
51
|
+
* @param {string} line - A table line.
|
|
52
|
+
* @returns {boolean} True for a separator row.
|
|
53
|
+
*/
|
|
54
|
+
function isSeparator(line) {
|
|
55
|
+
return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a breadboard's element ids.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} text - The breadboard markdown (or a pitch that carries one inline).
|
|
62
|
+
* @returns {{places: {id: string, name: string}[], ui: {id: string, places: string[]}[],
|
|
63
|
+
* code: {id: string, places: string[]}[], stores: {id: string, places: string[]}[],
|
|
64
|
+
* slices: {id: string}[]}} Every id found, de-duplicated by id; a repeated row merges its Places.
|
|
65
|
+
*/
|
|
66
|
+
export function parseBreadboard(text) {
|
|
67
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
68
|
+
const places = new Map();
|
|
69
|
+
const byKind = { U: new Map(), N: new Map(), S: new Map(), V: new Map() };
|
|
70
|
+
|
|
71
|
+
const addPlace = (id, name) => {
|
|
72
|
+
if (!places.has(id)) places.set(id, { id, name: name || "" });
|
|
73
|
+
else if (!places.get(id).name && name) places.get(id).name = name;
|
|
74
|
+
};
|
|
75
|
+
const addElement = (id, refs) => {
|
|
76
|
+
const m = byKind[id[0]];
|
|
77
|
+
if (!m.has(id)) m.set(id, new Set());
|
|
78
|
+
for (const p of refs) m.get(id).add(p);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let inFence = false;
|
|
82
|
+
let inPlacesSection = false;
|
|
83
|
+
let header = null; // the current table's header cells, lower-cased
|
|
84
|
+
let prevWasTable = false;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; prevWasTable = false; header = null; continue; }
|
|
89
|
+
if (inFence) continue;
|
|
90
|
+
|
|
91
|
+
const heading = line.match(/^\s*#{1,6}\s+(.*)$/);
|
|
92
|
+
if (heading) {
|
|
93
|
+
inPlacesSection = /^places\b/i.test(bare(heading[1]));
|
|
94
|
+
prevWasTable = false; header = null;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (/^\s*\|/.test(line)) {
|
|
99
|
+
if (!prevWasTable) {
|
|
100
|
+
// A table starts here. Its first line is the header when the next line is a separator.
|
|
101
|
+
header = isSeparator(lines[i + 1] ?? "") ? cells(line).map((c) => bare(c).toLowerCase()) : null;
|
|
102
|
+
prevWasTable = true;
|
|
103
|
+
if (header) continue;
|
|
104
|
+
}
|
|
105
|
+
if (isSeparator(line)) continue;
|
|
106
|
+
const row = cells(line);
|
|
107
|
+
const id = bare(row[0]);
|
|
108
|
+
if (!ID_PATTERN.test(id)) continue;
|
|
109
|
+
const placeCol = header ? header.indexOf("place") : -1;
|
|
110
|
+
if (id[0] === "P") {
|
|
111
|
+
const nameCol = placeCol > 0 ? placeCol : (header ? header.findIndex((h) => h === "name") : -1);
|
|
112
|
+
addPlace(id, bare(row[nameCol > 0 ? nameCol : 1] ?? ""));
|
|
113
|
+
} else {
|
|
114
|
+
const cell = placeCol > 0 ? bare(row[placeCol] ?? "") : "";
|
|
115
|
+
addElement(id, cell.match(PLACE_REF) ?? []);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
prevWasTable = false; header = null;
|
|
120
|
+
|
|
121
|
+
if (inPlacesSection) {
|
|
122
|
+
const m = line.match(PROSE_PLACE);
|
|
123
|
+
if (m) addPlace(m[1], bare(m[2]).split(/\s+[—–-]\s+/)[0]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const list = (m) => [...m].map(([id, refs]) => ({ id, places: [...refs] }));
|
|
128
|
+
return {
|
|
129
|
+
places: [...places.values()],
|
|
130
|
+
ui: list(byKind.U),
|
|
131
|
+
code: list(byKind.N),
|
|
132
|
+
stores: list(byKind.S),
|
|
133
|
+
slices: [...byKind.V.keys()].map((id) => ({ id })),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Does this text carry a breadboard of its own — at least one Place and one UI affordance?
|
|
139
|
+
*
|
|
140
|
+
* The test for a pitch that embeds its breadboard inline rather than as a second file. Both are
|
|
141
|
+
* required: a shaping doc routinely has tables, and a Place with nothing to place is not a
|
|
142
|
+
* breadboard anything downstream can check.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} text - Markdown to inspect.
|
|
145
|
+
* @returns {boolean} True when `parseBreadboard` finds a P# and a U#.
|
|
146
|
+
*/
|
|
147
|
+
export function hasBreadboardTables(text) {
|
|
148
|
+
const bb = parseBreadboard(text);
|
|
149
|
+
return bb.places.length > 0 && bb.ui.length > 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Count a parsed breadboard's ids by kind, the shape the run receipt records.
|
|
154
|
+
* @param {ReturnType<typeof parseBreadboard>} parsed - A `parseBreadboard` result.
|
|
155
|
+
* @returns {{P: number, U: number, N: number, S: number, V: number}} Ids per kind.
|
|
156
|
+
*/
|
|
157
|
+
export function idCounts(parsed) {
|
|
158
|
+
return {
|
|
159
|
+
P: parsed.places.length,
|
|
160
|
+
U: parsed.ui.length,
|
|
161
|
+
N: parsed.code.length,
|
|
162
|
+
S: parsed.stores.length,
|
|
163
|
+
V: parsed.slices.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
package/kernel/lib/paths.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export const localRoot = (cwd, slug) => join(cwd, LOCAL, slug);
|
|
|
91
91
|
export const specDir = (cwd, slug) => join(sharedRoot(cwd, slug), "spec");
|
|
92
92
|
/** Use-case directory inside the spec tree. */
|
|
93
93
|
export const usecasesDir = (cwd, slug) => join(specDir(cwd, slug), "usecases");
|
|
94
|
-
/** Shaping artifacts — pitch, framing, breadboard, baseline, glossary. */
|
|
94
|
+
/** Shaping artifacts — pitch, framing, breadboard, baseline, glossary. `init run` looks here for a breadboard. */
|
|
95
95
|
export const shapingDir = (cwd, slug) => join(sharedRoot(cwd, slug), "shaping");
|
|
96
96
|
// The three contracts are markdown on disk and JSON on the wire (ADR-0001) — see
|
|
97
97
|
// `lib/contract.mjs`. `readContract()` accepts either extension, so a project mid-migration
|
|
@@ -138,6 +138,8 @@ export const RECEIPT_FILE = "receipt.json";
|
|
|
138
138
|
export const receipt = (cwd, slug) => join(localRoot(cwd, slug), RECEIPT_FILE);
|
|
139
139
|
/** The intake, verbatim, next to its digest in the receipt. */
|
|
140
140
|
export const intake = (cwd, slug) => join(localRoot(cwd, slug), "intake.md");
|
|
141
|
+
/** The breadboard the pitch was shaped with, verbatim, next to its digest in the receipt. */
|
|
142
|
+
export const breadboard = (cwd, slug) => join(localRoot(cwd, slug), "breadboard.md");
|
|
141
143
|
/** The run ledger — rounds, decisions, status frontmatter. */
|
|
142
144
|
export const harnessRun = (cwd, slug) => join(localRoot(cwd, slug), "harness-run.md");
|
|
143
145
|
/** File-derived mid-run digest, frozen by `reduce snapshot --write` as an audit anchor. */
|
package/kernel/probe/resume.mjs
CHANGED
|
@@ -63,7 +63,7 @@ import { splitFrontmatter } from "../lib/contract.mjs";
|
|
|
63
63
|
import { globToRegExp } from "../verify/spec.mjs";
|
|
64
64
|
import {
|
|
65
65
|
intake, harnessRun, wiringMap, projectProfile, scopesDir, resultsDir, ordersDir,
|
|
66
|
-
orientDir, activeOrder, usecasesDir,
|
|
66
|
+
orientDir, activeOrder, usecasesDir, breadboard, receipt, readReceipt,
|
|
67
67
|
} from "../lib/paths.mjs";
|
|
68
68
|
import { evalVerdict } from "./eval.mjs";
|
|
69
69
|
|
|
@@ -378,6 +378,12 @@ export function deriveResumeState(cwd, slug) {
|
|
|
378
378
|
|
|
379
379
|
const facts = {
|
|
380
380
|
intake_path: intake(cwd, slug),
|
|
381
|
+
// The pitch's other half, staged by `init run` beside the intake. Null when the pitch had no
|
|
382
|
+
// separate breadboard — the planning dispatches then carry no `breadboard` key at all.
|
|
383
|
+
breadboard_path: existsSync(breadboard(cwd, slug)) ? breadboard(cwd, slug) : null,
|
|
384
|
+
// How it was found (flag | sibling | shaping-dir | shared-root | embedded), from the receipt;
|
|
385
|
+
// null when there was none or the receipt cannot be read.
|
|
386
|
+
breadboard_source: readReceipt(receipt(cwd, slug))?.breadboard?.source ?? null,
|
|
381
387
|
spec_folder: hr.spec_folder || null,
|
|
382
388
|
status: hr.status || null,
|
|
383
389
|
lens: hr.lens || null,
|
package/kernel/verify/spec.mjs
CHANGED
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
// Edge-cases heading with real content under it) but no usecases/UC-*.md declares
|
|
43
43
|
// a single [INV-NN] anywhere — a criteria-count check can't tell a healthy small
|
|
44
44
|
// tree from one that silently derived nothing from the pitch
|
|
45
|
+
// BREADBOARD-PLACE (red) a breadboard Place that owns UI affordances has no ux-behavior.md
|
|
46
|
+
// `## Screen: … (P#)` section and is not under `## Deferred Places`
|
|
47
|
+
// BREADBOARD-UI (red) a UI affordance (U#) not cited inside the screen section of any Place the
|
|
48
|
+
// breadboard puts it in. CITING IS NOT PLACING: a U# specified under another Place's
|
|
49
|
+
// screen passes a presence check and is exactly the defect this rule exists for
|
|
50
|
+
// BREADBOARD-TRACE (warn) N#/S# cited nowhere in the spec, V# slices no scope board records,
|
|
51
|
+
// U# the spec places that no manifest entry names as its `source`
|
|
52
|
+
// BREADBOARD-UNPARSED (warn) a staged breadboard this reader finds no ids in — a layout it
|
|
53
|
+
// cannot read must never become a hard stop
|
|
54
|
+
// All four are silent when the run has no breadboard (staged, or inline in the intake).
|
|
45
55
|
//
|
|
46
56
|
// Zero dependencies (glob matcher inlined from hooks/sandbox-guard.mjs). Judgment stays in the skill
|
|
47
57
|
// (gap severity, lens choice); this script only reports facts.
|
|
@@ -56,6 +66,8 @@ import { runArgs } from "../lib/argv.mjs";
|
|
|
56
66
|
import { LOCAL } from "../lib/paths.mjs";
|
|
57
67
|
import { specDir, scopesDir, tasksDir, intake, sharedRoot, requirements } from "../lib/paths.mjs";
|
|
58
68
|
import { readAllContracts, unreadableReason, ucId, scopePartitionConflicts, SCOPE_CONTRACT } from "../lib/contract.mjs";
|
|
69
|
+
import { breadboard as stagedBreadboard } from "../lib/paths.mjs";
|
|
70
|
+
import { parseBreadboard, hasBreadboardTables, idCounts } from "../lib/breadboard.mjs";
|
|
59
71
|
|
|
60
72
|
// Inlined from hooks/sandbox-guard.mjs so this skill ships self-contained (a skill's scripts
|
|
61
73
|
// must not reach outside its own folder — channels that copy only skills/ would dangle).
|
|
@@ -494,12 +506,169 @@ export function lintStructure({ specDir, tasks, intakeContent = "" }) {
|
|
|
494
506
|
return findings;
|
|
495
507
|
}
|
|
496
508
|
|
|
509
|
+
/** Every Place id inside a heading's parentheses — `## Screen: Sheet (P2)`, `(P1, P3)`. */
|
|
510
|
+
const PLACE_IN_PARENS = /\(([^)]*)\)/g;
|
|
511
|
+
const PLACE_ID = /\bP\d+(?:\.\d+)*\b/g;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Cut ux-behavior.md into the screen sections a breadboard's Places are checked against.
|
|
515
|
+
*
|
|
516
|
+
* A section runs from its `Screen:` heading to the next heading at the same level or higher, so
|
|
517
|
+
* a screen's `### States` table and behavior rules belong to it. Its Places are the P# ids in the
|
|
518
|
+
* heading's parentheses; a screen with none is kept (its citations are still "somewhere") but can
|
|
519
|
+
* place nothing.
|
|
520
|
+
*
|
|
521
|
+
* @param {string} uxText - ux-behavior.md, verbatim ("" when absent).
|
|
522
|
+
* @returns {{screens: {heading: string, places: string[], body: string}[], deferred: Set<string>}}
|
|
523
|
+
* The screen sections, and the Place ids listed first-cell under `## Deferred Places`.
|
|
524
|
+
*/
|
|
525
|
+
export function uxScreens(uxText) {
|
|
526
|
+
const lines = String(uxText ?? "").split(/\r?\n/);
|
|
527
|
+
const screens = [];
|
|
528
|
+
const deferred = new Set();
|
|
529
|
+
let cur = null; // { level, heading, places, body[] } for a screen, or { level, deferred: true }
|
|
530
|
+
for (const line of lines) {
|
|
531
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
532
|
+
if (h) {
|
|
533
|
+
const level = h[1].length;
|
|
534
|
+
if (cur && level <= cur.level) cur = null;
|
|
535
|
+
if (!cur) {
|
|
536
|
+
const text = h[2].trim();
|
|
537
|
+
if (/^screen\s*:/i.test(text)) {
|
|
538
|
+
const places = [...text.matchAll(PLACE_IN_PARENS)].flatMap((m) => m[1].match(PLACE_ID) ?? []);
|
|
539
|
+
cur = { level, heading: text, places, body: [] };
|
|
540
|
+
screens.push(cur);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (/^deferred places\b/i.test(text)) { cur = { level, deferred: true }; continue; }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!cur) continue;
|
|
547
|
+
if (cur.deferred) {
|
|
548
|
+
const row = line.match(/^\s*\|\s*([^|]*)\|/);
|
|
549
|
+
const id = row ? row[1].replace(/[*`\[\]]/g, "").match(/^\s*(P\d+(?:\.\d+)*)\b/) : null;
|
|
550
|
+
if (id) deferred.add(id[1]);
|
|
551
|
+
} else cur.body.push(line);
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
screens: screens.map((s) => ({ heading: s.heading, places: s.places, body: s.body.join("\n") })),
|
|
555
|
+
deferred,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Lint the spec against the pitch's breadboard: every Place with UI affordances has a screen, and
|
|
561
|
+
* every UI affordance is specified on a screen of a Place the breadboard puts it in.
|
|
562
|
+
*
|
|
563
|
+
* PLACEMENT, NOT CITATION. The loss this exists for did not drop a new sheet's affordances — they
|
|
564
|
+
* were all in the spec, inside the composer's state table. What was lost was the Place. A rule that
|
|
565
|
+
* only asks "is U2 cited?" passes that spec; this one asks "is U2 cited under P2?". It checks WHICH
|
|
566
|
+
* screen, never where on the screen: layout inside a Place stays the designer's.
|
|
567
|
+
*
|
|
568
|
+
* Absent breadboard ⇒ zero findings, the same "absent artifact ⇒ arm skipped" rule INV-FLOOR and
|
|
569
|
+
* SCOPE-COVERS follow — every pre-breadboard spec and every run without one is untouched.
|
|
570
|
+
*
|
|
571
|
+
* @param {object} input - What to lint (destructured).
|
|
572
|
+
* @param {string} input.uxText - ux-behavior.md, verbatim ("" when absent).
|
|
573
|
+
* @param {string} input.specText - Every markdown file under the spec tree, concatenated.
|
|
574
|
+
* @param {Array<object>} [input.scopes] - Parsed scope contracts ([] before MAP SCOPES).
|
|
575
|
+
* @param {(string|null)} [input.scopeSummaryText] - scope-summary.md, or null when absent.
|
|
576
|
+
* @param {(string|null)} [input.scopeBoardText] - scope-board.md, or null when absent — the scope
|
|
577
|
+
* architect's own write surface, where it records which scopes deliver each slice.
|
|
578
|
+
* @param {(string|null)} input.bbText - The breadboard, or null when the run has none.
|
|
579
|
+
* @returns {Array<{rule:string, level:("red"|"warn"), detail:string}>} Findings; [] when clean or
|
|
580
|
+
* when there is no breadboard.
|
|
581
|
+
*/
|
|
582
|
+
export function lintBreadboard({ uxText = "", specText = "", scopes = [], scopeSummaryText = null, scopeBoardText = null, bbText = null }) {
|
|
583
|
+
if (!bbText) return [];
|
|
584
|
+
const findings = [];
|
|
585
|
+
const bb = parseBreadboard(bbText);
|
|
586
|
+
const counts = idCounts(bb);
|
|
587
|
+
if (Object.values(counts).every((n) => n === 0)) {
|
|
588
|
+
findings.push({ rule: "BREADBOARD-UNPARSED", level: "warn", detail: "the run has a breadboard but no P#/U#/N#/S#/V# ids could be read from its tables — placement was not checked. A `#` or `ID` first column holding the id, and a `Place` column on affordance rows, is the layout this reads" });
|
|
589
|
+
return findings;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const { screens, deferred } = uxScreens(uxText);
|
|
593
|
+
/**
|
|
594
|
+
* A Place as a finding names it — id and breadboard name.
|
|
595
|
+
* @param {string} p - Place id.
|
|
596
|
+
* @returns {string} e.g. `P2 Payment Sheet`.
|
|
597
|
+
*/
|
|
598
|
+
const name = (p) => {
|
|
599
|
+
const n = bb.places.find((x) => x.id === p)?.name;
|
|
600
|
+
return n ? `${p} ${n}` : p;
|
|
601
|
+
};
|
|
602
|
+
/**
|
|
603
|
+
* Does the text cite this exact id — `U2` but not `U21` or `U2a`?
|
|
604
|
+
* @param {string} text - Markdown to search.
|
|
605
|
+
* @param {string} id - A breadboard id.
|
|
606
|
+
* @returns {boolean} True when the id appears as a whole word.
|
|
607
|
+
*/
|
|
608
|
+
const cites = (text, id) => new RegExp(`\\b${id.replace(/\./g, "\\.")}\\b`).test(text);
|
|
609
|
+
|
|
610
|
+
// BREADBOARD-PLACE — a Place with something to place needs a screen of its own.
|
|
611
|
+
const owners = new Map(); // Place → the U# it owns
|
|
612
|
+
for (const u of bb.ui) for (const p of u.places) (owners.get(p) ?? owners.set(p, []).get(p)).push(u.id);
|
|
613
|
+
for (const [p, us] of owners) {
|
|
614
|
+
if (deferred.has(p)) continue;
|
|
615
|
+
if (screens.some((s) => s.places.includes(p))) continue;
|
|
616
|
+
findings.push({ rule: "BREADBOARD-PLACE", level: "red", detail: `${name(p)} owns ${us.join(", ")} but ux-behavior.md has no "## Screen: … (${p})" section — add the screen or defer the Place under "## Deferred Places"; never fold it into another screen` });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// BREADBOARD-UI — each U# is specified on a screen of a Place the breadboard puts it in.
|
|
620
|
+
const unplaceable = [];
|
|
621
|
+
for (const u of bb.ui) {
|
|
622
|
+
if (!u.places.length) { unplaceable.push(u.id); continue; }
|
|
623
|
+
const live = u.places.filter((p) => !deferred.has(p));
|
|
624
|
+
if (!live.length) continue;
|
|
625
|
+
if (screens.some((s) => s.places.some((p) => live.includes(p)) && cites(s.body, u.id))) continue;
|
|
626
|
+
const elsewhere = [...new Set(screens.filter((s) => cites(s.body, u.id)).flatMap((s) => s.places.length ? s.places : [`"${s.heading}"`]))];
|
|
627
|
+
const where = elsewhere.length
|
|
628
|
+
? `is cited only under ${elsewhere.join(", ")}`
|
|
629
|
+
: cites(uxText, u.id) ? "is cited in ux-behavior.md but on no screen" : "is cited on no screen";
|
|
630
|
+
findings.push({ rule: "BREADBOARD-UI", level: "red", detail: `${u.id} (${live.map(name).join(" / ")}) ${where} — specify it in the screen section of its own Place` });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// BREADBOARD-TRACE — the rest of the breadboard, reported and never blocking.
|
|
634
|
+
const trace = [];
|
|
635
|
+
const lost = [...bb.code, ...bb.stores].map((x) => x.id).filter((id) => !cites(specText, id));
|
|
636
|
+
if (lost.length) trace.push(`N#/S# cited nowhere in the spec: ${lost.join(", ")}`);
|
|
637
|
+
const slicesText = [scopeSummaryText, scopeBoardText].filter((t) => t !== null && t !== undefined).join("\n");
|
|
638
|
+
if (scopeSummaryText !== null || scopeBoardText !== null) {
|
|
639
|
+
const unsliced = bb.slices.map((v) => v.id).filter((id) => !cites(slicesText, id));
|
|
640
|
+
if (unsliced.length) trace.push(`V# slices no scope board or scope summary records: ${unsliced.join(", ")}`);
|
|
641
|
+
}
|
|
642
|
+
if (scopes.length) {
|
|
643
|
+
const sourced = new Set(scopes.flatMap((s) => (s.affordance_manifest ?? []).map((a) => String(a?.source ?? "").trim())));
|
|
644
|
+
const unsourced = bb.ui.map((u) => u.id).filter((id) => cites(uxText, id) && !sourced.has(id));
|
|
645
|
+
if (unsourced.length) trace.push(`U# the spec places but no manifest entry names as its source: ${unsourced.join(", ")}`);
|
|
646
|
+
}
|
|
647
|
+
if (unplaceable.length) trace.push(`U# with no Place column to check placement against: ${unplaceable.join(", ")}`);
|
|
648
|
+
if (trace.length) findings.push({ rule: "BREADBOARD-TRACE", level: "warn", detail: trace.join("; ") });
|
|
649
|
+
return findings;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* The breadboard a run's spec is linted against: the staged copy, else the intake when it carries
|
|
654
|
+
* one inline, else null — and null switches every BREADBOARD-* rule off.
|
|
655
|
+
* @param {string} cwd - Project root.
|
|
656
|
+
* @param {string} slug - Feature slug.
|
|
657
|
+
* @param {string} intakeContent - The run's intake, verbatim ("" when absent).
|
|
658
|
+
* @returns {(string|null)} The breadboard text, or null when the run has none.
|
|
659
|
+
*/
|
|
660
|
+
export function runBreadboard(cwd, slug, intakeContent) {
|
|
661
|
+
const p = stagedBreadboard(cwd, slug);
|
|
662
|
+
if (existsSync(p)) return readFileSync(p, "utf8");
|
|
663
|
+
return hasBreadboardTables(intakeContent) ? intakeContent : null;
|
|
664
|
+
}
|
|
665
|
+
|
|
497
666
|
/**
|
|
498
667
|
* Run the full spec lint (scopes + structure) for a slug.
|
|
499
668
|
* @param {{cwd:string, slug:string}} opts - Working root and feature slug.
|
|
500
669
|
* @returns {{slug:string, scopes:number, tasks:number, red:number, warn:number,
|
|
501
|
-
* findings:Array<object>}} Counts and the combined findings from {@link lintScopes}
|
|
502
|
-
* {@link lintStructure}.
|
|
670
|
+
* findings:Array<object>}} Counts and the combined findings from {@link lintScopes},
|
|
671
|
+
* {@link lintStructure} and, when the run has a breadboard, {@link lintBreadboard}.
|
|
503
672
|
*/
|
|
504
673
|
export function lint({ cwd, slug }) {
|
|
505
674
|
const specRoot = specDir(cwd, slug);
|
|
@@ -525,6 +694,25 @@ export function lint({ cwd, slug }) {
|
|
|
525
694
|
...lintScopeAnchors({ scopes, specDir: specRoot, reqIds, tasks }),
|
|
526
695
|
...lintCommittedTier({ cwd, slug }),
|
|
527
696
|
...lintStructure({ specDir: specRoot, tasks, intakeContent }),
|
|
697
|
+
...(() => {
|
|
698
|
+
const bbText = runBreadboard(cwd, slug, intakeContent);
|
|
699
|
+
if (!bbText) return [];
|
|
700
|
+
/**
|
|
701
|
+
* A file's text, or null when it does not exist.
|
|
702
|
+
* @param {string} p - Absolute path.
|
|
703
|
+
* @returns {(string|null)} The contents, or null.
|
|
704
|
+
*/
|
|
705
|
+
const readOr = (p) => (existsSync(p) ? readFileSync(p, "utf8") : null);
|
|
706
|
+
const specFiles = existsSync(specRoot) ? walkFiles(specRoot).filter((f) => f.endsWith(".md")) : [];
|
|
707
|
+
return lintBreadboard({
|
|
708
|
+
uxText: readOr(join(specRoot, "ux-behavior.md")) ?? "",
|
|
709
|
+
specText: specFiles.map((f) => readFileSync(join(specRoot, f), "utf8")).join("\n"),
|
|
710
|
+
scopes,
|
|
711
|
+
scopeSummaryText: readOr(join(specRoot, "scope-summary.md")),
|
|
712
|
+
scopeBoardText: readOr(join(sharedRoot(cwd, slug), "scope-board.md")),
|
|
713
|
+
bbText,
|
|
714
|
+
});
|
|
715
|
+
})(),
|
|
528
716
|
];
|
|
529
717
|
return {
|
|
530
718
|
slug,
|
package/package.json
CHANGED
|
@@ -26,6 +26,7 @@ Invoked as `--order <path>`. Fields you may rely on (absent = unknown; surface i
|
|
|
26
26
|
|---|---|
|
|
27
27
|
| `operation` | `analyze` (pitch → full spec tree + board) · `reconcile` (fold discovered-ledger items into the board + UC invariants) · `retrofit-surface` (append `## Test Surface` to a pre-surface spec) · `coverage` (extract atomic requirement clauses → the SHARED `requirements.md` registry) |
|
|
28
28
|
| `payload.pitch` | The pitch/PRD path (analyze) |
|
|
29
|
+
| `payload.breadboard` | The breadboard (analyze): its Places are your screens; its U# and N# are the affordances you place and cite. Absent = none separate; never inferred |
|
|
29
30
|
| `payload.requirements` | (coverage) the REQ source to extract atomic clauses from — pitch / a customer-requirements doc / the use-case bodies. Absent → default to the pitch and record the choice in `assumptions[]` |
|
|
30
31
|
| `payload.lens` | `lite` \| `standard` \| `cross-context`. Absent → judge it: LITE for ≤2-week appetite, no third-party, ≤3 user-facing actions; STANDARD for multi-team, third-party, or bigger appetite; genuinely unclear → one binary question, or `status: "escalated"` with the question in `deviations[]` |
|
|
31
32
|
| `payload.orient_dir` | The Scout's artifacts — `code-surface.md` IS your codebase map (do not re-scan), `discovered-seed.md` seeds task gen, `spike-*.md` feeds feasibility |
|
|
@@ -43,8 +44,11 @@ Phases, each with a checkpoint (pause only per `interaction`). Read the referenc
|
|
|
43
44
|
its phase; templates live in `assets/templates/`.
|
|
44
45
|
|
|
45
46
|
```
|
|
46
|
-
1 INGEST pitch +
|
|
47
|
-
|
|
47
|
+
1 INGEST pitch + breadboard (`payload.breadboard`, or tables inline in the pitch) +
|
|
48
|
+
orient artifacts + KB. Extract slug, appetite, in/out boundaries, rabbit
|
|
49
|
+
holes, third-party mentions. With a breadboard, list every Place (P#) and UI
|
|
50
|
+
affordance (U#) first — they are the screens and interactive elements Phase 3
|
|
51
|
+
must place. No files written yet.
|
|
48
52
|
1b FEASIBILITY (third-party/API/SDK/webhook mentioned) verification questions + fallback
|
|
49
53
|
scope per API-NN → api-feasibility.md
|
|
50
54
|
2 DDD bounded contexts, aggregates (new vs extended), value objects, domain events,
|
|
@@ -52,8 +56,9 @@ its phase; templates live in `assets/templates/`.
|
|
|
52
56
|
2b CONTRACTS (standard lens) typed Request/Response/Error per repository; two-pass rule:
|
|
53
57
|
unresolvable at spec time → `⏳ TBD — verify in the [UC-x] spike`, resolved
|
|
54
58
|
post-SPIKE with citation → contracts/ [references/contract-patterns.md]
|
|
55
|
-
3 UX per screen
|
|
56
|
-
|
|
59
|
+
3 UX per screen — with a breadboard, one screen per Place that owns UI affordances:
|
|
60
|
+
state table (idle→loading→error→success), error cases with message+action,
|
|
61
|
+
ASCII flows → ux-behavior.md [references/ux-behavior-patterns.md]
|
|
57
62
|
4 USE CASES one file per actor+action: typed Input/Output, numbered Steps, all error
|
|
58
63
|
cases with codes, ## System Flow (UI→API→UC→Repo→DB), ## Test Surface
|
|
59
64
|
(DERIVED ONLY from D1 Invariants · D2 Error Cases · D3 Contract shape ·
|
|
@@ -69,7 +74,8 @@ its phase; templates live in `assets/templates/`.
|
|
|
69
74
|
overflow is a fact you REPORT for the caller's HAMMER gate, never resolve)
|
|
70
75
|
node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" verify spec --slug <slug>
|
|
71
76
|
(structure, wikilinks, edge symmetry — fix reds, then re-run; you never
|
|
72
|
-
self-grade with a hand-walked checklist
|
|
77
|
+
self-grade with a hand-walked checklist. BREADBOARD-PLACE / BREADBOARD-UI:
|
|
78
|
+
add the screen or defer the Place; never fold it into another screen)
|
|
73
79
|
→ scope-summary.md + synthesis.md (traceability matrix, risk register,
|
|
74
80
|
dependency graph — the JUDGMENT layers over board-derive's numbers)
|
|
75
81
|
8 INDEX _index.md (pitch digest + document map) + feedback.md template
|
|
@@ -29,7 +29,8 @@ audit_rules_version: "2.5"
|
|
|
29
29
|
## Solution Elements
|
|
30
30
|
|
|
31
31
|
### Breadboarding
|
|
32
|
-
<!-- Text-based flow showing the key interaction path, no images needed
|
|
32
|
+
<!-- Text-based flow showing the key interaction path, no images needed. With a breadboard,
|
|
33
|
+
name Places and affordances by id: P1 Cart ──U1──► P2 Payment Sheet -->
|
|
33
34
|
```
|
|
34
35
|
[Screen A] ──action──► [Screen B] ──action──► [Outcome]
|
|
35
36
|
│
|
|
@@ -29,7 +29,7 @@ status: draft
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## Screen: [ScreenName]
|
|
32
|
+
## Screen: [ScreenName] ([P#] — omit without a breadboard)
|
|
33
33
|
|
|
34
34
|
### States
|
|
35
35
|
|
|
@@ -54,7 +54,7 @@ status: draft
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
<!-- Repeat "Screen: [Name]" section for each screen -->
|
|
57
|
+
<!-- Repeat "Screen: [Name]" section for each screen — one per breadboard Place with UI affordances -->
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -63,3 +63,13 @@ status: draft
|
|
|
63
63
|
| Behavior | Mobile | Web |
|
|
64
64
|
|---|---|---|
|
|
65
65
|
| [behavior] | [mobile treatment] | [web treatment] |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Deferred Places
|
|
70
|
+
|
|
71
|
+
<!-- Breadboard Places with UI affordances this shape will not build. Each needs the PO's yes at GATE L1b. Omit the section when there are none. -->
|
|
72
|
+
|
|
73
|
+
| Place | Reason |
|
|
74
|
+
|---|---|
|
|
75
|
+
| [P#] [Place name] | [why this shape does not build it] |
|
|
@@ -126,6 +126,9 @@ Required sections: Screen Flow (ASCII diagram), one section per Screen with:
|
|
|
126
126
|
- **Visual & Layout Specs**: Flex/Grid structure, spacing, alignment rules, and desktop/mobile responsiveness
|
|
127
127
|
- **Design Tokens**: Specific CSS variables or Tailwind classes used for background, borders, fonts, and actions
|
|
128
128
|
- **States table**, **Behavior Rules list**, **Error States table**
|
|
129
|
+
- Screen headings carry the breadboard Place id when a breadboard exists — `## Screen: Payment Sheet (P2)`, one screen per Place that owns UI affordances, each U# cited inside its own Place's section
|
|
130
|
+
|
|
131
|
+
Plus **Deferred Places**, when any: a `Place | Reason` table of breadboard Places this shape will not build.
|
|
129
132
|
|
|
130
133
|
|
|
131
134
|
---
|
|
@@ -15,6 +15,15 @@ From the pitch breadboarding and fat marker sketches, identify:
|
|
|
15
15
|
|
|
16
16
|
Each decision point is typically a screen boundary.
|
|
17
17
|
|
|
18
|
+
> **With a breadboard, the screens are its Places.** Write one `## Screen:` section per Place that
|
|
19
|
+
> owns at least one UI affordance, with the Place id in the heading — `## Screen: Payment Sheet (P2)`.
|
|
20
|
+
> Cite each UI affordance's id (`U3`) in the state-table row or behavior rule that specifies it,
|
|
21
|
+
> inside the section of the Place the breadboard puts it in. A Place is a screen boundary: a sheet or
|
|
22
|
+
> modal the breadboard names as its own Place is never folded into its parent's state table, even
|
|
23
|
+
> when the pitch's prose describes it as part of the parent. A Place with no UI affordances (a
|
|
24
|
+
> backend, a store) needs no screen. A Place this shape will not build goes in `## Deferred Places`
|
|
25
|
+
> with the reason; it surfaces at GATE L1b, where the PO accepts or rejects the deferral.
|
|
26
|
+
|
|
18
27
|
---
|
|
19
28
|
|
|
20
29
|
## State Machine per Screen
|
package/skills/orient/SKILL.md
CHANGED
|
@@ -42,7 +42,8 @@ from `tech-lead`; it never reads or writes a shared run-state file.
|
|
|
42
42
|
## Input contract (pure worker)
|
|
43
43
|
|
|
44
44
|
Orchestrated, you are invoked as `--order <path>` (a WorkOrder): `payload.pitch` (the
|
|
45
|
-
kicked-off pitch path), `payload.
|
|
45
|
+
kicked-off pitch path), `payload.breadboard` (the pitch's breadboard — Places, affordances, slices;
|
|
46
|
+
absent = none separate), `payload.stack` (sweep hint), `payload.spec_folder` (the SHARED spec
|
|
46
47
|
deliverable dir) and `payload.feature` (the run slug), plus `substrate.allowed` naming your one
|
|
47
48
|
write surface — the orient output dir. Anything absent = unknown: confirm at GATE O-A
|
|
48
49
|
(standalone) or report it in the result's `deviations`, never guess. Standalone, the
|
|
@@ -104,7 +105,8 @@ Confirm (do not guess):
|
|
|
104
105
|
Shaped signal: frontmatter status: shaped AND bet: <S1|S2|...> (or equivalent).
|
|
105
106
|
If the pitch lacks appetite AND solution boundaries → STOP and tell tech-lead:
|
|
106
107
|
"Orient runs on a kicked-off pitch, not a raw idea. Shape/bet first (PO upstream)."
|
|
107
|
-
- breadboard
|
|
108
|
+
- breadboard — read `payload.breadboard` when present; absent means the pitch has no separate
|
|
109
|
+
breadboard — look for Places and affordance tables in the pitch itself
|
|
108
110
|
- spec folder target (create orient/ if absent)
|
|
109
111
|
- codebase root
|
|
110
112
|
```
|
|
@@ -136,7 +138,8 @@ Useful sweeps (adapt to the stack arg):
|
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
Write `code-surface.md`: one row per pitch element → `file:line` it touches (or "NEW — no
|
|
139
|
-
existing home"), the seam it extends, and whether it's new vs. existing.
|
|
141
|
+
existing home"), the seam it extends, and whether it's new vs. existing. With a breadboard, each row
|
|
142
|
+
carries the P#, U# or N# of the element it locates — a Place with no existing home is a NEW screen. Flag every place the
|
|
140
143
|
map is uncertain — uncertainty is signal for Phase 3, not something to hide.
|
|
141
144
|
|
|
142
145
|
> **Output location.** All four orient artifacts are run-trace (recon scratch), so they
|
|
@@ -257,7 +260,7 @@ Tech-lead uses this to render the GATE L1a Hill and confirm the spike before han
|
|
|
257
260
|
### Flags
|
|
258
261
|
| Flag | Effect |
|
|
259
262
|
|------|--------|
|
|
260
|
-
| `--pitch <path>` | The kicked-off pitch
|
|
263
|
+
| `--pitch <path>` | The kicked-off pitch. Read `payload.breadboard` when present; absent means the pitch has no separate breadboard — look for Places and affordance tables in the pitch itself |
|
|
261
264
|
| `--spec <path>` | SHARED spec deliverable dir (shapeup/<feat>/spec/); orient *artifacts* are written to the LOCAL root `.shapeup/<feat>/orient/` |
|
|
262
265
|
| `--stack <hint>` | Stack hint to aim the code-surface sweeps |
|
|
263
266
|
| `--auto` | Auto-confirm O-A and O-B; run straight through |
|
|
@@ -21,6 +21,7 @@ the ship report's census table.
|
|
|
21
21
|
|---|---|
|
|
22
22
|
| `operation` | `map-scopes` — the only operation this skill has. It covers first slicing after the board exists, folding discovered items in, and re-slicing a stuck scope; the payload says which of those you are doing |
|
|
23
23
|
| `payload.feature` / `payload.spec_folder` | Slug + committed spec (read ux-behavior.md for manifests; usecases for flows) |
|
|
24
|
+
| `payload.breadboard` | When present, every U# the spec places is one manifest entry's `source`; record which scopes deliver each V# slice in `scope-board.md` (your write surface — `scope-summary.md` is the planner's) |
|
|
24
25
|
| `payload.tasks[]` | The board's tasks with their touched files — the slicing INPUT only. Each carries `use_case_refs`; those UC ids are what you write into the contract. Never copy a task id into a contract |
|
|
25
26
|
| `substrate.allowed` | `scopes/*.md` + `scope-board.md` — your ONLY write surface |
|
|
26
27
|
|
|
@@ -65,6 +66,8 @@ the ship report's census table.
|
|
|
65
66
|
element as {test_id, role} +
|
|
66
67
|
required_states [idle, loading,
|
|
67
68
|
success, error, empty]
|
|
69
|
+
+ `source` — the U# the
|
|
70
|
+
ux-behavior row cites
|
|
68
71
|
e2e_verification_fixtures[] — the command(s)/spec file(s)
|
|
69
72
|
that drive this scope
|
|
70
73
|
end-to-end (T0 layer); too
|
|
@@ -134,6 +137,7 @@ territory — and any lint warn left standing, with why). You never touch task f
|
|
|
134
137
|
- [ ] Every scope that consumes another's output declares it in `depends_on`
|
|
135
138
|
- [ ] Substrates disjoint except declared shared_substrate (DISJOINT = 0 red)
|
|
136
139
|
- [ ] Every interactive element in scope screens appears in exactly one affordance_manifest
|
|
140
|
+
- [ ] Every U# the spec places is some manifest entry's `source`
|
|
137
141
|
- [ ] Every scope has fixtures or an explicit TBD flag
|
|
138
142
|
- [ ] Every hill_phase written is UPHILL_UNKNOWN; superseded contracts kept
|
|
139
143
|
- [ ] The WorkResult validates against `work-result.schema.json`
|
|
@@ -42,6 +42,7 @@ return as a WorkResult.
|
|
|
42
42
|
|---|---|
|
|
43
43
|
| `operation` | `wire` (author/refresh the wiring map after `analyze`, before `map-scopes`) |
|
|
44
44
|
| `payload.feature` / `payload.spec_folder` | Slug + committed spec — read `usecases/` for the UCs and the engine each one needs, `domain-model.md`/`synthesis.md` for the module surface |
|
|
45
|
+
| `payload.breadboard` | When present, name each UC's `affordance` by its U# and Place |
|
|
45
46
|
| `payload.project_profile` | Path to the SHARED `project-profile.md`. Its `entry_point` is the composition root every engine must attach to — **archetype-specific** (a client-only game's `main.js` is not a web-service's `src/server.ts`). Read it; never guess the entry point |
|
|
46
47
|
| `substrate.allowed` | `wiring-map.md` — your ONLY write surface (the spec core, scopes, and the profile are frozen) |
|
|
47
48
|
|
|
@@ -69,7 +70,9 @@ guessed `main.js` would make the later oracle certify nothing.
|
|
|
69
70
|
file:line is a build-time fact (the oracle proves reachability by
|
|
70
71
|
the import graph, it does not parse this field)
|
|
71
72
|
affordance the player-visible thing this UC exposes once wired (the human
|
|
72
|
-
end of the chain — what a user can DO, not an internal call)
|
|
73
|
+
end of the chain — what a user can DO, not an internal call);
|
|
74
|
+
with a breadboard, name it by U# and Place —
|
|
75
|
+
`U1 Pay (P1) → P2 Payment Sheet`
|
|
73
76
|
3 WRITE shapeup/<slug>/wiring-map.md (WiringMap): frontmatter for schema_version, feature
|
|
74
77
|
and entry_point (echo of the profile), then entries[] as ONE MARKDOWN TABLE under a
|
|
75
78
|
`## Wiring` heading — this exact shape, because it is the only one the reader parses:
|
|
@@ -23,7 +23,7 @@ turns fighting shell quoting):
|
|
|
23
23
|
node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" init run \
|
|
24
24
|
--slug <slug-from-the-request> --intake-file <path/to/the/requirement.md> \
|
|
25
25
|
--auto-level <interactive|auto|unattended> \
|
|
26
|
-
[--dimensions <a,b>] [--gate-answers <ci|guarded|path.json>] [--wall-clock-budget <seconds>] [--max-rounds 3]
|
|
26
|
+
[--dimensions <a,b>] [--gate-answers <ci|guarded|path.json>] [--wall-clock-budget <seconds>] [--max-rounds 3] [--breadboard <path>]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
**After a compaction, or in a fresh session over an open run, re-derive before you act.** One
|
|
@@ -43,11 +43,11 @@ counts.
|
|
|
43
43
|
plugin and need a one-time permission grant (`npx shapeup-sdlc init` writes it). Do not route
|
|
44
44
|
around it, and do not silently hand-build the feature instead.
|
|
45
45
|
|
|
46
|
-
**Language gate (delegated to `translator`, not this skill):**
|
|
47
|
-
an Agent (model: exec)
|
|
48
|
-
English → proceed as-is. Non-English →
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
**Language gate (delegated to `translator`, not this skill):** before Step 1 opens the run, dispatch
|
|
47
|
+
an Agent (model: exec) calling `Skill(shapeup-sdlc-plugin:translator) --check` on the pitch *and* its
|
|
48
|
+
breadboard. English → proceed as-is. Non-English → a second Agent translates both (`--auto` under
|
|
49
|
+
auto/unattended); Step 1 then names the `.en.md` files (a run already open on the original: re-open
|
|
50
|
+
it with `--force` — nothing is dispatched yet). The tech lead detects and sequences; never translates.
|
|
51
51
|
|
|
52
52
|
**Step 2 — pin GATE L0, then launch.** Collect the L0.1–L0.9 config (spec folder, lens, stack,
|
|
53
53
|
eval dims, max_rounds, the model/budget matrix — see `references/gates.md` GATE L0 for the full
|
|
@@ -31,13 +31,19 @@ escalates, writes nothing, and every relaunch re-dispatches it.
|
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
Collect (explicit — never inferred):
|
|
34
|
-
L0.1 Kicked-off pitch source:
|
|
34
|
+
L0.1 Kicked-off pitch source: `shaping.md` — and its `breadboard.md`, which the run finds
|
|
35
|
+
beside it (or in `shaping/`) or takes from `--breadboard`; a `pitch.md` may carry the
|
|
36
|
+
breadboard inline. Already shaped + bet by PO.
|
|
35
37
|
Not a raw idea — shaping (1-4) / betting (5) / kick-off (6) are PO-personal, upstream.
|
|
36
|
-
L0.1a Language gate: Agent (model: exec) → Skill(shapeup-sdlc-plugin:translator)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
L0.1a Language gate, BEFORE init run: Agent (model: exec) → Skill(shapeup-sdlc-plugin:translator)
|
|
39
|
+
--check over the pitch AND its breadboard.
|
|
40
|
+
English → use both as-is.
|
|
41
|
+
non-English → Agent (model: exec) → Skill(shapeup-sdlc-plugin:translator) <pitch> <breadboard>
|
|
42
|
+
(--auto under auto/unattended), then open the run on the produced
|
|
43
|
+
<name>.en.md — init run prefers a breadboard.en.md beside it, and warns
|
|
44
|
+
when it can find only the untranslated breadboard. The run's intake is
|
|
45
|
+
what every planning worker reads, so a translation made after the run
|
|
46
|
+
opened reaches nobody: re-open with --force naming the .en.md. Log in ledger.
|
|
41
47
|
L0.1b Appetite: read the `appetite` field from the pitch's YAML frontmatter (set by /shapeup).
|
|
42
48
|
Surface it in the gate output. Use it to:
|
|
43
49
|
- Contextualise the scope at L1b (right-size cuts to the budget).
|
|
@@ -167,8 +173,9 @@ committing to a scope map. This is the first Hill read (area-level — slices do
|
|
|
167
173
|
```
|
|
168
174
|
Read .shapeup/<slug>/orient/. Render the 🗻 Hill from hill-signal.md (see protocol.md "Hill report"):
|
|
169
175
|
- each suspected area → uphill (open unknowns) | crest (approach proven by the spike) | downhill
|
|
170
|
-
Print:
|
|
171
|
-
|
|
176
|
+
Print: `Breadboard: <source> | none` (how init run found the pitch's breadboard — flag, sibling,
|
|
177
|
+
shaping-dir, shared-root, embedded — or none), the code-surface headline (where it lands),
|
|
178
|
+
the spiked area + result, the riskiest open unknowns going into mapping.
|
|
172
179
|
Ask (max 2): is the riskiest area the right one to have spiked? any unknown that must be
|
|
173
180
|
resolved (another spike) before we map scopes?
|
|
174
181
|
```
|
|
@@ -186,7 +193,7 @@ Do NOT enter MAP SCOPES until Orient is accepted.
|
|
|
186
193
|
mobile|library|data-pipeline}; entry_point is the reachability seam (a game's main.js is NOT a
|
|
187
194
|
service's src/server.ts). Validate the enum — a typo must fail, not silently disable the check.
|
|
188
195
|
2. WIRE — compile-order --operation wire --slug <slug> (worker→solution-architect), payload
|
|
189
|
-
{project_profile}. Sole writer of committed wiring-map.md (per-UC engine → seam → entry-point
|
|
196
|
+
{project_profile, breadboard?}. Sole writer of committed wiring-map.md (per-UC engine → seam → entry-point
|
|
190
197
|
call site → affordance). ⏸ GATE L1a.5: confirm each UC has a declared seam before slicing.
|
|
191
198
|
⟐ PRECONDITION: MAP SCOPES step 1 (ANALYZE) has already run and usecases/ is
|
|
192
199
|
populated. WIRE writes one entry per use case, so dispatching it against an empty spec folder
|
|
@@ -212,7 +219,7 @@ against the seams WIRE declared. Sequence: ORIENT → L1a → **ANALYZE** → **
|
|
|
212
219
|
```
|
|
213
220
|
Two orders, two workers, one step (both model: exec — see references/protocol.md):
|
|
214
221
|
1. ANALYZE + BOARD — compile-order --operation analyze --slug <slug> --worker ba-pitch-analyzer
|
|
215
|
-
--payload '{"pitch": "<path>", "lens": "<lens>", "orient_dir": ".shapeup/<slug>/orient/"}'
|
|
222
|
+
--payload '{"pitch": "<path>", "breadboard": "<the path init run printed, when it printed one>", "lens": "<lens>", "orient_dir": ".shapeup/<slug>/orient/"}'
|
|
216
223
|
dispatch: Skill(shapeup-sdlc-plugin:ba-pitch-analyzer) --order <path>. The order hands it
|
|
217
224
|
code-surface.md (Phase-1 ingest consumes the map, does not re-scan), discovered-seed.md
|
|
218
225
|
(task gen starts from reality), spike-<area>.md (feasibility/contracts).
|
|
@@ -265,6 +272,8 @@ Scope contracts present:
|
|
|
265
272
|
- scope board: scope_id, topology_type, substrate file count (scopes/*.md / scope-board.md)
|
|
266
273
|
- any SPIKE blockers (scope-summary.md)
|
|
267
274
|
- scope-summary "Done when" headline statements
|
|
275
|
+
- the Deferred Places from ux-behavior.md (breadboard Places this shape will not build) —
|
|
276
|
+
each one needs the PO's yes; a rejected deferral goes back to the planner as a screen
|
|
268
277
|
No scope contracts (pre-v0.3.0, unchanged from v0.2.6):
|
|
269
278
|
Read tasks/_index.md (LOCAL root). Print:
|
|
270
279
|
- task count by package/variant (.shared / .be / .web / .mobile / .e2e)
|
|
@@ -280,7 +289,11 @@ this is the orchestrator's own re-confirmation before committing to a build sequ
|
|
|
280
289
|
waiting to happen), PA1 (directory-aligned scope), PA2 (size cap), SCOPE-ANCHOR (a scope
|
|
281
290
|
naming no committed use case, or one that does not resolve), TIER-DIRECTION (a committed
|
|
282
291
|
contract naming LOCAL task ids), SCOPE-DEPS (a build-order id naming a scope that is not
|
|
283
|
-
in this run)
|
|
292
|
+
in this run), BREADBOARD-PLACE (a breadboard Place with UI affordances has no
|
|
293
|
+
`## Screen: … (P#)` in ux-behavior.md and is not deferred), BREADBOARD-UI (a U# not
|
|
294
|
+
specified on a screen of its own Place). Any red → HARD STOP, past a 🔴 at the
|
|
295
|
+
architect's own checkpoint. The breadboard reds are the planner's to fix — add the screen
|
|
296
|
+
or defer the Place; never fold it into another screen.
|
|
284
297
|
- Lock the build SEQUENCE riskiest-first: order scopes by open-unknowns count (from
|
|
285
298
|
hill/<scope-id>.yml if present, else the orient hill signal), not by file count or
|
|
286
299
|
alphabetical — Shape Up's "solve in the right sequence" (step 10).
|
|
@@ -414,7 +427,8 @@ S.6 Harvest one signal row → append to `.shapeup/metrics/<machine-id>.jsonl`
|
|
|
414
427
|
deliberately without colliding on one filename. The read plane is
|
|
415
428
|
`harness probe stats`, or `cat .shapeup/metrics/*.jsonl`).
|
|
416
429
|
Copy fields that ALREADY exist as structured output (run-state, final EVAL report,
|
|
417
|
-
discovery ledger, qa/hunt-report, breadboard
|
|
430
|
+
discovery ledger, qa/hunt-report, and the receipt's `breadboard.ids.V` for `slice_count` —
|
|
431
|
+
the slices init run counted in the staged breadboard, never a hand copy). Two hard rules:
|
|
418
432
|
1. Harvest only fields that already exist at ship time — never evaluate something new.
|
|
419
433
|
2. Record facts, never compute a new verdict (no `run_quality_score` — that would be
|
|
420
434
|
a second judge behind spec-evaluator). The eval suite interprets; harvest records.
|
|
@@ -311,14 +311,15 @@ workers keep only their own product-idempotency key and emit domain artifacts.
|
|
|
311
311
|
|
|
312
312
|
## 0. LANGUAGE GATE → translator (GATE L0, only if non-English)
|
|
313
313
|
```
|
|
314
|
-
Invoke via Agent (model: exec): Skill(shapeup-sdlc-plugin:translator) --check "<intake path>"
|
|
314
|
+
Invoke via Agent (model: exec), BEFORE init run: Skill(shapeup-sdlc-plugin:translator) --check "<intake path>" "<breadboard path>"
|
|
315
315
|
# detect-only, writes nothing
|
|
316
316
|
English → skip; ORIENT against the original.
|
|
317
|
-
non-English → Agent (model: exec): Skill(shapeup-sdlc-plugin:translator) "<intake path>" [--auto]
|
|
318
|
-
# full pass
|
|
319
|
-
Writes: <name>.en.md (English copy; original untouched) + glossary.md
|
|
317
|
+
non-English → Agent (model: exec): Skill(shapeup-sdlc-plugin:translator) "<intake path>" "<breadboard path>" [--auto]
|
|
318
|
+
# full pass — the pitch AND its breadboard, before init run
|
|
319
|
+
Writes: <name>.en.md per file (English copy; original untouched) + glossary.md
|
|
320
320
|
+ translation-report.md.
|
|
321
|
-
|
|
321
|
+
Open the run on the .en.md pitch: init run stages breadboard.en.md beside it,
|
|
322
|
+
and every planning worker reads what init run pinned — never a later copy.
|
|
322
323
|
Read back: the detect table (--check) / the .en.md path + residual scan result (full pass).
|
|
323
324
|
Authority: translator normalizes language only — it does not orient/plan/build/judge. The tech
|
|
324
325
|
lead never translates itself; it only detects and sequences this step before ORIENT.
|
|
@@ -340,7 +341,7 @@ Authority: pure worker — no code, no board, no run-state, no reporting.
|
|
|
340
341
|
```
|
|
341
342
|
Order A (the spec tree + board):
|
|
342
343
|
compile-order --operation analyze --slug <slug> --worker ba-pitch-analyzer
|
|
343
|
-
--payload '{"pitch": "<path>", "lens": "<lens>", "orient_dir": ".shapeup/<slug>/orient/"}'
|
|
344
|
+
--payload '{"pitch": "<path>", "breadboard": "<the path init run printed, when it printed one>", "lens": "<lens>", "orient_dir": ".shapeup/<slug>/orient/"}'
|
|
344
345
|
Agent (model: exec): Skill(shapeup-sdlc-plugin:ba-pitch-analyzer) --order <path>
|
|
345
346
|
The order hands it code-surface.md (Phase-1 ingest, no re-scan), discovered-seed.md (task
|
|
346
347
|
gen from reality), spike-<area>.md (feasibility/contracts).
|
|
@@ -826,7 +827,7 @@ fixtures run in isolation and do not consume it.
|
|
|
826
827
|
| `spike_unresolved_count` | `SPIKE-UNRESOLVED` markers at bet | shaping quality — open risk into bet |
|
|
827
828
|
| `scope_cut_count` | `~` items cut at SHIP S.0 | appetite pressure / scope hammer |
|
|
828
829
|
| `qa_findings` | `.shapeup/<slug>/qa/hunt-report.md` + triage → `{total, promoted, held}` | edge quality |
|
|
829
|
-
| `slice_count` | breadboard
|
|
830
|
+
| `slice_count` | `receipt.json` → `breadboard.ids.V` (the V# slices init run counted in the staged breadboard; omit the field when `breadboard` is null) | **normalizer / denominator** |
|
|
830
831
|
| `sources` | path to each **SHARED** source artifact — never a LOCAL `.shapeup/` path (the run-trace is superseded run by run, so a LOCAL path dangles by the time anyone reads the row; SHARED paths resolve on any clone — tier-direction rule) | auditability |
|
|
831
832
|
|
|
832
833
|
- `slice_count` is the **denominator**: `round_count=4` on a 2-slice feature is alarming,
|
|
@@ -314,6 +314,7 @@
|
|
|
314
314
|
],
|
|
315
315
|
"ba-pitch-analyzer": [
|
|
316
316
|
"pitch",
|
|
317
|
+
"breadboard",
|
|
317
318
|
"lens",
|
|
318
319
|
"orient_dir",
|
|
319
320
|
"spec_folder",
|
|
@@ -324,12 +325,14 @@
|
|
|
324
325
|
"scope-architect": [
|
|
325
326
|
"feature",
|
|
326
327
|
"spec_folder",
|
|
327
|
-
"tasks"
|
|
328
|
+
"tasks",
|
|
329
|
+
"breadboard"
|
|
328
330
|
],
|
|
329
331
|
"solution-architect": [
|
|
330
332
|
"feature",
|
|
331
333
|
"spec_folder",
|
|
332
|
-
"project_profile"
|
|
334
|
+
"project_profile",
|
|
335
|
+
"breadboard"
|
|
333
336
|
],
|
|
334
337
|
"spec-evaluator": [
|
|
335
338
|
"spec_folder",
|
|
@@ -342,6 +345,7 @@
|
|
|
342
345
|
],
|
|
343
346
|
"orient": [
|
|
344
347
|
"pitch",
|
|
348
|
+
"breadboard",
|
|
345
349
|
"stack",
|
|
346
350
|
"spec_folder",
|
|
347
351
|
"feature"
|
|
@@ -709,6 +713,10 @@
|
|
|
709
713
|
"type": "string"
|
|
710
714
|
},
|
|
711
715
|
"description": "Subset of [idle, loading, success, error, empty] the element must express via data-state."
|
|
716
|
+
},
|
|
717
|
+
"source": {
|
|
718
|
+
"type": "string",
|
|
719
|
+
"description": "The breadboard UI affordance (U#) this element implements; absent without a breadboard."
|
|
712
720
|
}
|
|
713
721
|
}
|
|
714
722
|
},
|
|
@@ -2248,6 +2256,10 @@
|
|
|
2248
2256
|
"type": "string",
|
|
2249
2257
|
"description": "ba-pitch-analyzer (analyze) / orient: the kicked-off pitch path (shaped + bet; frontmatter appetite/status/bet)."
|
|
2250
2258
|
},
|
|
2259
|
+
"breadboard": {
|
|
2260
|
+
"type": "string",
|
|
2261
|
+
"description": "orient / ba-pitch-analyzer (analyze) / solution-architect / scope-architect: the run's staged breadboard — Places (P#), UI and code affordances (U#, N#), stores (S#), slices (V#). Absent = the pitch has no separate breadboard (it may carry one inline); never inferred from the pitch's folder."
|
|
2262
|
+
},
|
|
2251
2263
|
"lens": {
|
|
2252
2264
|
"type": "string",
|
|
2253
2265
|
"enum": [
|
|
@@ -2379,6 +2391,20 @@
|
|
|
2379
2391
|
"type": "string",
|
|
2380
2392
|
"description": "Resolved path to the run's intake — the pitch a fresh ORIENT dispatch is compiled against."
|
|
2381
2393
|
},
|
|
2394
|
+
"breadboard_path": {
|
|
2395
|
+
"type": [
|
|
2396
|
+
"string",
|
|
2397
|
+
"null"
|
|
2398
|
+
],
|
|
2399
|
+
"description": "Resolved path to the run's staged breadboard (`.shapeup/<slug>/breadboard.md`), the pitch's second half, handed to the four planning dispatches as payload.breadboard. Null when the pitch had no separate breadboard file."
|
|
2400
|
+
},
|
|
2401
|
+
"breadboard_source": {
|
|
2402
|
+
"type": [
|
|
2403
|
+
"string",
|
|
2404
|
+
"null"
|
|
2405
|
+
],
|
|
2406
|
+
"description": "How `init run` found the breadboard — flag | sibling | shaping-dir | shared-root | embedded — read from the receipt. Null when there was none or the receipt is unreadable."
|
|
2407
|
+
},
|
|
2382
2408
|
"spec_folder": {
|
|
2383
2409
|
"type": [
|
|
2384
2410
|
"string",
|
|
@@ -400,6 +400,7 @@ const RESUME = {
|
|
|
400
400
|
type: "object",
|
|
401
401
|
properties: {
|
|
402
402
|
intake_path: nullable("string"), spec_folder: nullable("string"), orient_dir: nullable("string"),
|
|
403
|
+
breadboard_path: nullable("string"), breadboard_source: nullable("string"),
|
|
403
404
|
project_profile_path: nullable("string"), status: nullable("string"),
|
|
404
405
|
lens: nullable("string"), stack: nullable("string"),
|
|
405
406
|
run_cmd: nullable("string"), app_url: nullable("string"),
|
|
@@ -972,7 +973,7 @@ if (!rs.has_orient_artifacts) {
|
|
|
972
973
|
await setRunStatus("orienting", "Orient");
|
|
973
974
|
const o = await worker({
|
|
974
975
|
skill: "orient", operation: "orient", schema: ORIENT, phase: "Orient", label: "orient",
|
|
975
|
-
payload: { pitch: rs.intake_path, spec_folder: specFolder, feature: slug, stack: rs.stack },
|
|
976
|
+
payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, stack: rs.stack },
|
|
976
977
|
// NAME THE FILES. "write the orient/ artifacts" was the whole instruction, while completion is
|
|
977
978
|
// decided by four exact filenames — so a leg that did the work and called its output
|
|
978
979
|
// `code-surface-map.md` and `discovered-tasks.md` aborted the run at the post-condition, having
|
|
@@ -998,8 +999,10 @@ if (!rs.has_orient_artifacts) {
|
|
|
998
999
|
}
|
|
999
1000
|
|
|
1000
1001
|
{
|
|
1002
|
+
// `breadboard` travels in the block because a missing one is invisible anywhere later: every
|
|
1003
|
+
// downstream artifact reads the same whether or not the pitch's second half reached the run.
|
|
1001
1004
|
const g = await crossGate("L1a", "Orient", ["proceed", "ask", "abort"],
|
|
1002
|
-
{ spiked_area: spikedArea, spike_result: spikeResult, riskiest_unknowns: riskiest });
|
|
1005
|
+
{ breadboard: rs.breadboard_source ?? "none", spiked_area: spikedArea, spike_result: spikeResult, riskiest_unknowns: riskiest });
|
|
1003
1006
|
if (g.stop) return withWarnings(g.stop);
|
|
1004
1007
|
}
|
|
1005
1008
|
|
|
@@ -1015,7 +1018,7 @@ if (!rs.has_spec_tree) {
|
|
|
1015
1018
|
await setRunStatus("mapping", "Analyze");
|
|
1016
1019
|
const a = await worker({
|
|
1017
1020
|
skill: "ba-pitch-analyzer", operation: "analyze", schema: PHASE_OK, phase: "Analyze", label: "analyze",
|
|
1018
|
-
payload: { pitch: rs.intake_path, spec_folder: specFolder, feature: slug, lens: rs.lens, orient_dir: rs.orient_dir },
|
|
1021
|
+
payload: { pitch: rs.intake_path, breadboard: rs.breadboard_path, spec_folder: specFolder, feature: slug, lens: rs.lens, orient_dir: rs.orient_dir },
|
|
1019
1022
|
extra: "Write the spec tree and the board from the orient artifacts — do not re-scan the code.",
|
|
1020
1023
|
});
|
|
1021
1024
|
if (a.__failed) return diedAt("ANALYZE", a);
|
|
@@ -1047,7 +1050,7 @@ if (!rs.has_wiring_map) {
|
|
|
1047
1050
|
log(`WIRE — dispatching (slug ${slug})`);
|
|
1048
1051
|
const w = await worker({
|
|
1049
1052
|
skill: "solution-architect", operation: "wire", schema: PHASE_OK, phase: "Wire", label: "wire",
|
|
1050
|
-
payload: { feature: slug, spec_folder: specFolder, project_profile: rs.project_profile_path },
|
|
1053
|
+
payload: { feature: slug, spec_folder: specFolder, project_profile: rs.project_profile_path, breadboard: rs.breadboard_path },
|
|
1051
1054
|
extra: "Write the wiring map: per use case, engine → seam → entry-point call site → affordance.",
|
|
1052
1055
|
});
|
|
1053
1056
|
if (w.__failed) return diedAt("WIRE", w);
|
|
@@ -1078,7 +1081,7 @@ if (scopes.length === 0) {
|
|
|
1078
1081
|
log(`MAP SCOPES — dispatching (slug ${slug})`);
|
|
1079
1082
|
const m = await worker({
|
|
1080
1083
|
skill: "scope-architect", operation: "map-scopes", schema: MAPSCOPES, phase: "MapScopes", label: "map-scopes",
|
|
1081
|
-
payload: { feature: slug },
|
|
1084
|
+
payload: { feature: slug, breadboard: rs.breadboard_path },
|
|
1082
1085
|
// SAY THE PASS RULE, for the same reason ORIENT's filenames are named above: the rule lives in
|
|
1083
1086
|
// `verify t0` (a fixture passes iff it exits 0) and the architect never saw it. Given a contract
|
|
1084
1087
|
// that said only "commands that drive this scope end-to-end", it wrote the scope's error paths
|
|
@@ -1163,11 +1166,13 @@ if (waves.length > 1 || excluded.added || ceiling < maxParallelScopes) {
|
|
|
1163
1166
|
`${ceiling < maxParallelScopes ? ` (the window is ${maxParallelScopes}; the substrate the contracts declared is what caps it, not the dial)` : ""}`);
|
|
1164
1167
|
}
|
|
1165
1168
|
|
|
1166
|
-
// Advisory lints at L1b. spec-lint is hard — a substrate overlap makes parallel builds unsafe
|
|
1167
|
-
//
|
|
1169
|
+
// Advisory lints at L1b. spec-lint is hard — a substrate overlap makes parallel builds unsafe, and
|
|
1170
|
+
// a breadboard Place with no screen builds the wrong thing; trace-lint stays advisory until
|
|
1171
|
+
// `covers:` is populated; hill-derive is a projection. The abort names no cause of its own: spec-lint
|
|
1172
|
+
// has more than one kind of red, and the detail says which.
|
|
1168
1173
|
const specLint = await cmd(`verify spec --slug ${slug}`, "MapScopes", "spec-lint");
|
|
1169
1174
|
if (!specLint.ok) {
|
|
1170
|
-
return aborted("L1b", `spec-lint reported
|
|
1175
|
+
return aborted("L1b", `spec-lint reported red findings before BUILD: ${specLint.detail || `exit ${specLint.exit_code}`}`);
|
|
1171
1176
|
}
|
|
1172
1177
|
await advisory(`verify trace --slug ${slug} --quiet`, "MapScopes", "trace-lint");
|
|
1173
1178
|
await advisory(`reduce hill --slug ${slug}`, "MapScopes", "hill-derive");
|