shapeup-sdlc 1.7.0 → 3.0.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.
Files changed (98) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +17 -3
  4. package/README.md +124 -103
  5. package/SECURITY.md +37 -30
  6. package/bin/init.mjs +77 -47
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +7 -6
  9. package/commands/ship.md +29 -16
  10. package/commands/wire.md +1 -1
  11. package/hooks/dispatch-receipt.mjs +195 -0
  12. package/hooks/gate-intake.mjs +3 -3
  13. package/hooks/gate-zerowork.mjs +25 -24
  14. package/hooks/hooks.json +9 -48
  15. package/hooks/lib/decision.mjs +19 -8
  16. package/hooks/safety-spine.mjs +3 -3
  17. package/hooks/sandbox-guard.mjs +84 -40
  18. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +283 -41
  19. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +58 -14
  20. package/kernel/harness.mjs +134 -0
  21. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +15 -12
  22. package/{skills/tech-lead/scripts/init-run.mjs → kernel/init/run.mjs} +152 -31
  23. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  24. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +251 -7
  25. package/kernel/lib/paths.mjs +491 -0
  26. package/kernel/probe/concurrency.mjs +510 -0
  27. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +9 -10
  28. package/kernel/probe/eval.mjs +77 -0
  29. package/kernel/probe/leg.mjs +125 -0
  30. package/{skills/tech-lead/scripts/resume-state.mjs → kernel/probe/resume.mjs} +204 -63
  31. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +93 -11
  32. package/kernel/probe/t0.mjs +66 -0
  33. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +99 -23
  34. package/kernel/reduce/graph.mjs +437 -0
  35. package/{skills/tech-lead/scripts/hill-derive.mjs → kernel/reduce/hill.mjs} +28 -9
  36. package/kernel/reduce/ingest.mjs +633 -0
  37. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  38. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +61 -13
  39. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +21 -15
  40. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +13 -7
  41. package/kernel/report/export.mjs +325 -0
  42. package/kernel/report/facts.mjs +347 -0
  43. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +15 -12
  44. package/kernel/verify/dispatch.mjs +114 -0
  45. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  46. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  47. package/kernel/verify/skills.mjs +125 -0
  48. package/kernel/verify/spec.mjs +559 -0
  49. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +134 -24
  50. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +26 -11
  51. package/oracles/http-oracle.mjs +1 -1
  52. package/oracles/process-oracle.mjs +1 -1
  53. package/oracles/snapshot-oracle.mjs +1 -1
  54. package/oracles/test-oracle.mjs +1 -1
  55. package/package.json +11 -9
  56. package/skills/ba-pitch-analyzer/SKILL.md +5 -5
  57. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  58. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  59. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  60. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  61. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  62. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  63. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  64. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  65. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  66. package/skills/ba-pitch-analyzer/references/doc-schemas.md +2 -0
  67. package/skills/ba-pitch-analyzer/references/task-generation.md +2 -2
  68. package/skills/qa-edge-hunter/SKILL.md +7 -3
  69. package/skills/scope-architect/SKILL.md +40 -6
  70. package/skills/solution-architect/SKILL.md +19 -4
  71. package/skills/spec-evaluator/SKILL.md +12 -2
  72. package/skills/task-executor/SKILL.md +7 -6
  73. package/skills/tech-lead/SKILL.md +50 -38
  74. package/skills/tech-lead/references/gates.md +69 -20
  75. package/skills/tech-lead/references/protocol.md +832 -0
  76. package/skills/tech-lead/schemas/domain.schema.json +341 -66
  77. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  78. package/skills/tech-lead/schemas/work-result.schema.json +1 -1
  79. package/skills/tech-lead/workflows/shapeup-run.js +1313 -750
  80. package/hooks/anti-rationalization.mjs +0 -238
  81. package/hooks/compact-snapshot.mjs +0 -47
  82. package/hooks/gate-deadline.mjs +0 -147
  83. package/hooks/gate-l2.mjs +0 -161
  84. package/hooks/session-rehydrate.mjs +0 -108
  85. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  86. package/skills/spec-evaluator/README.md +0 -93
  87. package/skills/tech-lead/README.md +0 -71
  88. package/skills/tech-lead/references/delegation.md +0 -261
  89. package/skills/tech-lead/references/hard-rules.md +0 -34
  90. package/skills/tech-lead/references/invocation.md +0 -45
  91. package/skills/tech-lead/references/ledger-schema.md +0 -213
  92. package/skills/tech-lead/references/round-protocol.md +0 -234
  93. package/skills/tech-lead/references/state-model.md +0 -66
  94. package/skills/tech-lead/scripts/ingest-result.mjs +0 -258
  95. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -81
  96. package/skills/tech-lead/scripts/lib/paths.mjs +0 -280
  97. package/skills/tech-lead/scripts/run-workflow.mjs +0 -381
  98. package/skills/translator/README.md +0 -66
