shapeup-sdlc 1.6.3 → 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 (118) hide show
  1. package/.claude/settings.local.example.json +5 -5
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +54 -107
  4. package/README.md +165 -151
  5. package/SECURITY.md +49 -27
  6. package/bin/init.mjs +93 -108
  7. package/bin/lib/grant.mjs +145 -0
  8. package/commands/build.md +20 -0
  9. package/commands/eval.md +5 -4
  10. package/commands/scopes.md +5 -4
  11. package/commands/shape.md +1 -1
  12. package/commands/ship.md +53 -7
  13. package/commands/wire.md +1 -1
  14. package/hooks/dispatch-receipt.mjs +195 -0
  15. package/hooks/gate-intake.mjs +16 -17
  16. package/hooks/gate-zerowork.mjs +107 -25
  17. package/hooks/hooks.json +9 -48
  18. package/hooks/lib/decision.mjs +38 -19
  19. package/hooks/safety-spine.mjs +4 -4
  20. package/hooks/sandbox-guard.mjs +130 -50
  21. package/{skills/tech-lead/scripts/compile-order.mjs → kernel/compile.mjs} +344 -55
  22. package/{skills/tech-lead/scripts/gate-answers.mjs → kernel/gate.mjs} +69 -26
  23. package/kernel/harness.mjs +134 -0
  24. package/{skills/tech-lead/scripts/fit-check.mjs → kernel/init/fit.mjs} +25 -22
  25. package/kernel/init/run.mjs +489 -0
  26. package/{skills/tech-lead/scripts → kernel}/lib/argv.mjs +52 -31
  27. package/{skills/tech-lead/scripts/lib/contract-md.mjs → kernel/lib/contract.mjs} +261 -17
  28. package/kernel/lib/paths.mjs +491 -0
  29. package/kernel/probe/concurrency.mjs +510 -0
  30. package/{skills/tech-lead/scripts/aegis-digest.mjs → kernel/probe/digest.mjs} +10 -11
  31. package/kernel/probe/eval.mjs +77 -0
  32. package/kernel/probe/leg.mjs +125 -0
  33. package/kernel/probe/resume.mjs +528 -0
  34. package/{skills/tech-lead/scripts → kernel/probe}/stats.mjs +102 -21
  35. package/kernel/probe/t0.mjs +66 -0
  36. package/{skills/ba-pitch-analyzer/scripts/board-derive.mjs → kernel/reduce/board.mjs} +104 -28
  37. package/kernel/reduce/graph.mjs +437 -0
  38. package/kernel/reduce/hill.mjs +152 -0
  39. package/kernel/reduce/ingest.mjs +633 -0
  40. package/{hooks/slop-cleaner.mjs → kernel/reduce/leftovers.mjs} +40 -57
  41. package/{skills/tech-lead/scripts/ship-report.mjs → kernel/reduce/ship.mjs} +93 -14
  42. package/{skills/tech-lead/scripts/run-snapshot.mjs → kernel/reduce/snapshot.mjs} +22 -21
  43. package/{skills/spec-evaluator/scripts/verdict-ledger.mjs → kernel/reduce/verdict.mjs} +14 -8
  44. package/kernel/report/export.mjs +325 -0
  45. package/kernel/report/facts.mjs +347 -0
  46. package/{skills/tech-lead/scripts/budget-check.mjs → kernel/verify/budget.mjs} +22 -25
  47. package/kernel/verify/dispatch.mjs +114 -0
  48. package/{skills/tech-lead/scripts/validate-envelope.mjs → kernel/verify/envelope.mjs} +20 -15
  49. package/{skills/tech-lead/scripts/lib → kernel/verify}/ratchet-tree.mjs +36 -14
  50. package/kernel/verify/skills.mjs +125 -0
  51. package/kernel/verify/spec.mjs +559 -0
  52. package/{skills/tech-lead/scripts/t0-verify.mjs → kernel/verify/t0.mjs} +138 -28
  53. package/{skills/tech-lead/scripts/trace-lint.mjs → kernel/verify/trace.mjs} +28 -13
  54. package/oracles/_shared.mjs +1 -1
  55. package/oracles/http-oracle.mjs +2 -2
  56. package/oracles/index.mjs +1 -1
  57. package/oracles/process-oracle.mjs +2 -2
  58. package/oracles/snapshot-oracle.mjs +2 -2
  59. package/oracles/test-oracle.mjs +2 -2
  60. package/package.json +11 -13
  61. package/skills/ba-pitch-analyzer/SKILL.md +24 -19
  62. package/skills/ba-pitch-analyzer/assets/templates/_index.tmpl.md +1 -1
  63. package/skills/ba-pitch-analyzer/assets/templates/api-feasibility.tmpl.md +12 -7
  64. package/skills/ba-pitch-analyzer/assets/templates/contracts/third-party-api.contract.tmpl.md +6 -3
  65. package/skills/ba-pitch-analyzer/assets/templates/cross-context/migration-plan.tmpl.md +5 -4
  66. package/skills/ba-pitch-analyzer/assets/templates/cross-context/team-handoff.tmpl.md +23 -13
  67. package/skills/ba-pitch-analyzer/assets/templates/scope-summary.tmpl.md +17 -12
  68. package/skills/ba-pitch-analyzer/assets/templates/synthesis.tmpl.md +17 -38
  69. package/skills/ba-pitch-analyzer/assets/templates/task.tmpl.md +3 -0
  70. package/skills/ba-pitch-analyzer/references/contract-patterns.md +8 -4
  71. package/skills/ba-pitch-analyzer/references/doc-schemas.md +3 -1
  72. package/skills/ba-pitch-analyzer/references/task-generation.md +8 -8
  73. package/skills/ba-pitch-analyzer/references/test-surface.md +1 -1
  74. package/skills/coach/SKILL.md +3 -3
  75. package/skills/orient/SKILL.md +2 -1
  76. package/skills/qa-edge-hunter/SKILL.md +15 -41
  77. package/skills/scope-architect/SKILL.md +57 -18
  78. package/skills/scope-hammer/SKILL.md +5 -5
  79. package/skills/shapeup/SKILL.md +3 -3
  80. package/skills/shapeup/resources/context-compaction.md +4 -3
  81. package/skills/solution-architect/SKILL.md +37 -15
  82. package/skills/spec-evaluator/SKILL.md +24 -7
  83. package/skills/spec-evaluator/references/dimensions/_registry.md +2 -2
  84. package/skills/spec-evaluator/references/dimensions/completeness.md +1 -1
  85. package/skills/spec-evaluator/references/dimensions/integration.md +98 -67
  86. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +4 -4
  87. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +1 -1
  88. package/skills/spec-evaluator/references/probing.md +42 -23
  89. package/skills/spec-evaluator/references/report-schema.md +2 -2
  90. package/skills/task-executor/SKILL.md +32 -18
  91. package/skills/tech-lead/SKILL.md +127 -438
  92. package/skills/tech-lead/references/gates.md +140 -49
  93. package/skills/tech-lead/references/protocol.md +832 -0
  94. package/skills/tech-lead/schemas/domain.schema.json +645 -198
  95. package/skills/tech-lead/schemas/gate-answers.schema.json +2 -2
  96. package/skills/tech-lead/schemas/work-order.schema.json +11 -2
  97. package/skills/tech-lead/schemas/work-result.schema.json +56 -18
  98. package/skills/tech-lead/workflows/shapeup-run.js +1474 -0
  99. package/skills/translator/SKILL.md +2 -2
  100. package/hooks/anti-rationalization.mjs +0 -244
  101. package/hooks/compact-snapshot.mjs +0 -47
  102. package/hooks/gate-deadline.mjs +0 -151
  103. package/hooks/gate-l2.mjs +0 -161
  104. package/hooks/session-rehydrate.mjs +0 -109
  105. package/skills/advisor-protocol/SKILL.md +0 -171
  106. package/skills/ba-pitch-analyzer/scripts/spec-lint.mjs +0 -239
  107. package/skills/spec-evaluator/README.md +0 -93
  108. package/skills/tech-lead/README.md +0 -71
  109. package/skills/tech-lead/references/delegation.md +0 -254
  110. package/skills/tech-lead/references/invocation.md +0 -45
  111. package/skills/tech-lead/references/ledger-schema.md +0 -214
  112. package/skills/tech-lead/references/round-protocol.md +0 -184
  113. package/skills/tech-lead/references/state-model.md +0 -66
  114. package/skills/tech-lead/scripts/ingest-result.mjs +0 -270
  115. package/skills/tech-lead/scripts/init-run.mjs +0 -326
  116. package/skills/tech-lead/scripts/lib/is-main.mjs +0 -82
  117. package/skills/tech-lead/scripts/lib/paths.mjs +0 -278
  118. package/skills/translator/README.md +0 -66