@@ -1,108 +0,0 @@
1
- #!/usr/bin/env node
2
- // Session rehydrate — SessionStart hook, matcher "startup|compact|resume|clear".
3
- //
4
- // Fires when a session begins with a harness run already in flight, and injects the RunSnapshot's
5
- // rehydrate_hint as additionalContext: re-derive round/attempt/hill from the files, never from
6
- // memory. This is the reflex that makes the file-first run-state load-bearing at the exact moment
7
- // the in-context copy is absent or degraded: without it the orchestrator can re-dispatch an
8
- // already-ingested order, miscount attempts off a lossy summary, or — measured, below — re-open a
9
- // run that is already open and rebuild the whole pipeline from phase 1.
10
- //
11
- // v1.4.1 — `startup` ADDED TO THE MATCHER, and this is a measured correction, not a widening.
12
- //
13
- // The matcher was `compact|resume`. Both of those are continuations of a conversation that still
14
- // exists. The commonest continuity event in real use has neither: you close the terminal and come
15
- // back tomorrow, or a teammate picks the work up in a fresh checkout. The CLI reports that as
16
- // `SessionStart:startup`, and the reflex whose entire purpose is "trust the files, not your
17
- // memory" did not fire in the one case where there IS no memory to distrust.
18
- //
19
- // The cost is concrete. A fresh session in a workspace where a prior one was cut mid-build sees
20
- // only `SessionStart:startup` — the plugin's load echo, not this hook. With no pointer to the run
21
- // in flight, the orchestrator re-enters at phase 1 and spends its whole budget rebuilding a
22
- // pipeline that is already on disk, closing none of the gap, while the run receipt and board sit
23
- // there the entire time. A session can reach GATE L4 — ship sign-off — having advanced the
24
- // deliverable by zero criteria.
25
- //
26
- // Firing on `startup` is free when there is nothing to say: `findRun` returns a run only for an
27
- // `active-scope` pointer or a `harness-run.md` whose status is mid-run, so an ordinary session in a
28
- // repo with no run — or with a shipped one — gets silence and exit 0, exactly as before.
29
- //
30
- // `clear` is included for the same reason: it discards the conversation and keeps the workspace,
31
- // which is the same problem wearing a different name.
32
- //
33
- // Derivation is always FRESH (files beat any persisted copy); the run-snapshot.json written
34
- // by hooks/compact-snapshot.mjs is only the fallback if live derivation throws. Fail-open:
35
- // no active run, or any error → silent exit 0.
36
- //
37
- // Contract: SessionStart stdin JSON { cwd, source: "startup"|"resume"|"clear"|"compact" }.
38
- // Inject via { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } }.
39
-
40
- import { readFileSync } from "node:fs";
41
- import { deriveSnapshot, snapshotPath } from "../skills/tech-lead/scripts/run-snapshot.mjs";
42
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
43
- import { runHook, readStdin } from "./lib/decision.mjs";
44
- import { activeScope } from "../skills/tech-lead/scripts/lib/paths.mjs";
45
-
46
- // The snapshot's own hint says "trust the files, not the conversation summary" — correct after a
47
- // compaction, and slightly wrong on a cold start, where there is no summary and no conversation
48
- // either. The two sources also fail in DIFFERENT directions, so they need different first sentences:
49
- //
50
- // compact / resume — the risk is acting on a lossy summary of work you remember doing.
51
- // startup / clear — the risk is not knowing the run exists, and OPENING IT AGAIN. That is the
52
- // failure that actually happens: a fresh session re-enters at phase 1 and
53
- // spends its budget rebuilding a pipeline that was already on disk.
54
- //
55
- // Naming the right failure is the whole value of the injection. A generic pointer to the files is
56
- // what a competent agent finds anyway; "there is a run open, do not re-open it" is not.
57
- function lead(source) {
58
- if (source === "startup" || source === "clear") {
59
- return "A shapeup-sdlc run is ALREADY OPEN in this workspace and you have no memory of it. " +
60
- "Do NOT open a new run, re-run intake, or restart the pipeline from phase 1 — the receipt, " +
61
- "the board and the ledger already exist. RESUME from the phase the files say the run is in. ";
62
- }
63
- return "";
64
- }
65
-
66
- async function main() {
67
- await runHook("session-rehydrate", async () => {
68
- const raw = await readStdin();
69
- let p;
70
- try { p = JSON.parse(raw || "{}"); }
71
- catch (e) { return { verdict: "error", event: "SessionStart", reason: `unparseable payload: ${e.message}` }; }
72
- const cwd = p.cwd || process.cwd();
73
-
74
- let snapshot = null;
75
- let source = "live";
76
- try {
77
- snapshot = deriveSnapshot(cwd);
78
- } catch {
79
- // Live derivation failed → fall back to the pre-compaction anchor, best effort.
80
- source = "anchor";
81
- try {
82
- const pointer = JSON.parse(readFileSync(activeScope(cwd), "utf8"));
83
- if (pointer?.slug) snapshot = JSON.parse(readFileSync(snapshotPath(cwd, pointer.slug), "utf8"));
84
- } catch { /* no anchor either → stay silent */ }
85
- }
86
- if (!snapshot?.rehydrate_hint) {
87
- return { verdict: "allow", event: "SessionStart", cwd, subject: p.source ?? null, reason: "no active run — nothing to rehydrate" };
88
- }
89
-
90
- return {
91
- verdict: "allow", event: "SessionStart", cwd, rule: `injected:${source}`,
92
- subject: snapshot.slug ?? p.source ?? null,
93
- reason: `rehydrate_hint injected on SessionStart:${p.source ?? "?"}`,
94
- // additionalContext is not a deny, but it IS output, so it rides the same channel.
95
- payload: {
96
- hookSpecificOutput: {
97
- hookEventName: "SessionStart",
98
- additionalContext: `${lead(p.source)}${snapshot.rehydrate_hint}\n${JSON.stringify(snapshot)}`,
99
- },
100
- },
101
- emit: true,
102
- };
103
- });
104
- }
105
-
106
- if (isMain(import.meta.url)) {
107
- main();
108
- }
@@ -1,239 +0,0 @@
1
- #!/usr/bin/env node
2
- // Spec lint (pure-skill architecture v1.0, plan §8.2).
3
- //
4
- // The mechanical half of the old ba-pitch-analyzer Phase 7a self-audit + Phase 7c parse steps
5
- // + Phase 6b PA1/PA2 lints — checkbox walking and glob checks a model should never grade on
6
- // its own output (a worker grading itself was always a judge-purity smell):
7
- //
8
- // PA1 a scope substrate aligned 1:1 with a single top-level directory (directory-thinking)
9
- // PA2 a scope substrate resolving to more than the size cap (~15 files)
10
- // DISJOINT a path matched by two scopes' allowed_file_substrate without BOTH declaring it
11
- // in shared_substrate
12
- // STRUCTURE spec tree completeness (usecases/ ≥1 UC, domain-model, UC ## Steps),
13
- // unresolved wikilinks, task frontmatter completeness, unlocks edge-symmetry,
14
- // depends_on referencing unknown tasks
15
- // TIER-DIRECTION a committed (SHARED) spec doc wikilinking the LOCAL board ([[tasks/...]]).
16
- // Persisted links flow LOCAL→SHARED only: task ids are machine-local (boards
17
- // regenerate and renumber) and .shapeup/ is gitignored — a committed task
18
- // link dangles on every fresh clone. Cite the UC or scope_id instead.
19
- // UC-ANCHOR a task whose use_case_refs is empty or names a UC with no usecases/UC-*.md —
20
- // the LOCAL→SHARED anchor must be complete (single-anchor rule; SPIKE/CHORE/
21
- // DOCS/MIGRATION tasks anchor elsewhere and are exempt)
22
- //
23
- // Zero dependencies (glob matcher inlined from hooks/sandbox-guard.mjs). Judgment stays in the skill
24
- // (gap severity, lens choice); this script only reports facts.
25
- //
26
- // Usage: node skills/ba-pitch-analyzer/scripts/spec-lint.mjs --slug <slug> [--cwd <dir>]
27
- // Prints a JSON report. Exit 0 = no red findings, 1 = at least one red.
28
-
29
- import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
30
- import { resolve, join, relative } from "node:path";
31
- import { parseBoard, deriveUnlocks } from "./board-derive.mjs";
32
- import { isMain } from "../../tech-lead/scripts/lib/is-main.mjs";
33
- import { runArgs } from "../../tech-lead/scripts/lib/argv.mjs";
34
- import { LOCAL } from "../../tech-lead/scripts/lib/paths.mjs";
35
- import { specDir, scopesDir, tasksDir } from "../../tech-lead/scripts/lib/paths.mjs";
36
- import { readAllContracts, unreadableReason, SCOPE_CONTRACT } from "../../tech-lead/scripts/lib/contract-md.mjs";
37
-
38
- // Inlined from hooks/sandbox-guard.mjs so this skill ships self-contained (a skill's scripts
39
- // must not reach outside its own folder — channels that copy only skills/ would dangle).
40
- /**
41
- * Compile a substrate glob into an anchored RegExp (inlined from sandbox-guard so the skill ships
42
- * self-contained). Supports single-star, double-star, and double-star-slash segment wildcards.
43
- * @param {string} glob - The glob pattern.
44
- * @returns {RegExp} A full-string (`^…$`) matcher for repo-relative paths.
45
- */
46
- export function globToRegExp(glob) {
47
- let re = "";
48
- for (let i = 0; i < glob.length; i++) {
49
- const c = glob[i];
50
- if (c === "*") {
51
- if (glob[i + 1] === "*") {
52
- re += glob[i + 2] === "/" ? "(?:[^/]+/)*" : ".*";
53
- i += glob[i + 2] === "/" ? 2 : 1;
54
- } else re += "[^/]*";
55
- } else if ("\\^$.|?+()[]{}".includes(c)) re += "\\" + c;
56
- else re += c;
57
- }
58
- return new RegExp(`^${re}$`);
59
- }
60
-
61
- const SIZE_CAP = 15;
62
-
63
- /**
64
- * Recursively list repo-relative file paths under a root, skipping .git/node_modules/.shapeup.
65
- * @param {string} root - The base the results are made relative to.
66
- * @param {string} [dir=root] - Current directory being walked (callers omit it).
67
- * @param {string[]} [acc=[]] - Accumulator (callers omit it).
68
- * @returns {string[]} Repo-relative paths of every file found.
69
- */
70
- function walkFiles(root, dir = root, acc = []) {
71
- for (const e of readdirSync(dir, { withFileTypes: true })) {
72
- if (e.name === ".git" || e.name === "node_modules" || e.name === LOCAL) continue;
73
- const p = join(dir, e.name);
74
- if (e.isDirectory()) walkFiles(root, p, acc);
75
- else acc.push(relative(root, p));
76
- }
77
- return acc;
78
- }
79
-
80
- /**
81
- * Lint scope contracts for PA1 (directory-thinking), PA2 (size cap), and DISJOINT substrate overlap.
82
- * @param {Array<{scope_id:string, topology_type?:string, allowed_file_substrate?:string[],
83
- * shared_substrate?:string[]}>} scopes - The scope contracts.
84
- * @param {string[]} repoFiles - Repo-relative file list the substrate globs resolve against.
85
- * @returns {Array<{rule:string, level:("red"|"warn"), scope:string, detail:string}>} Findings; [] when clean.
86
- */
87
- export function lintScopes(scopes, repoFiles) {
88
- const findings = [];
89
- /**
90
- * Resolve a list of substrate globs to the repo files they match.
91
- * @param {(string[]|undefined)} globs - Glob patterns (undefined → none).
92
- * @returns {string[]} The repo-relative files matched by any glob (deduplication is the caller's).
93
- */
94
- const resolveGlobs = (globs) => {
95
- const res = (globs || []).map((g) => globToRegExp(g));
96
- return repoFiles.filter((f) => res.some((r) => r.test(f)));
97
- };
98
- for (const s of scopes) {
99
- const allowed = s.allowed_file_substrate || [];
100
- // PA1 — directory-thinking: every glob confined to ONE layer directory (e.g. all of it
101
- // under apps/web/). A flow slice crosses layers (apps/web/cart + apps/api/cart passes).
102
- const layers = new Set(allowed.map((g) => g.split("/").slice(0, 2).join("/")));
103
- if (allowed.length && layers.size === 1 && s.topology_type !== "CHOWDER") {
104
- findings.push({ rule: "PA1", level: "red", scope: s.scope_id, detail: `substrate aligns 1:1 with '${[...layers][0]}/' — slice by flow, not by directory` });
105
- }
106
- // PA2 — resolved file count over the cap (chowder absorbs true strays).
107
- const files = resolveGlobs(allowed);
108
- if (files.length > SIZE_CAP && s.topology_type !== "CHOWDER") {
109
- findings.push({ rule: "PA2", level: "warn", scope: s.scope_id, detail: `substrate resolves to ${files.length} files (cap ~${SIZE_CAP}) — consider splitting` });
110
- }
111
- }
112
- // DISJOINT — pairwise overlap not covered by BOTH scopes' shared_substrate.
113
- for (let i = 0; i < scopes.length; i++) {
114
- for (let j = i + 1; j < scopes.length; j++) {
115
- const a = scopes[i], b = scopes[j];
116
- const filesA = new Set(resolveGlobs(a.allowed_file_substrate));
117
- const overlap = resolveGlobs(b.allowed_file_substrate).filter((f) => filesA.has(f));
118
- const sharedA = (a.shared_substrate || []).map(globToRegExp);
119
- const sharedB = (b.shared_substrate || []).map(globToRegExp);
120
- for (const f of overlap) {
121
- const declared = sharedA.some((r) => r.test(f)) && sharedB.some((r) => r.test(f));
122
- if (!declared) findings.push({ rule: "DISJOINT", level: "red", scope: `${a.scope_id}+${b.scope_id}`, detail: `${f} is in both substrates but not in both shared_substrate lists — PA3 waiting to happen` });
123
- }
124
- }
125
- }
126
- return findings;
127
- }
128
-
129
- /**
130
- * Lint spec-tree completeness, wikilink resolution, tier-direction, task frontmatter/graph
131
- * integrity (edge symmetry, dependency existence), and UC-anchor completeness.
132
- * @param {{specDir:string, tasks:Array<object>}} input - The SHARED spec dir and the parsed board.
133
- * @returns {Array<{rule:string, level:("red"|"warn"), detail:string}>} Findings; [] when clean.
134
- */
135
- export function lintStructure({ specDir, tasks }) {
136
- const findings = [];
137
- const ucDir = join(specDir, "usecases");
138
- if (!existsSync(join(specDir, "domain-model.md"))) findings.push({ rule: "STRUCTURE", level: "red", detail: "domain-model.md missing" });
139
- const ucs = existsSync(ucDir) ? readdirSync(ucDir).filter((f) => /^UC-.*\.md$/.test(f)) : [];
140
- if (!ucs.length) findings.push({ rule: "STRUCTURE", level: "red", detail: "usecases/ has no UC-*.md — nothing to build or grade against" });
141
- for (const f of ucs) {
142
- const body = readFileSync(join(ucDir, f), "utf8");
143
- if (!/^##\s+Steps/m.test(body)) findings.push({ rule: "STRUCTURE", level: "warn", detail: `${f} has no ## Steps section` });
144
- }
145
- // Wikilinks in spec docs must resolve within the spec dir — and never cross the tier
146
- // boundary: a SHARED doc linking the LOCAL board is the wrong direction by construction.
147
- const specFiles = existsSync(specDir) ? walkFiles(specDir) : [];
148
- const names = new Set(specFiles.map((f) => f.replace(/\.md$/, "")));
149
- for (const f of specFiles.filter((x) => x.endsWith(".md"))) {
150
- const body = readFileSync(join(specDir, f), "utf8");
151
- for (const m of body.matchAll(/\[\[([^\]#|]+)/g)) {
152
- const target = m[1].trim().replace(/\.md$/, "");
153
- if (target.startsWith("tasks/")) {
154
- findings.push({ rule: "TIER-DIRECTION", level: "red", detail: `${f} → [[${m[1].trim()}]] links the LOCAL board from a committed doc — links flow LOCAL→SHARED only; cite the UC or scope_id instead (task ids renumber per machine)` });
155
- continue;
156
- }
157
- if (!names.has(target) && ![...names].some((n) => n.endsWith(`/${target}`) || n === target)) {
158
- findings.push({ rule: "WIKILINK", level: "warn", detail: `${f} → [[${m[1].trim()}]] unresolved in spec dir` });
159
- }
160
- }
161
- }
162
- // Task frontmatter + graph integrity (edge symmetry — the hand-authored-unlocks drift, mechanized).
163
- const ids = new Set(tasks.map((t) => t.id));
164
- const derived = deriveUnlocks(tasks);
165
- for (const t of tasks) {
166
- for (const k of ["id", "status"]) if (!t[k] || t[k] === "unknown") findings.push({ rule: "TASK", level: "red", detail: `${t.file} missing frontmatter ${k}` });
167
- for (const d of t.depends_on) if (!ids.has(d)) findings.push({ rule: "TASK", level: "red", detail: `${t.id} depends_on ${d} which does not exist` });
168
- if (JSON.stringify([...t.unlocks].sort()) !== JSON.stringify(derived[t.id] || [])) {
169
- findings.push({ rule: "EDGE-SYMMETRY", level: "red", detail: `${t.id} unlocks ${JSON.stringify(t.unlocks)} ≠ derived inverse ${JSON.stringify(derived[t.id])} — run board-derive.mjs --write` });
170
- }
171
- }
172
- // UC-ANCHOR — the LOCAL→SHARED anchor must be complete: every implementation task names
173
- // ≥1 UC (single-anchor rule, task-generation.md) and each named UC exists on disk.
174
- // SPIKE/CHORE/DOCS/MIGRATION tasks anchor elsewhere (api_ref / linked_docs) — exempt.
175
- const ucIds = new Set(ucs.map((f) => f.replace(/\.md$/, "")));
176
- const anchorExempt = new Set(["spike", "chore", "docs", "migration"]);
177
- for (const t of tasks) {
178
- if (anchorExempt.has((t.type || "").toLowerCase())) continue;
179
- const refs = t.use_case_refs || [];
180
- if (!refs.length) {
181
- findings.push({ rule: "UC-ANCHOR", level: "red", detail: `${t.id} has empty use_case_refs — every task must anchor into the committed spec (LOCAL→SHARED, single-anchor rule)` });
182
- continue;
183
- }
184
- for (const r of refs) {
185
- const ucId = r.replace(/^\[\[|\]\]$/g, "").replace(/^usecases\//, "");
186
- if (!ucIds.has(ucId)) findings.push({ rule: "UC-ANCHOR", level: "red", detail: `${t.id} use_case_refs "${r}" does not resolve to usecases/${ucId}.md` });
187
- }
188
- }
189
- return findings;
190
- }
191
-
192
- /**
193
- * Run the full spec lint (scopes + structure) for a slug.
194
- * @param {{cwd:string, slug:string}} opts - Working root and feature slug.
195
- * @returns {{slug:string, scopes:number, tasks:number, red:number, warn:number,
196
- * findings:Array<object>}} Counts and the combined findings from {@link lintScopes} and
197
- * {@link lintStructure}.
198
- */
199
- export function lint({ cwd, slug }) {
200
- const specRoot = specDir(cwd, slug);
201
- const contracts = readAllContracts(scopesDir(cwd, slug), SCOPE_CONTRACT);
202
- const scopes = contracts.map((c) => c.contract);
203
- const tasks = parseBoard(tasksDir(cwd, slug));
204
- const repoFiles = walkFiles(cwd);
205
- const findings = [
206
- // A contract whose table this parser cannot see reads as a contract that declared no
207
- // table, and every rule below then passes for the part it could not read. Loud, not empty.
208
- ...contracts
209
- .map(({ contract, path }) => ({ reason: unreadableReason(contract), scope: contract.scope_id || path }))
210
- .filter((x) => x.reason)
211
- .map((x) => ({ rule: "CONTRACT-UNREADABLE", level: "red", scope: x.scope, detail: `${x.reason} — the rules below could not check what they could not read` })),
212
- ...lintScopes(scopes, repoFiles),
213
- ...lintStructure({ specDir: specRoot, tasks }),
214
- ];
215
- return {
216
- slug,
217
- scopes: scopes.length,
218
- tasks: tasks.length,
219
- red: findings.filter((f) => f.level === "red").length,
220
- warn: findings.filter((f) => f.level === "warn").length,
221
- findings,
222
- };
223
- }
224
-
225
- /** The typed argv contract (see `skills/tech-lead/scripts/lib/argv.mjs`). */
226
- export const ARGV_SPEC = {
227
- usage: "spec-lint.mjs --slug <slug> [--cwd <dir>]",
228
- _: { arity: 0, max: 0, name: "(no positional operands)" },
229
- slug: { type: "str", required: true },
230
- cwd: { type: "path" },
231
- };
232
-
233
- const isMainModule = isMain(import.meta.url);
234
- if (isMainModule) {
235
- const args = runArgs(ARGV_SPEC);
236
- const report = lint({ cwd: resolve(args.cwd || process.cwd()), slug: args.slug });
237
- console.log(JSON.stringify(report, null, 2));
238
- process.exit(report.red > 0 ? 1 : 0);
239
- }
@@ -1,93 +0,0 @@
1
- # spec-evaluator
2
-
3
- The **judge** in a planner → generator → evaluator harness. Pairs with `task-executor`
4
- (generator) and `ba-pitch-analyzer` (planner). Reads the same spec tree, exercises the
5
- **running** app, returns a hard-threshold verdict + file:line bug list, and hands bugs back
6
- to the generator. Skeptical by default; never marks a task `done`.
7
-
8
- v0.1 evaluates one dimension only — **spec-conformance** (AC + Done-when + contract shapes +
9
- non-go). Security / performance ship disabled, injectable later with zero core changes.
10
-
11
- ---
12
-
13
- ## Resource map
14
-
15
- ```
16
- spec-evaluator/
17
- ├── SKILL.md # entry point — frontmatter, GATE V0–V3, flags, hard rules
18
- ├── README.md # this file
19
- └── references/
20
- ├── dimension-contract.md # ★ the injection interface every dimension implements
21
- ├── anti-leniency.md # skeptical posture — read before any verdict (GATE V2)
22
- ├── probing.md # Phase A — Playwright CLI + per-variant probe strategy
23
- ├── report-schema.md # Phase B — the EVAL-TASK-NNN.md handoff file
24
- └── dimensions/
25
- ├── _registry.md # which dimensions are active + how to add one
26
- ├── spec-conformance.md # ✅ the only enabled dimension (correctness)
27
- ├── security.md # ⛔ disabled stub — worked example of the contract
28
- └── performance.md # ⛔ disabled stub — worked example of the contract
29
- ```
30
-
31
- | Resource | Loaded at | Purpose |
32
- |----------|-----------|---------|
33
- | `SKILL.md` | always | gate pipeline, invocation, authority rules |
34
- | `references/dimension-contract.md` | GATE V0.5 | the interface; validates each dimension before running it |
35
- | `references/dimensions/_registry.md` | GATE V0.5 | active set resolution (`--dimensions` overrides) |
36
- | `references/dimensions/spec-conformance.md` | per task | the default correctness dimension |
37
- | `references/dimensions/{security,performance}.md` | only if enabled | injectable stubs, off by default |
38
- | `references/anti-leniency.md` | before GATE V2 | absence of evidence = FAIL; banned phrases |
39
- | `references/probing.md` | Phase A | how to collect evidence (CLI > MCP, per variant) |
40
- | `references/report-schema.md` | Phase B | the file the generator reads next |
41
-
42
- ★ = the load-bearing file for "spec now, more dimensions later."
43
-
44
- ---
45
-
46
- ## Install
47
-
48
- ```bash
49
- # Project scope (shareable via version control) — recommended
50
- cp -r spec-evaluator <repo>/.claude/skills/
51
-
52
- # or user scope (all your projects)
53
- cp -r spec-evaluator ~/.claude/skills/
54
- ```
55
-
56
- Restart the session (or re-open the skills interface) so the new skill is discovered.
57
-
58
- ## Invoke
59
-
60
- ```bash
61
- /spec-evaluator --spec shapeup/checkout-vnpay/spec/ --task TASK-007 # default: spec-conformance
62
- /spec-evaluator --spec ... --task TASK-007.web # platform variant
63
- /spec-evaluator --spec ... --task TASK-007.be --browser none # backend-only, no browser
64
- /spec-evaluator --spec ... --task TASK-007 --dimensions spec-conformance,security # inject a dimension
65
- /spec-evaluator --spec ... --task TASK-007 --single-pass --auto # one end pass, skip sign-off
66
- ```
67
-
68
- ## Pipeline position
69
-
70
- ```
71
- ba-pitch-analyzer (planner) ─► task-executor (generator) ─► spec-evaluator (judge)
72
- ▲ │
73
- └──── bug list (EVAL-TASK-NNN) ◄─┘ on FAIL
74
- ```
75
- The evaluator writes `.shapeup/<slug>/evaluation/EVAL-<task_id>.md` (LOCAL run-trace root) and sets `eval_verdict` on the task.
76
- `task-executor` owns `status: done`. Judge and doer stay separate by design.
77
-
78
- ## Inject a new dimension (zero core edits)
79
-
80
- 1. Copy the "Minimal valid dimension" block from `references/dimension-contract.md` to
81
- `references/dimensions/<id>.md`; fill criteria, probes, threshold, bug template.
82
- 2. Set `enabled: true` in `references/dimensions/_registry.md` (or pass `--dimensions … ,<id>`).
83
- 3. Re-run. GATE V0.5 validates it against the contract and loads it; non-conforming files
84
- are skipped with a warning, never half-run.
85
-
86
- `applies_to` scopes a dimension to a lens / package / platform variant, so a `visual`
87
- dimension can run only on `.web` tasks and `security` only on `.be`/`.shared`.
88
-
89
- ---
90
-
91
- ## Version
92
- 0.1 — initial template. GATE V0–V3, skeptical-by-default, single enabled dimension,
93
- pluggable dimension contract, Playwright CLI probing, file:line bug handoff.
@@ -1,71 +0,0 @@
1
- # tech-lead
2
-
3
- The orchestrator over the harness. Acts as the tech lead that runs a feature end-to-end
4
- across three skills, makes round decisions, and reports to the PO at gates. It is **thin** —
5
- it sequences and decides, it does not plan, build, or judge itself.
6
-
7
- ```
8
- ba-pitch-analyzer (planner) → task-executor (generator) → spec-evaluator (judge)
9
- PLAN BUILD (loop all tasks) EVALUATE (once / round)
10
- ▲ │
11
- └──── bugs, round r+1 ◄────────┘ on FAIL
12
- ```
13
-
14
- **Core rule:** the evaluator runs exactly once per build round, only after the task board
15
- is 100% done — never per task. That single end-of-round QA pass is the long-running harness
16
- V2 lesson, and enforcing its timing is the reason this skill exists.
17
-
18
- ## Resource map
19
- ```
20
- tech-lead/
21
- ├── SKILL.md # entry — GATE L0–L4, the round loop, flags, hard rules
22
- ├── README.md # this file
23
- └── references/
24
- ├── round-protocol.md # ★ the loop: r=1 vs r>1, stop conditions, eval timing rule
25
- ├── delegation.md # how each of the 3 sub-skills is invoked + handoff files
26
- └── ledger-schema.md # harness-run.md — the round table / decisions / escalation
27
- ```
28
-
29
- | Resource | Loaded at | Purpose |
30
- |----------|-----------|---------|
31
- | `SKILL.md` | always | L-gates, phase sequence, invocation, authority rules |
32
- | `references/round-protocol.md` | BUILD/EVAL | the loop semantics + the "eval once at end" rule |
33
- | `references/delegation.md` | each phase | exact sub-skill commands + which handoff files to read |
34
- | `references/ledger-schema.md` | throughout | the run ledger that carries state across rounds/sessions |
35
-
36
- ## Install
37
- ```bash
38
- cp -r tech-lead <repo>/.claude/skills/ # or ~/.claude/skills/
39
- ```
40
- Requires the harness skills installed: `ba-pitch-analyzer`, `task-executor`,
41
- `spec-evaluator`, plus `translator` (the GATE L0 language gate for non-English intake).
42
- The EVAL phase uses spec-evaluator's feature-level pass
43
- (`--feature <slug>`) — see the dependency note in `references/delegation.md`.
44
-
45
- ## Invoke
46
- ```bash
47
- /tech-lead --pitch shapeup/checkout/shaping/shaping.md --spec shapeup/checkout/spec/ --lens standard
48
- /tech-lead --pitch ... --spec ... --auto # sub-skills unattended; pause at L1/L3/L4
49
- /tech-lead --pitch ... --spec ... --unattended --max-rounds 3 # headless / CI (Agent SDK)
50
- /tech-lead --spec shapeup/checkout/spec/ --from build # resume an existing run
51
- /tech-lead --pitch ... --spec ... --no-eval # skip eval for a trivial feature
52
- ```
53
-
54
- ## Gate map
55
- | Gate | When | Decision |
56
- |------|------|----------|
57
- | L0 | intake | language gate (`/translator --check`, translate if non-English) + run config: spec folder, lens, dims, max_rounds, auto level |
58
- | L1a | after ORIENT | PO reviews the spiked area + 🗻 Hill unknowns before mapping scopes |
59
- | L1b | after MAP SCOPES | PO accepts the task board (appetite guard) before any code |
60
- | L2 | after BUILD round | board 100% done? → the single EVAL pass (hook warns if not; advisory) |
61
- | L3 | after EVAL | PASS → /qa-edge-hunter pass → ship; FAIL → bug-only round r+1; max_rounds → escalate |
62
- | L4 | after SHIP | PO sign-off, close the ledger, harvest metrics to metrics.jsonl |
63
-
64
- ## Auto levels
65
- - **interactive** (default): pause at every L-gate; sub-skills keep their own gates.
66
- - **--auto**: sub-skills run unattended; tech lead still pauses at L1a / L1b / L3 / L4.
67
- - **--unattended**: auto-confirm all L-gates; stop only on PASS, max_rounds, or hard error.
68
- This is the headless mode for an Agent SDK / CI driver.
69
-
70
- ## Version
71
- 0.10 — Two-root workspace split (`shapeup/` vs `.shapeup/`), automated discovered-task reconciliation (`/ba-pitch-analyzer --tasks-only --from-discovered`), regression rule (touched UCs re-run), QA edge-hunter wiring (exploratory findings, triage at SHIP S.0), metrics harvest, and split L1a/L1b gates.