@@ -1,109 +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 SDD harness benchmark measured the cost. Its F4 handoff design is exactly this scenario — a
20
- // fresh `claude -p` in a workspace where a prior session was cut mid-build — and across three
21
- // Sonnet rows every one recorded `hooks_fired: ["SessionStart:startup"]` (the plugin's load echo,
22
- // not this hook). With no pointer to the run in flight, the orchestrator re-entered at phase 1:
23
- // 82–120 turns before its first write, $4.57–$10.36 for the session, and **0/3 of the gap closed**
24
- // while the run receipt and board sat on disk the whole time. One row reached GATE L4 — ship
25
- // sign-off — having advanced the deliverable by zero criteria.
26
- //
27
- // Firing on `startup` is free when there is nothing to say: `findRun` returns a run only for an
28
- // `active-scope` pointer or a `harness-run.md` whose status is mid-run, so an ordinary session in a
29
- // repo with no run — or with a shipped one — gets silence and exit 0, exactly as before.
30
- //
31
- // `clear` is included for the same reason: it discards the conversation and keeps the workspace,
32
- // which is the same problem wearing a different name.
33
- //
34
- // Derivation is always FRESH (files beat any persisted copy); the run-snapshot.json written
35
- // by hooks/compact-snapshot.mjs is only the fallback if live derivation throws. Fail-open:
36
- // no active run, or any error → silent exit 0.
37
- //
38
- // Contract: SessionStart stdin JSON { cwd, source: "startup"|"resume"|"clear"|"compact" }.
39
- // Inject via { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } }.
40
-
41
- import { readFileSync } from "node:fs";
42
- import { deriveSnapshot, snapshotPath } from "../skills/tech-lead/scripts/run-snapshot.mjs";
43
- import { isMain } from "../skills/tech-lead/scripts/lib/is-main.mjs";
44
- import { runHook, readStdin } from "./lib/decision.mjs";
45
- import { activeScope } from "../skills/tech-lead/scripts/lib/paths.mjs";
46
-
47
- // The snapshot's own hint says "trust the files, not the conversation summary" — correct after a
48
- // compaction, and slightly wrong on a cold start, where there is no summary and no conversation
49
- // either. The two sources also fail in DIFFERENT directions, so they need different first sentences:
50
- //
51
- // compact / resume — the risk is acting on a lossy summary of work you remember doing.
52
- // startup / clear — the risk is not knowing the run exists, and OPENING IT AGAIN. That is the
53
- // failure the benchmark measured: a fresh session re-entered at phase 1 and
54
- // spent 82–120 turns rebuilding a pipeline that was already on disk.
55
- //
56
- // Naming the right failure is the whole value of the injection. A generic pointer to the files is
57
- // what a competent agent finds anyway; "there is a run open, do not re-open it" is not.
58
- function lead(source) {
59
- if (source === "startup" || source === "clear") {
60
- return "A shapeup-sdlc run is ALREADY OPEN in this workspace and you have no memory of it. " +
61
- "Do NOT open a new run, re-run intake, or restart the pipeline from phase 1 — the receipt, " +
62
- "the board and the ledger already exist. RESUME from the phase the files say the run is in. ";
63
- }
64
- return "";
65
- }
66
-
67
- async function main() {
68
- await runHook("session-rehydrate", async () => {
69
- const raw = await readStdin();
70
- let p;
71
- try { p = JSON.parse(raw || "{}"); }
72
- catch (e) { return { verdict: "error", event: "SessionStart", reason: `unparseable payload: ${e.message}` }; }
73
- const cwd = p.cwd || process.cwd();
74
-
75
- let snapshot = null;
76
- let source = "live";
77
- try {
78
- snapshot = deriveSnapshot(cwd);
79
- } catch {
80
- // Live derivation failed → fall back to the pre-compaction anchor, best effort.
81
- source = "anchor";
82
- try {
83
- const pointer = JSON.parse(readFileSync(activeScope(cwd), "utf8"));
84
- if (pointer?.slug) snapshot = JSON.parse(readFileSync(snapshotPath(cwd, pointer.slug), "utf8"));
85
- } catch { /* no anchor either → stay silent */ }
86
- }
87
- if (!snapshot?.rehydrate_hint) {
88
- return { verdict: "allow", event: "SessionStart", cwd, subject: p.source ?? null, reason: "no active run — nothing to rehydrate" };
89
- }
90
-
91
- return {
92
- verdict: "allow", event: "SessionStart", cwd, rule: `injected:${source}`,
93
- subject: snapshot.slug ?? p.source ?? null,
94
- reason: `rehydrate_hint injected on SessionStart:${p.source ?? "?"}`,
95
- // additionalContext is not a deny, but it IS output, so it rides the same channel.
96
- payload: {
97
- hookSpecificOutput: {
98
- hookEventName: "SessionStart",
99
- additionalContext: `${lead(p.source)}${snapshot.rehydrate_hint}\n${JSON.stringify(snapshot)}`,
100
- },
101
- },
102
- emit: true,
103
- };
104
- });
105
- }
106
-
107
- if (isMain(import.meta.url)) {
108
- main();
109
- }
@@ -1,171 +0,0 @@
1
- ---
2
- name: advisor-protocol
3
- description: "Use this skill whenever a harness worker (task-executor, scope-architect) hits a decision it cannot make alone during Build Vertically — a design decision, a spec ambiguity, or a request to write outside its scope's substrate — and needs it adjudicated under a budget instead of guessed or asked ad hoc. Trigger on: \"escalate this decision\", \"ESCALATE\", \"adjudicate this ambiguity\", \"ask the advisor\", \"substrate expansion request\", \"resolve this design decision within budget\", \"how many escalations does this scope have left\". Also triggers when tech-lead needs to answer a worker's structured ESCALATE return during a build round."
4
- ---
5
-
6
- # Advisor Protocol (ESCALATE grammar + budgets)
7
-
8
- The mechanism behind Shape Up's "advisor, not micromanager" posture (design spec v1.1 DD-1,
9
- DD-8). A worker mid-attempt hits something it genuinely cannot decide alone — not "what should
10
- I name this variable" (that's Principle A, resolved silently or at the worker's own gate), but
11
- a decision whose wrong guess is expensive: a design trade-off the spec left open, a spec
12
- ambiguity that changes behavior, or a file the worker needs to touch outside its scope's
13
- `allowed_file_substrate`. Instead of guessing or opening an unbounded chat with the PO, the
14
- worker emits one structured `ESCALATE` return and this skill adjudicates it.
15
-
16
- **Why this exists as its own skill, not inline PO chat.** Zero-memory handoff (design spec
17
- §3.6) means every attempt gets a *fresh* context — no chat history. An answer given once must
18
- survive every future reset for that scope. This skill is what turns a transient Q&A into a
19
- persisted decision: it writes the answer to the round-ledger (committed, Tier A) the moment
20
- it's made, and every future brief for that scope reads it back. Escalation memory lives in
21
- files, not in chat (DD-8).
22
-
23
- ---
24
-
25
- ## The ESCALATE grammar
26
-
27
- A worker never asks a free-form question mid-attempt. It returns a structured block and stops
28
- work on the blocked AC/file until an answer comes back:
29
-
30
- ```yaml
31
- ESCALATE:
32
- scope_id: cart-creation
33
- round: 2
34
- attempt: 3
35
- kind: design-decision | spec-ambiguity | substrate-expansion
36
- question: "Contract leaves cart-total rounding unspecified — round half-up or banker's rounding?"
37
- options: ["round half-up (matches existing order-total code)", "banker's rounding (matches contract's other money fields)"]
38
- default_if_silent: "round half-up" # optional — see --unattended below
39
- ```
40
-
41
- Three kinds, three different resolution paths (below). `default_if_silent` is optional — a
42
- worker proposes it when it has a reasonable, low-risk fallback; the advisor is never forced to
43
- accept it.
44
-
45
- ---
46
-
47
- ## Budget
48
-
49
- **≤3 ESCALATEs per scope per round** (design spec §3.3). Counted per `scope_id` + `round`, reset
50
- each new round. The budget exists so a struggling scope surfaces as *stuck* (routed to the
51
- hill's stuck-split rule, ≥3 rounds at the same position → forced scope-architect split order) rather than
52
- draining PO attention one question at a time.
53
-
54
- ```
55
- 1st–3rd ESCALATE this scope/round → adjudicate normally (below).
56
- 4th+ → do NOT ask again. Apply the most conservative resolution
57
- (narrowest scope, no substrate expansion, closest precedent)
58
- and flag it as a GATE-H hammer proposal: "scope exceeded its
59
- escalation budget — question N unresolved, applied [X]".
60
- The worker's attempt continues with that resolution.
61
- ```
62
-
63
- Exceeding budget is never a hard stop — Build Vertically must keep moving; it's a signal that
64
- this scope needs GATE H's attention, not a excuse to freeze the round.
65
-
66
- ---
67
-
68
- ## Adjudication (in order — cheapest resolution wins)
69
-
70
- ```
71
- 1. Precedent check (free, no PO time):
72
- Search this run's round-ledger.md "Decisions" section (this scope AND sibling scopes) and
73
- shapeup/knowledge-base/*.md for an already-answered question that is the same or a
74
- clear superset of this one. Found → reuse verbatim, log "resolved by precedent: [ref]",
75
- do NOT count against budget (a repeat question is a zero-memory artifact, not a new decision).
76
-
77
- 2. kind: substrate-expansion:
78
- Never silently approved. Present the requested path(s) + why the worker says it needs them.
79
- PO/TL approves → dispatch a scope-architect remap order to add the path to the scope contract's
80
- `shared_substrate` (never hand-edit the contract — `ba` is its sole writer, F.5). PO/TL
81
- declines → worker re-plans within its existing substrate; log the decline as a decision too
82
- (prevents re-asking).
83
-
84
- 3. kind: design-decision | spec-ambiguity, interactive/--auto:
85
- Print the ESCALATE block verbatim + budget remaining for this scope/round. Ask (max 1):
86
- pick an option / provide a different answer / defer to default_if_silent if offered.
87
- Never answer on the PO's behalf — a wrong silent guess here is exactly what zero-memory
88
- handoff makes expensive to unwind later.
89
-
90
- 4. --unattended (no PO available):
91
- default_if_silent present → apply it, log "auto-resolved (default, unattended)".
92
- No default offered → apply the most conservative option (narrowest scope / least
93
- irreversible) and flag it as a GATE-H hammer proposal for PO review before ship — the
94
- run must not block, but an unattended guess is never allowed to hide from the PO forever.
95
- ```
96
-
97
- ---
98
-
99
- ## Persistence — the decision must survive a reset
100
-
101
- The instant an ESCALATE is answered (any path above), append one row to the run's committed
102
- `round-ledger.md` "Decisions" section — never batch it for later, never keep it only in the
103
- current session (a crash before the next promotion point would lose it):
104
-
105
- ```
106
- | Round | Scope | Kind | Question | Answer | Resolved by |
107
- |-------|-------|------|----------|--------|-------------|
108
- | 2 | cart-creation | design-decision | cart-total rounding | round half-up | PO (interactive) |
109
- ```
110
-
111
- Every subsequent isolated brief for that scope (task-executor's zero-memory handoff, design
112
- spec §3.6) includes the full decisions table for its `scope_id` — this is how an answer given
113
- once in round 2 is still known in round 5's fresh-context attempt without replaying any chat
114
- history.
115
-
116
- ---
117
-
118
- ## Envelope contract — the domain layer
119
-
120
- Orchestrated, this skill is dispatched like every worker: a **WorkOrder** in (`--order <path>`,
121
- operation `adjudicate`), a **WorkResult** out. The standalone flags below map 1:1 onto the
122
- payload fields registered for this worker in the central domain registry
123
- (`skills/tech-lead/schemas/domain.schema.json`, `x-payload-by-worker`):
124
-
125
- | Payload field | Standalone flag | Meaning |
126
- |---|---|---|
127
- | `payload.ledger` | `--ledger` | The run's committed `round-ledger.md` — every answer persists here |
128
- | `payload.escalate` | `--escalate` | The worker's ESCALATE block to adjudicate (the `EscalateBlock` grammar above) |
129
- | `payload.scope_id` | `--scope` | The scope the budget is counted against |
130
- | `payload.round` | `--round` | The round the budget/decision is counted against |
131
-
132
- The WorkResult may carry only `files_touched`, `artifacts`, `assumptions`, `deviations`
133
- (`x-result-by-worker`): the adjudicated Decision itself persists to the committed round-ledger,
134
- never to the envelope, so it survives every zero-memory reset.
135
-
136
- ---
137
-
138
- ## Invocation
139
-
140
- ```bash
141
- # Adjudicate one ESCALATE return from a worker (typical: tech-lead calls this mid-round)
142
- /advisor-protocol --ledger shapeup/checkout-vnpay/round-ledger.md --escalate '<ESCALATE yaml>'
143
-
144
- # Check remaining budget for a scope this round (worker or tech-lead, before emitting ESCALATE)
145
- /advisor-protocol --budget --scope cart-creation --round 2 --ledger shapeup/checkout-vnpay/round-ledger.md
146
-
147
- # Headless run — no PO available; apply defaults / conservative fallback, flag the rest for GATE H
148
- /advisor-protocol --unattended --ledger ... --escalate '<ESCALATE yaml>'
149
- ```
150
-
151
- ### Flags
152
- | Flag | Effect |
153
- |------|--------|
154
- | `--ledger <path>` | The run's committed `round-ledger.md` (decisions persist here) |
155
- | `--escalate <yaml\|path>` | The worker's ESCALATE block to adjudicate |
156
- | `--budget --scope <id> --round <N>` | Report remaining ESCALATE budget for a scope this round, no adjudication |
157
- | `--unattended` | No PO available — apply defaults/conservative fallback per rule 4 above |
158
-
159
- ---
160
-
161
- ## Hard Rules (never override without explicit user instruction)
162
-
163
- | Rule | Rationale |
164
- |------|-----------|
165
- | A worker never guesses a design decision or spec ambiguity silently | The whole point of ESCALATE — wrong silent guesses are the expensive failure mode zero-memory handoff amplifies |
166
- | Substrate-expansion is never auto-approved | PA3 (cross-scope contamination) exists precisely because agents self-justify "just this once" writes |
167
- | Budget is ≤3/scope/round; exceeding it never blocks the round | Signals a stuck scope to GATE L2's stuck-split rule instead of freezing Build Vertically |
168
- | Every answer is persisted to the committed round-ledger the instant it's given | Zero-memory handoff means the *only* place a decision survives is a file, not a session |
169
- | Precedent reuse never counts against budget | A repeat question is a zero-memory artifact, not a new decision — don't penalize the worker for context loss |
170
- | An unattended conservative resolution is always flagged for GATE H | An automated guess must stay visible to the PO before ship, never silently absorbed |
171
- | This skill never designs, builds, or judges | Advisor, not a fifth worker — it adjudicates, it does not decide scope or verify code |
@@ -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 mechanized KB-BA-001 check).
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
- // HD-001. 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.