From e512d9fcb0352440f22c2ef0ff9a3809c2e391c4 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:40:15 -0300 Subject: [PATCH 1/3] feat(prompts): make spec-driven workflow the default Bundle tlc-spec-driven with the CodeDeck plugin and apply its fallback contract across orchestrator modes. Co-Authored-By: Claude --- NOTICE | 14 +- plugin/agents/orchestrator-edit.md | 11 +- plugin/agents/orchestrator-read.md | 11 +- plugin/agents/orchestrator.md | 4 +- plugin/prompts/_partials/spec-gates.md | 2 + plugin/prompts/roles/orchestrator-edit.md | 1 + plugin/prompts/roles/orchestrator-read.md | 1 + plugin/prompts/roles/orchestrator.md | 2 +- plugin/skills/tlc-spec-driven/SKILL.md | 184 +++++++ .../references/code-analysis.md | 98 ++++ .../references/coding-principles.md | 72 +++ .../references/context-limits.md | 31 ++ .../tlc-spec-driven/references/design.md | 199 ++++++++ .../tlc-spec-driven/references/discuss.md | 159 ++++++ .../tlc-spec-driven/references/implement.md | 436 +++++++++++++++++ .../tlc-spec-driven/references/lessons.md | 115 +++++ .../tlc-spec-driven/references/memory.md | 144 ++++++ .../tlc-spec-driven/references/specify.md | 228 +++++++++ .../tlc-spec-driven/references/sub-agents.md | 147 ++++++ .../tlc-spec-driven/references/tasks.md | 451 ++++++++++++++++++ .../tlc-spec-driven/references/validate.md | 355 ++++++++++++++ .../tlc-spec-driven/scripts/check_commit.py | 115 +++++ .../skills/tlc-spec-driven/scripts/lessons.py | 412 ++++++++++++++++ .../tlc-spec-driven/scripts/validate_spec.py | 260 ++++++++++ .../tlc-spec-driven/scripts/validate_state.py | 162 +++++++ .../tlc-spec-driven/scripts/validate_tasks.py | 251 ++++++++++ .../orchestrator-agents.test.ts.snap | 26 +- tests/prompt-layers.test.ts | 6 +- tests/tlc-spec-driven.test.ts | 61 +++ 29 files changed, 3941 insertions(+), 17 deletions(-) create mode 100644 plugin/skills/tlc-spec-driven/SKILL.md create mode 100644 plugin/skills/tlc-spec-driven/references/code-analysis.md create mode 100644 plugin/skills/tlc-spec-driven/references/coding-principles.md create mode 100644 plugin/skills/tlc-spec-driven/references/context-limits.md create mode 100644 plugin/skills/tlc-spec-driven/references/design.md create mode 100644 plugin/skills/tlc-spec-driven/references/discuss.md create mode 100644 plugin/skills/tlc-spec-driven/references/implement.md create mode 100644 plugin/skills/tlc-spec-driven/references/lessons.md create mode 100644 plugin/skills/tlc-spec-driven/references/memory.md create mode 100644 plugin/skills/tlc-spec-driven/references/specify.md create mode 100644 plugin/skills/tlc-spec-driven/references/sub-agents.md create mode 100644 plugin/skills/tlc-spec-driven/references/tasks.md create mode 100644 plugin/skills/tlc-spec-driven/references/validate.md create mode 100644 plugin/skills/tlc-spec-driven/scripts/check_commit.py create mode 100644 plugin/skills/tlc-spec-driven/scripts/lessons.py create mode 100644 plugin/skills/tlc-spec-driven/scripts/validate_spec.py create mode 100644 plugin/skills/tlc-spec-driven/scripts/validate_state.py create mode 100644 plugin/skills/tlc-spec-driven/scripts/validate_tasks.py create mode 100644 tests/tlc-spec-driven.test.ts diff --git a/NOTICE b/NOTICE index 2f234bd..a08c56a 100644 --- a/NOTICE +++ b/NOTICE @@ -16,11 +16,11 @@ license text for CC-BY-4.0 is available at: What this means in practice --------------------------- -Our spec-driven workflow files in this repository are an original, -lean reimplementation written in our own words. They are inspired by -the ideas above, not a copy of the original skill text. No content -was pasted from the original SKILL.md or its reference files. +The plugin/skills/tlc-spec-driven/ tree bundles the original skill and +its supporting references and scripts from version 3.3.0. That tree is +distributed under the original CC-BY-4.0 license. -Where the CC-BY-4.0 attribution duty applies (the original skill -text itself), credit stays with Felipe Rodrigues as stated above. -Where our own files apply, the MIT License in LICENSE governs. +The prompt prose outside that tree is an original, lean fallback written +in CodeDeck's own words. It keeps the same workflow available to +harnesses that cannot load plugin skills, and the MIT License in LICENSE +governs that CodeDeck prose. diff --git a/plugin/agents/orchestrator-edit.md b/plugin/agents/orchestrator-edit.md index 7625dab..d602925 100644 --- a/plugin/agents/orchestrator-edit.md +++ b/plugin/agents/orchestrator-edit.md @@ -1,5 +1,5 @@ --- -# DO NOT EDIT: generated from roles/orchestrator-edit.md + partials (worktree, dispatch, proof, reports-dispatch). +# DO NOT EDIT: generated from roles/orchestrator-edit.md + partials (worktree, dispatch, proof, spec-gates, reports-dispatch). # Do not hand-edit; edit the manifest or partials and rebuild. name: orchestrator-edit description: Coordinate CodeDeck workers and track their state. @@ -20,6 +20,15 @@ tools: Read, Grep, Glob, Edit, Write, Bash - Verify before you claim. - Read `codedeck diff ` yourself before believing any worker. The artifact is authoritative, the success message is not. +## Spec gates + +- Treat `tlc-spec-driven` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. +- Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. +- Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. +- Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. +- Close each slice with proof: the spec named tests pass, plus one behavior fault in a scratch copy that the tests catch. Discard the scratch. Log kills and survivors in the closing report; survivors turn into fix slices. + ## Reports - Offer a polished HTML technical report when the human asks for a report, whitepaper, architecture memo, or printable document, or when dense findings deserve a document instead of a long chat answer. Ask once, then dispatch it. diff --git a/plugin/agents/orchestrator-read.md b/plugin/agents/orchestrator-read.md index 59d4730..624349c 100644 --- a/plugin/agents/orchestrator-read.md +++ b/plugin/agents/orchestrator-read.md @@ -1,5 +1,5 @@ --- -# DO NOT EDIT: generated from roles/orchestrator-read.md + partials (worktree, dispatch, proof, reports-dispatch). +# DO NOT EDIT: generated from roles/orchestrator-read.md + partials (worktree, dispatch, proof, spec-gates, reports-dispatch). # Do not hand-edit; edit the manifest or partials and rebuild. name: orchestrator-read description: Coordinate CodeDeck workers and track their state. @@ -20,6 +20,15 @@ tools: Read, Grep, Glob, Bash - Verify before you claim. - Read `codedeck diff ` yourself before believing any worker. The artifact is authoritative, the success message is not. +## Spec gates + +- Treat `tlc-spec-driven` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. +- Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. +- Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. +- Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. +- Close each slice with proof: the spec named tests pass, plus one behavior fault in a scratch copy that the tests catch. Discard the scratch. Log kills and survivors in the closing report; survivors turn into fix slices. + ## Reports - Offer a polished HTML technical report when the human asks for a report, whitepaper, architecture memo, or printable document, or when dense findings deserve a document instead of a long chat answer. Ask once, then dispatch it. diff --git a/plugin/agents/orchestrator.md b/plugin/agents/orchestrator.md index a0b783a..9c30143 100644 --- a/plugin/agents/orchestrator.md +++ b/plugin/agents/orchestrator.md @@ -22,6 +22,8 @@ tools: Bash ## Spec gates +- Treat `tlc-spec-driven` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. - Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. - Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. - Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. @@ -92,7 +94,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe - Drive the whole run without being asked for each phase. The human asked for the outcome once. Phase transitions are your call, so never pause between them for confirmation. - Size it from the request, then commit to the size. Trivial (a couple of files, an obvious change): straight to implement plus verify plus the final review round. Anything shaped like a feature: the full loop below. - Specify: dispatch a worker to write `.specs/features//spec.md` with the goal, the acceptance criteria, and what is out of scope. Design and Tasks go the same way when the work needs them: `design.md` for architecture calls, `tasks.md` for atomic tasks that each carry their Tests and Gate. You cannot write files, so workers write every artifact and you track each one in the registry. -- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name when its harness offers it, otherwise to follow the lean briefing steps exactly without pasting skill text. +- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name. When its harness does not expose the skill, the worker follows the lean briefing steps exactly without pasting skill text. - Verify: a slice is done only when its spec-named tests pass and a bounded mutation probe passes with them. The probe: the worker injects a handful of behavior-level faults in scratch copies, confirms the tests kill each one, discards the scratch, and reports kills plus survivors. Survivors become fix slices, not excuses. - Review: run the final round yourself with `codedeck run --role reviewer --no-worktree "" --bg --json` over the finished scope. Slice self-review never replaces it. Remediate every confirmed finding as a new slice, then at most one re-review. After that, report whatever still stands instead of looping. - Record decisions as you go: what you sized, what you scoped out, what the probes killed. They land in the closing report in one batch, never as questions mid-run. diff --git a/plugin/prompts/_partials/spec-gates.md b/plugin/prompts/_partials/spec-gates.md index 40a0e45..0432e1d 100644 --- a/plugin/prompts/_partials/spec-gates.md +++ b/plugin/prompts/_partials/spec-gates.md @@ -1,5 +1,7 @@ ## Spec gates +- Treat `tlc-spec-driven` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. - Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. - Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. - Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. diff --git a/plugin/prompts/roles/orchestrator-edit.md b/plugin/prompts/roles/orchestrator-edit.md index 082ac62..d2e498e 100644 --- a/plugin/prompts/roles/orchestrator-edit.md +++ b/plugin/prompts/roles/orchestrator-edit.md @@ -6,6 +6,7 @@ includes: - worktree - dispatch - proof + - spec-gates - reports-dispatch --- diff --git a/plugin/prompts/roles/orchestrator-read.md b/plugin/prompts/roles/orchestrator-read.md index 405d573..b33e43c 100644 --- a/plugin/prompts/roles/orchestrator-read.md +++ b/plugin/prompts/roles/orchestrator-read.md @@ -6,6 +6,7 @@ includes: - worktree - dispatch - proof + - spec-gates - reports-dispatch --- diff --git a/plugin/prompts/roles/orchestrator.md b/plugin/prompts/roles/orchestrator.md index bb39f4d..6371e91 100644 --- a/plugin/prompts/roles/orchestrator.md +++ b/plugin/prompts/roles/orchestrator.md @@ -69,7 +69,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe - Drive the whole run without being asked for each phase. The human asked for the outcome once. Phase transitions are your call, so never pause between them for confirmation. - Size it from the request, then commit to the size. Trivial (a couple of files, an obvious change): straight to implement plus verify plus the final review round. Anything shaped like a feature: the full loop below. - Specify: dispatch a worker to write `.specs/features//spec.md` with the goal, the acceptance criteria, and what is out of scope. Design and Tasks go the same way when the work needs them: `design.md` for architecture calls, `tasks.md` for atomic tasks that each carry their Tests and Gate. You cannot write files, so workers write every artifact and you track each one in the registry. -- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name when its harness offers it, otherwise to follow the lean briefing steps exactly without pasting skill text. +- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name. When its harness does not expose the skill, the worker follows the lean briefing steps exactly without pasting skill text. - Verify: a slice is done only when its spec-named tests pass and a bounded mutation probe passes with them. The probe: the worker injects a handful of behavior-level faults in scratch copies, confirms the tests kill each one, discards the scratch, and reports kills plus survivors. Survivors become fix slices, not excuses. - Review: run the final round yourself with `codedeck run --role reviewer --no-worktree "" --bg --json` over the finished scope. Slice self-review never replaces it. Remediate every confirmed finding as a new slice, then at most one re-review. After that, report whatever still stands instead of looping. - Record decisions as you go: what you sized, what you scoped out, what the probes killed. They land in the closing report in one batch, never as questions mid-run. diff --git a/plugin/skills/tlc-spec-driven/SKILL.md b/plugin/skills/tlc-spec-driven/SKILL.md new file mode 100644 index 0000000..13fc4ab --- /dev/null +++ b/plugin/skills/tlc-spec-driven/SKILL.md @@ -0,0 +1,184 @@ +--- +name: tlc-spec-driven +description: Feature planning and implementation with 4 adaptive phases (Specify, Design, Tasks, Execute). Auto-sizes depth by complexity. Writes testable requirements in EARS notation, atomic tasks, atomic Conventional Commits, and requirement traceability. Ships deterministic Python validation scripts so structural gates are enforced by code, not memory. Features an independent Verifier (author != verifier, evidence-or-zero), a discrimination sensor, a decision log (STATE.md), a test-coverage matrix, and a self-improving lessons layer. Stack-agnostic and tool-agnostic. Use when (1) planning features, (2) implementing with verification and atomic commits, (3) validating an implementation against a spec. Triggers on "specify feature", "discuss feature", "design", "tasks", "implement", "validate", "verify work", "UAT", "record decision", "pause work", "resume work". Do NOT use for pure architecture decomposition analysis or standalone technical design documents. +license: CC-BY-4.0 +metadata: + author: Felipe Rodrigues - github.com/felipfr + version: 3.3.0 +--- + +# Tech Lead's Club - Spec-Driven Development + +Plan and implement features with precision. Granular tasks. Clear dependencies. Right tools. Zero ceremony. + +``` +┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐ +│ SPECIFY │ → │ DESIGN │ → │ TASKS │ → │ EXECUTE │ +└──────────┘ └──────────┘ └─────────┘ └─────────┘ + required optional* optional* required + +* Agent auto-skips when scope doesn't need it +``` + +## Critical Rules (read before acting) + +**Loading this skill's files.** Reference files live under `references/` in this skill's own directory (where this `SKILL.md` resides). Resolve them relative to the skill directory - never the workspace root - and load them through the active skill by name; never assume a fixed install path. When a step tells you to read a reference, **read it completely (to EOF)** before acting - never act on a partial/truncated read. + +**Running this skill's scripts.** Every `scripts/*.py` shipped with this skill lives under that same skill directory. Resolve the skill directory first, then invoke `python3 /scripts/.py ...`. Never run `python3 scripts/...` from the consuming project root - that looks for a project-local `scripts/` tree that is not this skill. Project data under `.specs/` is still read/written relative to the project root (pass `--root` when the cwd is elsewhere). Below, `` means the directory that contains this `SKILL.md`. + +**Execution contract - every task, non-negotiable (holds even if you do not open the reference files):** + +1. Tests derive from the spec's acceptance criteria and assert spec-defined outcomes - they never mirror the implementation. +2. The gate must pass (tests pass) before a task is done - the test runner decides, not self-assessment. +3. One atomic commit per task. Mark the task complete in `tasks.md` (and update spec traceability when used) **before** that commit, and include those updates in the same commit. Never batch tasks; never weaken, skip, or delete tests to make them pass. +4. After the LAST task, a fresh **Verifier always runs automatically** (author ≠ verifier) - spec-anchored outcome check + discrimination sensor. It is never optional and never prompted. See Sub-Agent Delegation. +5. **Blast radius:** approving a spec or tasks authorizes local implementation and local commits only. `git push`, force-push, deploy, production DB changes, and other remote / externally visible / destructive operations require an explicit go-ahead for that action. + +**Deterministic gates run before human review - not from memory.** The structural gates for the spec and tasks are enforced by scripts in this skill's `scripts/` directory, so they cannot silently drift when the model forgets a step: + +- Before confirming a spec: `python3 /scripts/validate_spec.py ` (closure gate: EARS-shaped ACs, filled assumptions, well-formed requirement IDs, required sections). +- Before presenting tasks for approval: `python3 /scripts/validate_tasks.py ` (granularity smell, diagram-vs-`Depends on` parity within a phase, no forward-phase dependency, every task carries `Tests` + `Gate`). +- On each commit: `python3 /scripts/check_commit.py --message ""` (Conventional Commits). Optionally wire it as a git `commit-msg` guard (git only, no agent dependency) - see [implement.md](references/implement.md). +- Before declaring a feature done: `python3 /scripts/validate_state.py ` (completion gate: the Verifier's `validation.md` exists, its verdict is filled to PASS, and it cites `file:line` evidence - a missing, FAIL, placeholder, or evidence-free report fails). The closing step of Execute runs this automatically, the same way the lessons layer runs at distillation; it is not a manual step. + +A non-zero exit means STOP and fix before proceeding. Skip a script only when no code-execution tool is available; then perform the same checks by reading the artifact. + +**Before Execute:** read [implement.md](references/implement.md) completely and run `/scripts/validate_tasks.py`; if a formal `tasks.md` packs into more than one task-budgeted batch (> ~8 tasks), present the sub-agent offer first (see Sub-Agent Delegation). + +## Auto-Sizing: The Core Principle + +**The complexity determines the depth, not a fixed pipeline.** Before starting any feature, assess its scope and apply only what's needed: + +| Scope | What | Specify | Design | Tasks | Execute | +| ----------- | ------------------------ | ------------------------------------------------------- | ----------------------------------------------- | ----------------------------- | ----------------------------------------------------- | +| **Small** | ≤3 files, one sentence | One-liner spec (inline) | Skip | Skip | Implement + verify inline | +| **Medium** | Clear feature, <10 tasks | Spec (brief) | Skip - design inline | Skip - tasks implicit | Implement + verify | +| **Large** | Multi-component feature | Full spec + requirement IDs | Architecture + components | Full breakdown + dependencies | Implement + verify per task | +| **Complex** | Ambiguity, new domain | Full spec + [discuss gray areas](references/discuss.md) | [Research](references/design.md) + architecture | Breakdown + phase plan | Implement + [interactive UAT](references/validate.md) | + +**Rules:** + +- **Specify and Execute are always required** - you always need to know WHAT and DO it +- **Design is skipped** when the change is straightforward (no architectural decisions, no new patterns) +- **Tasks is skipped** when there are ≤3 obvious steps (they become implicit in Execute) +- **Discuss is triggered within Specify** when the agent detects ambiguous gray areas that need user input, or when the feature has any implicit-requirement dimension present (persistence/state, external calls, auth, payments, concurrency, state transitions) +- **Interactive UAT is triggered within Execute** only for user-facing features with complex behavior + +**Safety valve:** Even when Tasks is skipped, Execute ALWAYS starts by listing atomic steps inline (see [implement.md](references/implement.md)). If that listing reveals >5 steps or complex dependencies, STOP and create a formal `tasks.md` - the Tasks phase was wrongly skipped. + +## .specs Structure + +``` +.specs/ +├── STATE.md # Project memory: Decisions log (AD-NNN) + Handoff snapshot +├── LESSONS.md # Self-improving lessons playbook (rendered by scripts/lessons.py - do not hand-edit) +├── lessons.json # Canonical lessons state (machine-owned) +└── features/ # Feature specifications + └── [feature]/ + ├── spec.md # Requirements with traceable IDs + ├── context.md # User decisions for gray areas (only when discuss is triggered) + ├── design.md # Architecture & components (only for Large/Complex) + ├── tasks.md # Atomic tasks with verification (only for Large/Complex) + └── validation.md # Verifier report: PASS/FAIL, per-AC evidence, sensor result, diff range +``` + +**Create artifacts lazily.** Write each file only when its phase actually produces content - never scaffold empty `context.md`, `design.md`, or `tasks.md` up front. An empty file signals a phase happened when it did not; absence is the correct state for a skipped phase. The deterministic validators (`scripts/validate_spec.py`, `scripts/validate_tasks.py`, `scripts/check_commit.py`, `scripts/validate_state.py`) ship inside this skill's own `scripts/` directory, alongside `lessons.py`. + +## Workflow + +**New feature:** + +1. Specify → (Design) → (Tasks) → Execute (depth auto-sized) + +**Resume work:** + +1. Read `.specs/STATE.md` (Handoff + Decisions). +2. Reconcile Handoff against git (`branch`, `status --porcelain`, recent commits) and `tasks.md` - evidence wins over a stale snapshot. Full procedure: [memory.md](references/memory.md). +3. Propose the reconciled next step before writing code. + +## Context Loading Strategy + +**On-demand load (only what the current task needs):** + +- `.specs/STATE.md` - Decisions section (read at Design, re-read on resume); Handoff section (read on resume only) +- confirmed lessons - load at Specify and Design via `python3 /scripts/lessons.py list --status confirmed` ([lessons.md](references/lessons.md)); confirmed only, never candidates +- spec.md (when working on a specific feature) +- context.md (when designing or implementing from user decisions) +- design.md (when implementing from design) +- tasks.md (when executing tasks) + +**Never load simultaneously:** + +- Multiple feature specs +- Multiple architecture docs + +**Target:** <40k tokens total context +**Reserve:** 160k+ tokens for work, reasoning, outputs +**Monitoring:** Display status when >40k (see [context-limits.md](references/context-limits.md)) + +## Sub-Agent Delegation + +**Trigger:** count total tasks. If the feature packs into more than one task-budgeted batch (> ~8 tasks) → offer sub-agents; if it fits a single batch (≤ ~8 tasks) → execute inline. + +**Offer-then-confirm** - never auto-spawn. The user must accept before any sub-agent is dispatched. + +**One worker per task-budgeted batch (~7 tasks, whole phases):** Phases stay the semantic/dependency unit; a **batch** is the execution unit - one or more *consecutive whole phases* packed to ~7 tasks. Walk phases in order, accumulate whole phases into the current batch until it reaches the budget, then start the next - **never split a phase** across workers. ~20 tasks → ~3 workers; scales linearly (40 → ~6). Each worker executes all its tasks in order (implement → gate → atomic commit), then reports a compact summary (tasks done, commit hashes, test counts, deviations). Batches run sequentially - a batch never starts until the previous one reports all tasks complete. Workers never spawn further sub-agents. + +**Verifier (always-on, never prompted):** After the final task is committed, the orchestrator dispatches a fresh Verifier sub-agent automatically - regardless of phase count. Validation never requires a user prompt; it is the closing step of Execute. **Author ≠ verifier**: the Verifier re-derives coverage independently using evidence-or-zero; it does not inherit the author's mental model. The Verifier: (1) performs a **spec-anchored outcome check** - confirms each test's asserted value matches the spec-defined expected outcome, flags spec-precision gaps; (2) runs a **discrimination sensor** - injects behavior-level faults in an isolated scratch (temp worktree or file copies - never `git stash`), confirms tests kill them, discards the scratch and verifies real-tree porcelain matches the pre-sensor baseline; surviving mutants become fix tasks; (3) writes `.specs/features/[feature]/validation.md` (PASS/FAIL, per-AC evidence, sensor result, diff range); (4) returns a compact verdict + ranked gap list to the orchestrator in chat. Gaps become fix tasks; the fix→re-verify loop is bounded to 3 iterations before escalating. (5) **distills lessons** - turns each grounded failure (surviving mutant, spec-precision gap, failed AC, SPEC_DEVIATION) into a reusable project-local lesson via `/scripts/lessons.py`; a clean PASS records nothing (see [lessons.md](references/lessons.md)). + +**Model tier per role (only if the harness supports choosing a model per sub-agent).** Match the reasoning cost to the work instead of paying top-tier reasoning for boilerplate. A batch worker on a mechanical, low-ambiguity phase (entities, config, wiring, straightforward CRUD) runs on a faster/cheaper tier; a worker on a core-domain or high-ambiguity phase, and the Design phase itself, runs on a high-reasoning tier; the Verifier runs on a mid-to-high tier because it does adversarial reasoning and designs mutations. This is a portable recommendation: if the harness cannot set a per-sub-agent model, ignore it. Full rubric in [sub-agents.md](references/sub-agents.md). + +**Standalone fallback:** Without sub-agents, run `validate.md` as an independent fresh-eyes pass after the final commit - including the spec-anchored check and discrimination sensor. + +Full mechanics (worker payload, compact summary format, failure handling, context sizing, model tier, Verifier report format): [sub-agents.md](references/sub-agents.md). + +## Commands + +**Feature-level (auto-sized):** +| Trigger Pattern | Reference | +|----------------|-----------| +| Specify feature, define requirements | [specify.md](references/specify.md) | +| Discuss feature, capture context, how should this work | [discuss.md](references/discuss.md) | +| Design feature, architecture | [design.md](references/design.md) | +| Break into tasks, create tasks | [tasks.md](references/tasks.md) | +| Implement task, build, execute | [implement.md](references/implement.md) | +| Validate, verify, test, UAT, walk me through it | [validate.md](references/validate.md) | + +**Memory:** +| Trigger Pattern | Reference | +|----------------|-----------| +| Record decision, this is a project-level decision | [memory.md](references/memory.md) | +| Pause work, end session, I need to stop | [memory.md](references/memory.md) | +| Resume work, continue, pick up where we left off | [memory.md](references/memory.md) | +| Load lessons, what have we learned, apply past lessons | [lessons.md](references/lessons.md) | +| Record lesson, distill lessons (auto-runs after validation) | [lessons.md](references/lessons.md) | + +## Knowledge Verification Chain + +When researching, designing, or making any technical decision, follow this chain in strict order. Never skip steps. + +``` +Step 1: Codebase → check existing code, conventions, and patterns already in use +Step 2: Project docs → README, docs/, inline comments, `.specs/STATE.md` (Decisions) +Step 3: Context7 MCP → resolve library ID, then query for current API/patterns +Step 4: Web search → official docs, reputable sources, community patterns +Step 5: Flag as uncertain → "I'm not certain about X - here's my reasoning, but verify" +``` + +**Rules:** + +- Never skip to Step 5 if Steps 1-4 are available +- Step 5 is ALWAYS flagged as uncertain - never presented as fact +- **NEVER assume or fabricate.** If you cannot find an answer, say "I don't know" or "I couldn't find documentation for this". Inventing APIs, patterns, or behaviors causes cascading failures across design → tasks → implementation. Uncertainty is always preferable to fabrication. + +## Output Behavior + +**Do the work; do not narrate the machinery.** Produce the right artifact for the phase instead of announcing the phase ("I will now run the Specify phase"). The user judges the output, not a play-by-play of the process. This keeps the flow from reading as robotic. + +**Match effort to the work.** Lightweight steps (feature-level checks, validation, mechanical tasks) do not need top-tier reasoning; heavy steps (complex design, ambiguous features) do. If the harness lets you pick a model per sub-agent, apply the tier rubric in [sub-agents.md](references/sub-agents.md); otherwise proceed and simply invest more care on the heavy steps. Mention this once per session at most, and only if it helps; skip it for an experienced user. + +**Write generated artifacts in a plain, decided voice.** Specs, ADRs, validation reports, commit messages, and chat summaries follow the writing rules in [coding-principles.md](references/coding-principles.md): lead with the verdict, state decisions definitively, cut filler and mechanical hedging. + +## Code Analysis + +Use available tools with graceful degradation. See [code-analysis.md](references/code-analysis.md). diff --git a/plugin/skills/tlc-spec-driven/references/code-analysis.md b/plugin/skills/tlc-spec-driven/references/code-analysis.md new file mode 100644 index 0000000..18257d7 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/code-analysis.md @@ -0,0 +1,98 @@ +# Code Analysis Tools + +Use graceful degradation for code search and structural analysis. + +## Tool Priority + +1. **ast-grep** (`sg`) - Structural pattern-based search +2. **ripgrep** (`rg`) - Fast context-aware text search +3. **grep** - Standard text search (always available) + +## Detection + +Check tool availability before use: + +```bash +# Check for ast-grep +if command -v sg >/dev/null 2>&1; then + # Use ast-grep for structural search +elif command -v rg >/dev/null 2>&1; then + # Fall back to ripgrep +else + # Use standard grep as final fallback +fi +``` + +## Usage Examples + +**Finding function definitions:** + +```bash +# ast-grep (best - structural) +sg -p 'function $NAME($$$) { $$$ }' + +# ripgrep (fallback - fast text) +rg '^function\s+\w+\(' --type-add 'source:*.[extension]' -t source + +# grep (last resort - basic) +grep -r '^function ' --include="*.[extension]" +``` + +**Finding imports/requires:** + +```bash +# ast-grep +sg -p 'import { $$$ } from "$MODULE"' + +# ripgrep +rg '^import .* from' --type-add 'source:*.[extension]' -t source + +# grep +grep -r '^import ' --include="*.[extension]" +``` + +**Finding class/component definitions:** + +```bash +# ast-grep +sg -p 'class $NAME { $$$ }' + +# ripgrep +rg '^(class|export class)\s+\w+' --type-add 'source:*.[extension]' -t source + +# grep +grep -r '^class ' --include="*.[extension]" +``` + +## Search Scope + +**Best practices:** + +- Limit to source file extensions relevant to project +- Exclude directories: `node_modules`, `vendor`, `dist`, `build`, `.git` +- Focus on source directories: `src`, `lib`, `app` +- Use file type filters when available + +**Performance tips:** + +- Use specific patterns over broad searches +- Limit directory depth with `--max-depth` (ripgrep/grep) +- Cache results for repeated queries + +## Fallback Notice + +If ast-grep unavailable, display once per session: + +``` +⚠️ ast-grep not detected. Install for more precise structural code analysis. + https://ast-grep.github.io/guide/quick-start.html +``` + +## When to Use + +- Finding usage patterns across codebase +- Identifying code structure and organization +- Locating function/class/component definitions +- Analyzing import/dependency patterns +- Refactoring impact analysis +- Code navigation in unfamiliar codebases diff --git a/plugin/skills/tlc-spec-driven/references/coding-principles.md b/plugin/skills/tlc-spec-driven/references/coding-principles.md new file mode 100644 index 0000000..6d30953 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/coding-principles.md @@ -0,0 +1,72 @@ +# Coding Principles + +Behavioral bias, not checklist. Read before every implementation. + +--- + +## Before Coding + +- State assumptions explicitly. If uncertain, ask. +- Multiple interpretations exist? Present all-don't pick silently. +- Simpler approach exists? Say so. Push back when warranted. +- Something unclear? Stop. Name what's confusing. Ask. +- User's approach seems wrong? Disagree honestly. Don't be sycophantic. + +--- + +## During Implementation + +### Simplicity + +- No features beyond what was asked +- No abstractions for single-use code +- No "flexibility" or "configurability" not requested +- No error handling for impossible scenarios +- 200 lines that could be 50? Rewrite it. + +### Surgical Changes + +- Don't "improve" adjacent code, comments, or formatting +- Don't refactor things that aren't broken +- Match existing style, even if you'd do differently +- Unrelated dead code noticed? Mention it-don't delete it +- Remove ONLY imports/variables/functions YOUR changes orphaned +- Don't remove pre-existing dead code unless asked + +### Test Integrity + +- NEVER weaken an existing test assertion to make it pass +- NEVER delete a test to reduce failure count +- NEVER use the test framework's skip/disable/pending mechanism to bypass a failing test +- NEVER modify a task's tests afterward to make the implementation pass +- If a test is genuinely wrong, STOP and confirm with the user before changing it +- Tests are the spec - implementation conforms to tests, not the other way around + +### Goal-Driven + +- Transform vague tasks into verifiable goals +- Multi-step work? State brief plan with verify checkpoints +- Every changed line must trace directly to user's request + +--- + +## After Each Change + +Ask: "Would senior engineer call this overcomplicated?" +If yes → simplify before proceeding. + +--- + +## Writing Voice (specs, ADRs, reports, commits, summaries) + +The artifacts this skill produces should read like a decided engineer wrote them, not like generated boilerplate. + +- **Lead with the verdict.** Validation reports and chat summaries open with PASS/FAIL and the one thing that matters, not a warm-up paragraph. +- **Decisions are definitive.** An ADR or a recorded decision states what you chose: "we will", not "we might" or "we should probably". If it still hedges, it is not a decision yet. +- **Cut filler and mechanical hedging.** Drop "it is worth noting", "as you can see", "in order to", and reflexive "may/might/could" on claims you are actually sure about. Reserve hedging for genuine uncertainty; using it everywhere signals nothing. +- **One idea per sentence; short sentences.** Prefer the plain verb over the nominalization ("evaluated", not "performed an evaluation of"). Keep subject-verb-object near the front. +- **Do not announce the phase.** Produce the artifact; do not narrate "I will now run Specify." +- **Avoid the em dash as a default connector.** A comma, colon, or two sentences usually read cleaner. +- **Writing in Portuguese:** keep sentences short; do not carry the long, multi-clause subordinate structure of Portuguese into the artifact. Plain and direct beats formal. + +None of this means dumbing down the content - only the prose carrying it. diff --git a/plugin/skills/tlc-spec-driven/references/context-limits.md b/plugin/skills/tlc-spec-driven/references/context-limits.md new file mode 100644 index 0000000..1cba5f9 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/context-limits.md @@ -0,0 +1,31 @@ +# Context Limits + +## File Size Limits + +| File | Max Tokens | ~Words | Warning At | +| --------- | ---------- | ------ | ---------- | +| spec.md | 5,000 | 3,000 | 4,000 | +| design.md | 8,000 | 4,800 | 6,400 | +| tasks.md | 10,000 | 6,000 | 8,000 | + +## Context Zones + +🟢 **Healthy** (<40k total): Silent +🟡 **Moderate** (40-60k): Discrete footer note +🔴 **Critical** (>60k): Active warning, suggest optimization + +## Monitoring + +Display context status in footer when >40k: + +``` +📊 Context: 52k tokens (moderate) + - tasks.md: 11k (ok) + - design.md: 6k (ok) + - Total: 52k / 200k (26%) +``` + +## Principles + +**Target:** <40k tokens loaded (20% of window) +**Reserve:** 160k+ tokens for work, reasoning, outputs diff --git a/plugin/skills/tlc-spec-driven/references/design.md b/plugin/skills/tlc-spec-driven/references/design.md new file mode 100644 index 0000000..26d84ab --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/design.md @@ -0,0 +1,199 @@ +# Design + +**Goal**: Define HOW to build it. Architecture, components, what to reuse. + +**Skip this phase when:** The change is straightforward - no architectural decisions, no new patterns, no component interactions to plan. For simple features, design happens inline during Execute. + +## Process + +### 1. Load Context + +Read `.specs/features/[feature]/spec.md` before designing. If `.specs/features/[feature]/context.md` exists, load it too - it contains implementation decisions that constrain the design (layout choices, behavior preferences, interaction patterns). Decisions marked as "Agent's Discretion" are yours to decide. + +**Mandatory: read `.specs/STATE.md` `## Decisions` now.** This MUST happen before any architectural choices are made. Every `active` `AD-NNN` entry is a project-level constraint this design must conform to. If a decision from a prior feature conflicts with what is best for this feature, you have two options - both require an explicit choice: + +1. **Conform** - Design within the active constraint. +2. **Supersede** - Append a new `AD-NNN` entry to `.specs/STATE.md` `## Decisions` that supersedes the old one (set the old entry's `status` to `superseded by AD-NNN`) and document the reason. The new decision becomes the project standard going forward. + +Silently ignoring an active decision is not an option - it creates invisible inconsistency across features. + +**Also load confirmed lessons** relevant to this feature: `python3 /scripts/lessons.py list --status confirmed` (filter with `--scope`/`--query`). These are past verification failures distilled into guidance - apply them while designing. Load only `confirmed`. Skip silently if no store or no code tool. See [lessons.md](lessons.md). + +### 1.5. Research (Optional but Recommended) + +If the feature involves unfamiliar technology, patterns, or integrations, research before designing. Document findings briefly in the design doc or as inline notes. This prevents incorrect assumptions from propagating into tasks. + +Follow the **Knowledge Verification Chain** (see SKILL.md) in strict order: + +``` +Codebase → Project docs → Context7 MCP → Web search → Flag as uncertain +``` + +**CRITICAL: NEVER assume or fabricate information.** If you cannot find an answer through the chain, explicitly say "I don't know" or "I couldn't find documentation for this". Inventing an API, a pattern, or a behavior that doesn't exist is far worse than admitting uncertainty. Wrong assumptions propagate through design → tasks → implementation and cause cascading failures. + +Good triggers for research: new libraries, unfamiliar APIs, performance-sensitive features, security-sensitive features, patterns you haven't used in this codebase before. + +**Concern flagging (MUST do while reading code):** While walking the codebase via the Knowledge Verification Chain, flag any concerns you encounter in the areas this feature touches. Capture each finding in the `## Risks & Concerns` section of `design.md`: + +- **Fragile code** - tight coupling, large functions, implicit state +- **Tech debt** - hacks, workarounds, deprecated APIs +- **Security risks** - unvalidated input, auth gaps, exposed secrets +- **Performance bottlenecks** - N+1 queries, unbounded loops, missing indexes +- **Test coverage gaps** - untested paths the feature depends on + +Every flagged concern MUST include a mitigation - how the design (or a follow-up task) addresses it. + +### 2. Define Architecture + +**Large/Complex only - approach exploration:** Before committing to a single architecture, present 2-3 viable approaches with trade-offs and a recommendation. Lead with the recommendation to avoid analysis paralysis. All approaches must deliver the same scoped thing (no alternative scopes). Confirm the chosen approach with the user before detailing components. Medium features: skip - design inline. + +Overview of how components interact. Use mermaid diagrams when helpful. + +### 3. Identify Code Reuse + +**CRITICAL**: What existing code can we leverage? This saves tokens and reduces errors. + +Flag any concerns found here per step 1.5 into `## Risks & Concerns`. + +### 4. Define Components and Interfaces + +Each component: Purpose, Location, Interfaces, Dependencies, What it reuses. + +### 5. Define Data Models + +If the feature involves data, define models before implementation. + +--- + +## Template: `.specs/features/[feature]/design.md` + +````markdown +# [Feature] Design + +**Spec**: `.specs/features/[feature]/spec.md` +**Status**: Draft | Approved + +--- + +## Architecture Overview + +[Brief description of the architecture approach] + +```mermaid +graph TD + A[User Action] --> B[Component A] + B --> C[Service Layer] + C --> D[Data Store] + B --> E[Component B] +``` +```` + +--- + +## Code Reuse Analysis + +### Existing Components to Leverage + +| Component | Location | How to Use | +| -------------------- | ------------------- | ------------------------- | +| [Existing Component] | `src/path/to/file` | [Extend/Import/Reference] | +| [Existing Utility] | `src/utils/file` | [How it helps] | +| [Existing Pattern] | `src/patterns/file` | [Apply same pattern] | + +### Integration Points + +| System | Integration Method | +| -------------- | --------------------------------------- | +| [Existing API] | [How new feature connects] | +| [Database] | [How data connects to existing schemas] | + +--- + +## Components + +### [Component Name] + +- **Purpose**: [What this component does - one sentence] +- **Location**: `src/path/to/component/` +- **Interfaces**: + - `methodName(param: Type): ReturnType` - [description] + - `methodName(param: Type): ReturnType` - [description] +- **Dependencies**: [What it needs to function] +- **Reuses**: [Existing code this builds upon] + +### [Component Name] + +- **Purpose**: [What this component does] +- **Location**: `src/path/to/component/` +- **Interfaces**: + - `methodName(param: Type): ReturnType` +- **Dependencies**: [Dependencies] +- **Reuses**: [Existing code] + +--- + +## Data Models (if applicable) + +### [Model Name] + +```typescript +interface ModelName { + id: string + field1: string + field2: number + createdAt: Date +} +``` + +**Relationships**: [How this relates to other models] + +### [Model Name] + +```typescript +interface AnotherModel { + id: string + // ... +} +``` + +--- + +## Error Handling Strategy + +| Error Scenario | Handling | User Impact | +| -------------- | ------------- | ---------------- | +| [Scenario 1] | [How handled] | [What user sees] | +| [Scenario 2] | [How handled] | [What user sees] | + +--- + +## Risks & Concerns + +| Concern | Location (file:line) | Impact | Mitigation | +| ------- | -------------------- | ------ | ---------- | +| [Fragile code / tech debt / security / perf / test gap] | `src/path/file.ts:42` | [What breaks or degrades] | [How the design or a follow-up task addresses it] | + +> None found - is a valid entry. + +--- + +## Tech Decisions (only non-obvious ones) + +| Decision | Choice | Rationale | +| ----------------- | --------------- | ------------- | +| [What we decided] | [What we chose] | [Why - brief] | + +> **Project-level decisions:** If a decision here sets a convention, pattern, or constraint that future features must follow, append it to `.specs/STATE.md` `## Decisions` as the next `AD-NNN` entry (see [memory.md](memory.md)). Feature-local decisions stay only in this table. + +--- + +## Tips + +- **Load context first** - If context.md exists, decisions there are locked +- **Research when uncertain** - 5 minutes of research prevents hours of rework +- **Reuse is king** - Every component should reference existing patterns +- **Interfaces first** - Define contracts before implementation +- **Keep it visual** - Diagrams save 1000 words +- **Small components** - If component does 3+ things, split it +- **Flag concerns inline** - Risks found during research go in Risks & Concerns with a mitigation +- **Confirm before Tasks** - User approves design before breaking into tasks diff --git a/plugin/skills/tlc-spec-driven/references/discuss.md b/plugin/skills/tlc-spec-driven/references/discuss.md new file mode 100644 index 0000000..23aab1b --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/discuss.md @@ -0,0 +1,159 @@ +# Specify: Discuss Gray Areas + +**Goal:** Capture HOW the user envisions the feature when the spec has ambiguous areas. This is NOT a separate phase - it's triggered within Specify when the agent detects gray areas that need user input. + +**Trigger:** Automatically when gray areas are detected during spec creation, or explicitly via "discuss feature", "how should this work?", "capture context" + +**When to trigger (auto-detect):** The spec contains user-facing behavior that could go multiple ways AND the user hasn't expressed a preference. If the spec is clear and unambiguous, skip this entirely. + +**When NOT to trigger:** Genuinely trivial features - a pure read endpoint, a config tweak, features with no [implicit-requirement dimensions](specify.md#implicit-requirement-dimensions) present (no persistence/state, external calls, auth, payments, concurrency, or state transitions). When any dimension is present, trigger discuss. + +## Why This Phase Exists + +Specifications capture WHAT to build. Design captures the architecture. But neither captures the user's vision for ambiguous areas - layout preferences, interaction patterns, error handling style, content tone. Without this, the agent guesses. With this, the agent builds what the user actually imagined. + +The output - `context.md` - feeds directly into Design and Tasks: + +- **Design reads it** to know what decisions are locked vs. flexible +- **Tasks reads it** to include specific behaviors in task definitions + +## Process + +### 1. Analyze the Feature + +Read `.specs/features/[feature]/spec.md` and identify the domain: + +| Domain | Gray areas to explore | +| ------------------------------ | ------------------------------------------------------------- | +| Something users **SEE** | Layout, density, interactions, empty states, visual hierarchy | +| Something users **CALL** (API) | Response format, errors, auth, versioning, rate limiting | +| Something users **RUN** (CLI) | Output format, flags, modes, error handling, verbosity | +| Something users **READ** | Structure, tone, depth, flow, navigation | +| Something being **ORGANIZED** | Grouping criteria, naming, duplicates, exceptions | +| Something with **backend / state / contract** | Failure & partial-failure states, idempotency/retry/dedup, auth boundaries & rate limits, data lifecycle/expiry, concurrency/ordering - see [implicit-requirement dimensions](specify.md#implicit-requirement-dimensions) | + +Generate 3-4 **feature-specific** gray areas. Not generic categories, but concrete decisions for THIS feature. + +### 2. Present Gray Areas + +Present the feature boundary (from spec.md) and the gray areas to the user. Let them choose which to discuss. Do NOT include a "skip all" option - the user invoked this phase to discuss. + +Any gray area the user **declines** to discuss, or that goes undiscussed, is written to the spec's **Assumptions & Open Questions** section (agent's chosen default + rationale) - never silently dropped. This ensures the spec's closure gate can pass: every gray area is either resolved through discussion or recorded as a signed-off assumption. + +### 3. Choose discussion pace (once) + +Before deep-diving, ask **one** pace question. Recommend **Guided** as the default. If the user skips, says "whatever", or "you choose", use Guided. + +| Pace | When it fits | Cadence | +| ------------ | ------------------------------------------------- | ----------------------------------------------------------------------- | +| **Quick** | User wants speed; trusts defaults | Propose defaults per area (rationale included); user accepts / overrides | +| **Guided** | Default - balances depth and turn count | Adaptive elicitation (see below) | +| **Detailed** | High ambiguity; user wants Socratic control | Exactly one decision per turn, dependency order | + +Honor mid-discussion switches immediately ("go faster", "slow down", "just decide") - change pace without restarting or re-asking settled decisions. + +### 4. Deep-Dive Each Area + +Shared rules for every pace: + +1. Options must be concrete ("Card layout" or "Table layout" - not "Option A" or "how should it look?"). +2. Lead with your recommended answer and one line of reasoning. You have read the codebase; the user should be able to accept or override in a word. +3. Offer "You decide" when reasonable - it records agent discretion explicitly. +4. Resolve anything discoverable from the code yourself (Knowledge Verification Chain); only put genuine product decisions to the user. +5. When an area is settled: "More on [area], or move on?" After all areas: "Ready to create context?" + +**Quick:** For each selected gray area, present the recommended decisions for that area in one turn (defaults + short rationale). Wait for accept / override. Do not drip-feed single questions unless the user challenges a default and opens a real fork. + +**Guided:** Adaptive elicitation - questions are a decision tree to prune, not a checklist to finish. + +1. Classify upcoming decisions as **independent** vs **dependent**. +2. Low-stakes / safe-to-default → state the assumption and invite correction (no blocking question). +3. Independent product decisions → ask **at most 2** in the same turn, each with options + recommended default. +4. Dependent decisions → ask **exactly one**, wait, then continue (the earlier answer should prune later questions). +5. Never dump 3+ questions in one turn. Never ask what the code already answers. +6. Stop the area as soon as enough is decided. + +**Detailed:** Walk selected gray areas as a strict decision tree - one concrete question per turn, dependency order, wait for each answer before the next. Use when the user wants maximum control or the feature is highly ambiguous. + +### 5. Scope Guardrail (CRITICAL) + +The feature boundary from spec.md is **fixed**. Discussion clarifies HOW to implement, never WHETHER to add new capabilities. + +**Allowed:** "How should posts be displayed?" (clarifying ambiguity) +**Not allowed:** "Should we also add comments?" (new capability) + +When user suggests scope creep: "That sounds like a separate feature. I'll note it in Deferred Ideas. Back to [current area]." + +### 6. Write context.md + +--- + +## Template: `.specs/features/[feature]/context.md` + +```markdown +# [Feature] Context + +**Gathered:** [date] +**Spec:** `.specs/features/[feature]/spec.md` +**Status:** Ready for design + +--- + +## Feature Boundary + +[Clear statement of what this feature delivers - the scope anchor from spec.md] + +--- + +## Implementation Decisions + +### [Area 1 that was discussed] + +- [Specific decision made] +- [Another decision if applicable] + +### [Area 2 that was discussed] + +- [Specific decision made] + +### [Area 3 that was discussed] + +- [Specific decision made] + +### Agent's Discretion + +[Areas where user explicitly said "you decide" - agent has flexibility here during design/implementation] + +### Declined / Undiscussed Gray Areas → Assumptions + +[Gray areas the user declined to discuss or that were not covered. Each entry is written to the spec's Assumptions & Open Questions section with the agent's chosen default and rationale - not left silently unresolved.] + +--- + +## Specific References + +[Any "I want it like X" moments, product references, specific behaviors, interaction patterns mentioned during discussion] + +[If none: "No specific requirements - open to standard approaches"] + +--- + +## Deferred Ideas + +[Ideas that came up during discussion but belong in other features/phases. Captured here so they're not lost, but explicitly out of scope] + +[If none: "None - discussion stayed within feature scope"] +``` + +--- + +## Tips + +- **Pace is a user choice; Guided is the default** - Quick for speed, Guided for balance, Detailed for Socratic depth; honor mid-discussion switches +- **Guided ≠ interrogation and ≠ form dump** - Assume-first when safe, ≤2 independent questions per turn, one-at-a-time only when answers depend on each other +- **Look it up, don't ask** - Resolve anything discoverable from the code yourself; ask only genuine product decisions +- **Decisions, not vision** - "Card-based layout with subtle shadows" is a decision. "Should feel modern" is not. +- **Scope is sacred** - Deferred Ideas captures scope creep without losing ideas +- **User = visionary, Agent = builder** - Ask about how they imagine it, not about technical implementation +- **Don't ask about:** Technical architecture, performance, implementation details - that's Design's job +- **Confirm before Design** - User approves context.md before moving to design phase diff --git a/plugin/skills/tlc-spec-driven/references/implement.md b/plugin/skills/tlc-spec-driven/references/implement.md new file mode 100644 index 0000000..0cba8d2 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/implement.md @@ -0,0 +1,436 @@ +# Execute + +**Goal**: Implement ONE task at a time. Surgical changes. Verify. Commit. Repeat. + +This is where code gets written. Every task follows the same cycle: plan → implement → verify → commit. Verification is built into every task, not a separate phase. + +--- + +## MANDATORY: Before Starting Any Implementation + +**Read [coding-principles.md](coding-principles.md) and state:** + +1. **Assumptions** - What am I assuming? Any uncertainty? +2. **Files to touch** - List ONLY files this task requires +3. **Success criteria** - How will I verify this works? + +⚠️ **Do not proceed without stating these explicitly.** + +--- + +## Process + +**Batch worker context:** When this task is executed as part of a phase-batch sub-agent, the worker +receives the task definitions for every phase in its batch, coding principles, the generated Test +Coverage Matrix and Gate Check Commands from tasks.md, and relevant spec/design context. A batch is +one or more consecutive whole phases packed to ~7 tasks. The worker executes ALL tasks in its +assigned batch in order - finishing every task in one phase before starting the next phase in the +batch - and each task follows every step below (implement → gate → atomic commit) before moving to +the next. After all tasks in the batch are complete, the worker reports a compact summary (tasks +done, commit hashes, test counts, deviations/blockers) to the orchestrator. See +[sub-agents.md](sub-agents.md) for the full model. + +### Before implementing: assess sub-agent delegation (MANDATORY - before the first task) + +Before implementing anything, if a formal `tasks.md` with an Execution Plan exists, **count its total tasks** and pack the phases into task-budgeted batches (~7 tasks per worker, whole phases - see [sub-agents.md](sub-agents.md)). If that yields **more than one batch** (> ~8 tasks), you MUST present the sub-agent offer to the user and wait for their choice before starting Execute - do not silently proceed inline. If the feature fits a single batch (≤ ~8 tasks, or the user declines), execute inline. Skip this check only when you are already a batch worker executing a delegated batch (the orchestrator already made the delegation decision). + +### 0. List Atomic Steps (MANDATORY when Tasks phase was skipped) + +If there is no `tasks.md` for this feature, you MUST list atomic steps before writing any code. This is non-negotiable - it prevents the agent from losing focus and doing too many things at once. + +``` +## Execution Plan + +1. [Step] → files: [list] → verify: [how] → commit: [message] +2. [Step] → files: [list] → verify: [how] → commit: [message] +3. [Step] → files: [list] → verify: [how] → commit: [message] +``` + +**Each step must be:** + +- ONE deliverable (one component, one function, one endpoint, one file change) +- Independently verifiable (can prove it works before moving on) +- Independently committable (gets its own atomic git commit) + +If listing steps reveals >5 steps or complex dependencies, STOP and create a formal `tasks.md` instead. The Tasks phase was wrongly skipped. + +### 1. Pick Task + +From tasks.md (if exists) or from the execution plan above. User specifies ("implement T3") or suggest next available. + +### 2. Verify Dependencies + +If tasks.md exists, check dependencies. If using inline plan, follow the order listed. + +❌ If blocked: "T3 depends on T2 which isn't done. Should I do T2 first?" + +### 3. State Implementation Plan + +Before writing code: + +``` +Files: [list] +Approach: [brief description] +Success: [how to verify] +``` + +### 4. Write Tests (derived from spec, not from implementation) + +If the task includes tests (per the Tests field and **Test Coverage Matrix** in tasks.md): + +1. Write the test file(s) covering the task's acceptance criteria. +2. Tests MUST be derived from the task's "Done when" criteria and `spec.md` ACs - **not** from the implementation. Each test encodes what the spec requires; never write tests by reading the code and asserting what it currently does. +3. Each acceptance criterion from "Done when" maps to at least one test assertion whose asserted value matches the **spec-defined expected outcome**. Where the spec does not define a precise outcome, note it as a **spec-precision gap** rather than writing a vague assertion and passing silently. +4. Edge cases from spec.md that apply to this task get test cases too. + +**HARD CONSTRAINTS (test integrity - never violate):** + +- Do NOT weaken assertions (making them less specific to pass more easily) +- Do NOT delete or skip test cases +- Do NOT use the test framework's skip/disable/pending mechanism to bypass failing tests + +If a test is genuinely wrong (tests the wrong behavior per spec), STOP and ask the user +before modifying it. Never silently change a test. + +If the task does NOT include tests (e.g., entity-only, config-only), skip to Step 4b. + +### 4b. Implement + +Write the minimum implementation needed to satisfy the task's success criteria: pass all relevant tests (when present) and meet the defined verification/gate checks when there are no direct tests. + +**HARD CONSTRAINTS:** + +- The test-integrity rules from step 4 still hold: do NOT weaken, delete, or skip/disable tests. The tests are the spec - implementation conforms to them, not the reverse. +- Modify a test only to fix a genuinely wrong assertion, and ask the user first. +- Minimum code to pass - save structural improvements for a refactor task + +Follow [coding-principles.md](coding-principles.md): + +- Simplest code that works +- Touch ONLY listed files +- No scope creep + +### 5. Gate Check (VERIFY) + +Run the gate check command from the task definition. This is MANDATORY - not "if applicable." + +1. Look up the command for the task's Gate level (quick/full/build) in the **Gate Check Commands** section of tasks.md, then run it +2. Non-zero exit code = STOP. Fix the failure. Re-run. Do not proceed until it passes. +3. Confirm the test count matches expectations (no tests were silently deleted or skipped) + +**Tiered gates (from the Gate Check Commands section of tasks.md):** + +| Task includes | Gate level | What runs | +| -------------------------------- | ---------- | ------------------------ | +| Unit tests only | Quick | Unit test command | +| E2E or integration tests | Full | Unit + E2E commands | +| Last task in a phase | Build | Build + lint + all tests | +| No tests (config, entities, etc) | Build | Build + lint only | + +The gate check is deterministic. The test runner decides if the code is correct, +not the agent's self-assessment. + +### 6. Post-Gate Review + +After the gate check passes: + +1. Verify test count: Are there at least as many test cases as before? (prevents silent deletion) +2. Verify no SPEC_DEVIATION: If implementation diverged from spec/design, add a marker: + +``` +// SPEC_DEVIATION: [what diverged] +// Reason: [why the deviation was necessary] +``` + +3. Quick complexity check: "Would senior engineer flag this as overcomplicated?" + - Yes → Simplify, re-run gate + - No → Proceed + +4. **Test Adequacy Review (MANDATORY - hard gate).** + + A task cannot be committed or marked done until all four checks below pass. Tests must be both **necessary** (every test traces to a requirement) and **sufficient** (every requirement is covered). The scope boundary is the feature spec - do not test beyond it. + + **Check A - Sufficient coverage (per-layer depth).** Build and output this table: + + | Done-when criterion / spec AC / listed edge case | `file:line` + assertion expression | Spec-defined outcome | Covered? | + | ------------------------------------------------- | ---------------------------------- | -------------------- | -------- | + | [criterion from task or spec] | `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | [expected value from spec] | ✅ Yes / ❌ No / ⚠️ Spec-precision gap | + + **Evidence-or-zero rule:** Each covered cell MUST cite the exact `file:line` where the assertion lives AND reproduce the assertion expression (not just the `describe`/`it` name). A criterion with no located `file:line` evidence counts as **NOT covered**; the task cannot be marked done. Do not declare a criterion absent without first searching the test files - show the search before concluding it is missing (mirror: evidence or zero, never a guess). + + **Spec-anchored outcome check:** For each covered criterion, derive the expected outcome from `spec.md` (or the task's "Done when" field) and confirm the test's asserted value matches it - not just that an assertion exists. Where the spec defines a precise outcome (e.g., a specific status code, a specific field value, a specific error message), the test assertion MUST target that exact outcome. Where the spec does not define a precise outcome, mark the cell as **⚠️ Spec-precision gap** and add a note; do NOT silently pass a vague assertion as if it were covered. + + Every "Done when" criterion, every spec.md acceptance criterion, and every listed edge case that applies to this task must map to at least one concrete test assertion. Enforce the layer's Coverage Expectation from the Test Coverage Matrix: + + - Domain / service layer: assertions map 1:1 to spec ACs; every listed edge case has a dedicated test. + - Route / controller / e2e layer: every route the task adds or modifies must have a happy-path test, a test for each listed edge case, and a test for each documented error/failure path. + + No criterion left unverified. + + **Check B - Non-shallow litmus.** Reject each of the following shallow patterns: + - Assertion-free tests or `expect(true)` / `expect(1).toBe(1)` style tautologies + - "No error thrown" as the only assertion - unless not-throwing IS the specified behavior + - Asserting only on mock call counts when the actual output/state is what the criterion demands + - Happy-path only when the task's "Done when" or spec.md lists edge cases + + **Payload/conjunction rule.** For each named field in an emitted event, returned object, or persisted record, apply a separate check: + 1. Open the constructed object at its `file:line` and confirm the field is present in the assertion. + 2. Confirm the assertion targets the field's **value or state**, not just the call that produced it. + 3. A present `emit(...)` / `return ...` / `save(...)` call does NOT prove the field - only an assertion on the result does. + 4. Asserting a method was called (spy/mock) != asserting the resulting state. Both may be needed; neither substitutes for the other. + + Apply this check to every payload-bearing criterion before marking it covered. + + **Stack-agnostic litmus:** An assertion is shallow if it would still pass under a plausible *wrong* implementation. If so, strengthen it before committing. + + **Check C - Necessary (no tests beyond the spec).** Reverse-map every test back to a spec AC, a listed edge case, or a "Done when" criterion. Build this table: + + | `file:line` + assertion expression | Maps to (AC / edge case / Done-when criterion) | Keep? | + | ---------------------------------- | ---------------------------------------------- | ----- | + | `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | [requirement ID or criterion text] | ✅ Keep / ❌ Remove | + + Any test that maps to nothing → remove it. A test with no requirement is scope creep - it proves nothing about the feature and expands scope beyond the spec. Do not write speculative "what if" tests, do not test framework or library behavior, and do not duplicate an assertion that is already covered at another layer for the same scenario. + + **Check D - Guideline conformance.** If project quality/testing guidelines were found in step 0 of tasks.md step 1.5, verify this task's tests conform to them (naming conventions, file locations, coverage thresholds, etc.). Note the guideline file followed. + + **Bound:** Tests prove the work; they do not expand it. Thoroughness is scoped to the feature + spec. Repo depth is a floor (never less thorough than existing tests for the same layer); the spec is the ceiling. Do not invent requirements or tests that have no spec anchor. + + **Anti-patterns - known verification cheats (treat any of these as an automatic Check failure):** + + | Anti-pattern | Why it fails | + | ------------ | ------------ | + | Committing before the gate check passes | Skips the deterministic verifier - the gate is not optional | + | Asserting call count / spy invocation instead of the resulting state | Proves the method ran, not that it did the right thing | + | Marking a criterion covered without a `file:line` citation | Violates evidence-or-zero; suspicion of coverage is not coverage | + | Weakening an assertion (making it less specific) to force a pass | Moves the goalposts instead of fixing the code | + | Deleting or skipping a test to make the suite pass | Destroys coverage permanently; a failing test is a signal, not noise | + | "Tested elsewhere" deferral without citing where | Coverage gaps hide behind vague claims; cite the file:line or it doesn't count | + | Speculative "what if" tests with no spec anchor | Expands scope beyond the ceiling; remove them in Check C | + | Testing framework or library behavior | Tests a dependency, not the feature; remove them in Check C | + + **On any failure** → rewrite or remove the affected test(s), re-run the gate, then re-run this review. + + *Honest caveat:* This is an inspection-based review (model judgment), complementary to - not a replacement for - the deterministic gate. The gate confirms the test suite runs; the feature-level discrimination sensor (step 9) confirms the tests can detect regressions. This review confirms the suite is meaningful and bounded. + + Add the two mapping tables and a one-line adequacy verdict to the Execution Template's Post-Gate section. + +### 7. Status + Atomic Commit (same commit) + +After the gate is green, close the task record **before** creating the commit, then commit code and status together. Never leave `tasks.md` still open after a successful task commit - a crash between those steps is how resume redoes finished work. + +1. Mark the task complete in `tasks.md`. Update requirement traceability in `spec.md` if requirement IDs are used. +2. Create **one** atomic commit that includes the implementation, its tests, and those status/traceability updates. + +Each task gets its own commit immediately after verification. Never batch multiple tasks into one commit. + +**Format ([Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)):** + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +**Types:** + +| Type | When to use | +| ---------- | ------------------------------------------------------- | +| `feat` | New feature or capability | +| `fix` | Bug fix | +| `refactor` | Code change that neither fixes a bug nor adds a feature | +| `docs` | Documentation only | +| `test` | Adding or correcting tests | +| `style` | Formatting, missing semicolons, etc. (no code change) | +| `perf` | Performance improvement | +| `build` | Build system or external dependencies | +| `ci` | CI configuration files and scripts | +| `chore` | Maintenance tasks that don't modify src or test files | + +**Scope:** Feature name or module area, lowercase, e.g., `auth`, `cart`, `api` + +**Description rules:** + +- Imperative mood ("add", not "added" or "adds") +- Lowercase first letter +- No period at the end +- Complete the sentence: "If applied, this commit will _[your description]_" + +**Breaking changes:** Append `!` after type/scope AND add `BREAKING CHANGE:` footer: + +``` +feat(api)!: change authentication endpoint response format + +BREAKING CHANGE: login endpoint now returns JWT in body instead of cookie +``` + +**Examples:** + +``` +feat(auth): add email validation to login form +``` + +``` +fix(cart): prevent negative quantity on item decrement +``` + +``` +refactor(api): extract token refresh logic into service + +Move token refresh from inline handler to dedicated AuthTokenService +for reuse across multiple endpoints. +``` + +**Rules:** + +- One task = one commit +- Description references what was DONE, not what was planned +- Include only files listed in the task - plus the `tasks.md` / `spec.md` status updates for this task +- Never sneak in "while I'm here" changes +- If tests are part of the task, include them in the same commit + +**Deterministic check.** Validate the message before committing: `python3 /scripts/check_commit.py --message ""`. A non-zero exit means fix the format first. This makes the format rule enforceable instead of memory-dependent. + +**Optional git-level guard (git only, no agent dependency).** In a git repo the same check can run on every commit by wiring it as a `commit-msg` hook, so a malformed message is rejected regardless of who or what drives the commit: + +```bash +# from the repo root, one time (resolve to the directory that contains this skill's SKILL.md): +ln -sf /scripts/check_commit.py .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg +``` + +This is a plain git hook, not tied to any editor or assistant. Skip it if the project manages hooks its own way (for example a pre-commit framework); the manual check above still applies. + +### 8. Scope Guardrail + +During implementation, you will notice things that could be improved, refactored, or added. **Do not act on them.** Instead: + +- If it's a bug: surface it to the user (or capture it as a separate task) +- If it's an improvement: add it to the feature's `context.md` under "Deferred Ideas" (or surface it to the user if there is no `context.md`) +- If it's related to the current task: only include it if it's in the "Done when" criteria + +**The heuristic:** "Is this in my task definition?" If no, don't touch it. + +**Blast radius (approval ≠ remote authority):** Approving a spec or tasks authorizes local implementation and local commits only. Before `git push`, force-push, deploy, production DB migration, or any other remote / externally visible / destructive operation, STOP and get an explicit go-ahead for that action - even if Execute was already approved. + +### 9. Feature-Level Validation (after the LAST task - MANDATORY, always runs) + +When the task you just completed is the **last task of the feature** (or of a priority group being delivered on its own, e.g. all P1 tasks), you MUST run feature-level validation before reporting the work as done. **This is not optional and is never prompted - it runs automatically.** Do not stop at the final task's commit. + +**Author ≠ verifier.** An author checking their own work reapplies the mental model that may have produced the gaps. The Verifier is a fresh sub-agent that re-derives coverage from the spec independently - this separation is the quality gate, not a style preference. + +**Layering:** +- Per-task adequacy self-check (steps 5-6): cheap, always runs, author does it, confirms each task in isolation. +- Feature-level validation (step 9): one trustworthy independent gate at completion, always-on, Verifier sub-agent does it. + +**How to delegate to the Verifier:** +Dispatch a fresh sub-agent following the **Verifier** role described in [sub-agents.md](sub-agents.md). Provide it with: +- `spec.md` (ACs = source of truth) +- The git diff surface for this feature (commit range) +- The test files in scope +- `validate.md` as its operating checklist + +**What the Verifier does** (full procedure in [sub-agents.md](sub-agents.md); operating checklist in [validate.md](validate.md)): a spec-anchored coverage check (evidence-or-zero, each asserted value matched to the spec outcome) plus a discrimination sensor (behavior-level mutations run in a scratch state and then discarded), after which it writes `.specs/features/[feature]/validation.md` (PASS/FAIL, per-AC evidence, sensor result, diff range) and returns a compact verdict + ranked gaps in chat. It runs read-only over the real tree and does NOT fix. + +If the Verifier returns FAIL, the orchestrator routes the ranked gaps back to an implementer as fix tasks, then re-dispatches the Verifier - bounded to **3 fix→re-verify iterations** before escalating to the user. + +If you are unsure whether more tasks remain, check `tasks.md`: if every task is marked complete, dispatch the Verifier now. + +--- + +## Execution Template + +```markdown +## Implementing T[X]: [Task Title] + +**Reading**: task definition from tasks.md +**Dependencies**: [All done? ✅ | Blocked by: TY] +**Tests**: [unit/e2e/integration/none] +**Gate**: [quick/full/build] + +### Pre-Implementation (MANDATORY) + +- **Assumptions**: [state explicitly] +- **Files to touch**: [list ONLY these] +- **Success criteria**: [how to verify] + +### Tests: Write tests derived from spec ACs + +- Test file(s): [paths] +- Test count: [N test cases] +- Spec-derived: each test's asserted value maps to spec-defined outcome (or gap flagged) + +### Implement + +[Write minimum code to pass tests] + +- Tests modified: None +- Tests skipped/deleted: None + +### VERIFY: Gate Check + +- Command: [gate check command] +- Result: [X passed, 0 failed] +- Test count: [N - matches planned test count] + +### Post-Gate + +- [x] No SPEC_DEVIATION (or markers added) +- [x] No unnecessary changes made +- [x] Matches existing patterns + +**Test Adequacy Review:** + +*Check A - Sufficient (coverage mapping):* + +| Done-when criterion / spec AC / listed edge case | `file:line` + assertion expression | Spec-defined outcome | Covered? | +| ------------------------------------------------- | ---------------------------------- | -------------------- | -------- | +| [criterion] | `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | [spec value] | ✅ Yes / ⚠️ Gap | + +*Check C - Necessary (reverse mapping):* + +| `file:line` + assertion expression | Maps to (AC / edge case / Done-when criterion) | Keep? | +| ---------------------------------- | ---------------------------------------------- | ----- | +| `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | [requirement or criterion text] | ✅ Keep | + +- [ ] Check A: every criterion covered with `file:line` evidence; spec-defined outcomes matched or gap flagged; per-layer depth met +- [ ] Check B: no shallow assertions; payload/conjunction rule applied to every payload-bearing criterion +- [ ] Check C: every test maps to a requirement - no speculative or unclaimed tests +- [ ] Check D: guideline conformance - [guideline file followed, or "none - strong defaults applied"] + +**Verdict**: [All criteria covered, spec outcomes matched, no shallow assertions, all tests necessary] / [Rewritten: describe what was fixed] + +**Status**: ✅ Complete | ❌ Blocked | ⚠️ Partial +``` + +**After the LAST task:** dispatch the Verifier sub-agent (see step 9 and [sub-agents.md](sub-agents.md)) for independent feature-level validation, including the spec-anchored check and discrimination sensor. Validation always runs automatically - never prompted. Execute is not done until the Verifier reports PASS and the validation report is written, confirmed deterministically by `python3 /scripts/validate_state.py ` (exit non-zero = not done); see [validate.md](validate.md). + +--- + +## Tips + +- **One task at a time** - Focus prevents errors +- **Tools matter** - Wrong MCP = wrong approach +- **Reuses save tokens** - Copy patterns, don't reinvent +- **Status then commit, same commit** - Mark `tasks.md` complete before the atomic commit and include that update in it +- **Stay surgical** - Touch only what's necessary +- **Commit per task** - Clean git history enables bisect and rollback +- **Never "while I'm here"** - Scope creep during implementation is the #1 quality killer +- **Approval is local** - Push, deploy, and other remote/destructive ops need an explicit go-ahead +- **Learn from mistakes** - If something goes wrong, surface it to the user so it informs the next task +- **Don't stop at the last commit** - Feature-level validation (step 9) is the final step of Execute, not optional +- **Plain voice in prose** - Commit bodies and the validation summary follow the writing rules in [coding-principles.md](coding-principles.md): lead with what changed, no filler +- **Validate the commit message** - `python3 /scripts/check_commit.py --message "..."` before committing + +--- + +## Pause / End of Session + +When work is interrupted, paused, or a session ends before the feature is complete: + +1. Open `.specs/STATE.md`. +2. Locate the `## Handoff` section. +3. **Replace only that section's body** with the current snapshot (feature, phase/task, completed, in-progress `file:line`, next step, blockers, uncommitted files, branch). See [memory.md](memory.md) for the exact format. +4. Do NOT touch the `## Decisions` section above it - decisions are written only during Design. + +**Section-scoped write (critical):** Replace the content between the `## Handoff` header and the next `##` header (or end of file). Never overwrite the full file - doing so silently destroys the Decisions log. diff --git a/plugin/skills/tlc-spec-driven/references/lessons.md b/plugin/skills/tlc-spec-driven/references/lessons.md new file mode 100644 index 0000000..a9b63c6 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/lessons.md @@ -0,0 +1,115 @@ +# Lessons - Self-Improving Layer + +**Purpose**: Turn verification failures into reusable, project-local guidance that actually changes future behavior - without the lessons file rotting into a dead log. + +**The split that keeps it alive**: the agent (you) supplies *judgment* - read the failure, phrase the lesson, cite its grounding. The script `scripts/lessons.py` owns everything *mechanical* - IDs, recurrence counting across distinct features, candidate→confirmed promotion, pruning, demotion, and rendering. Hand-kept bookkeeping is exactly what rots, so it is not your job; the script's job. + +**What feeds it**: only the execution signals already produced by the Verifier in [validate.md](validate.md) and written to `.specs/features/[feature]/validation.md`. No signal → no lesson. This is the hard gate: a lesson with no grounding in a real verification outcome is an opinion, and the script refuses it. + +**Scope discipline (critical)**: this layer captures *execution* lessons that are project-local and grounded in a signal. It does **NOT** capture methodology opinions about the SDD process itself ("we should always discuss earlier"). Those are maintainer decisions that ship in a version bump - never auto-written. If a candidate lesson is really about how to run the skill rather than about this codebase, do not record it. + +--- + +## Files + +| File | Owner | Purpose | +| ---- | ----- | ------- | +| `.specs/lessons.json` | script | Canonical machine state. Never hand-edit. | +| `.specs/LESSONS.md` | script (rendered) | Human/agent-readable playbook. Read it; never write it by hand. | +| `/scripts/lessons.py` | package | The only way to mutate lessons. Invoke via the skill directory - never `python3 scripts/lessons.py` from the project root. | + +`confirmed` lessons are the playbook the agent loads. `candidate` lessons are tracked but NOT trusted until corroborated across `promote_threshold` distinct features (default 2). `quarantined` lessons failed when applied and are ignored. + +**Invocation:** resolve `` as the directory that contains this skill's `SKILL.md`, then run `python3 /scripts/lessons.py ...`. The store under `.specs/` is still relative to the project root (use `--root` when cwd differs). + +--- + +## WRITE - distill lessons (runs inside Execute, after validation) + +This is **not a new phase**. It is the final action of the Verifier step in [validate.md](validate.md), grafted onto a step that already always runs. Do it immediately after `validation.md` is written, before reporting completion. + +### When to write + +Walk the just-written `validation.md`. For each **grounded** signal, record one lesson: + +| validation.md signal | `--signal` value | +| -------------------- | ---------------- | +| An acceptance criterion failed or had no evidence | `ac_gap` | +| A discrimination-sensor mutant survived (weak test) | `surviving_mutant` | +| A criterion flagged ⚠️ Spec-precision gap | `spec_precision_gap` | +| A `// SPEC_DEVIATION` marker was added during implement | `spec_deviation` | +| The build-level gate check failed | `gate_fail` | + +If `validation.md` is a clean PASS with no surviving mutants, no spec-precision gaps, and no deviations → **write nothing**. A clean run produces no lessons. This is correct, not a miss. + +### How to write + +For each signal, phrase the lesson as **one terse, actionable, codebase-general sentence** - a rule a future feature could apply, not a restatement of this bug. Then call the script: + +```bash +python3 /scripts/lessons.py add \ + --feature "[feature folder name]" \ + --signal "[signal value from table above]" \ + --source "[file:line | AC id | mutant id | SPEC_DEVIATION ref from validation.md]" \ + --text "[the one-sentence lesson]" \ + --scope "[optional: path/layer/tag, e.g. billing, routes, repo-layer]" +``` + +**Phrasing rules** (they make recurrences actually merge - dedup is exact-after-normalization, not semantic): + +- Write the general rule, not the incident. ✅ `"Assert the exact persisted status value, not just that a status field exists"` ❌ `"The subscription test on line 88 was too weak"`. +- Be canonical and terse. Two lessons that mean the same thing must read the same way, or the script counts them as different and neither gets promoted. +- One lesson per signal. Don't bundle. + +`--source` is **mandatory**. The script exits non-zero if it is empty - that is the grounding gate working, not an error to route around. + +### Self-check (do not skip) + +After distilling, if `validation.md` contained any FAIL, surviving mutant, spec-precision gap, or SPEC_DEVIATION but you recorded zero lessons, state plainly in chat: *"Validation had signal X but no lesson was recorded - recording now / here's why it's out of scope."* Silent skipping is how the file dies. + +### Demotion + +If a `confirmed` lesson was loaded for this feature (see READ below) and the *same* failure recurred anyway, the guidance is not working: + +```bash +python3 /scripts/lessons.py penalize --id L-NNN +``` + +Two penalties quarantine it. Use sparingly and only on real repeats. + +--- + +## READ - load lessons (runs at Specify and Design) + +A lessons file nobody reads is dead by definition. Loading is **mandatory**, not optional. + +At the start of **Specify** (and again at **Design** for Large/Complex), load the confirmed lessons relevant to this feature: + +```bash +# All confirmed lessons: +python3 /scripts/lessons.py list --status confirmed + +# Or filter by the area this feature touches: +python3 /scripts/lessons.py list --status confirmed --scope billing +python3 /scripts/lessons.py list --status confirmed --query "idempotency" +``` + +Apply the returned lessons as guidance while writing the spec / design. Do **not** load `candidate` or `quarantined` lessons as guidance - they are not trusted. Keep the loaded set small; this runs inside the <40k token budget. + +--- + +## Fallback when code execution is unavailable + +Some harnesses cannot run Python. Only then: maintain `.specs/LESSONS.md` by hand, following the exact same rules - grounded entries only, candidate→confirmed after 2 distinct features, prune stale candidates. **This path is degraded**: hand bookkeeping is the failure mode this layer exists to avoid, so prefer the script wherever a code tool exists. State once in chat that you are in the no-script fallback so the user knows accounting is best-effort. + +--- + +## Disable + +This layer is additive and self-gating (no signal → no write). To turn it off for a project, delete `.specs/lessons.json` and `.specs/LESSONS.md` and skip the WRITE/READ steps. The core Specify→Design→Tasks→Execute flow is unaffected. + +--- + +## Known limitation + +Deduplication is exact-after-normalization (Unicode casefold, diacritic-stripped, punctuation-stripped, any-script alnum preserved) - there are no embeddings (stdlib-only, zero-dependency by design). Near-duplicate lessons phrased differently will not merge and will each sit as separate candidates that never promote. Mitigation: follow the phrasing rules above. A future version may add embedding-based dedup. diff --git a/plugin/skills/tlc-spec-driven/references/memory.md b/plugin/skills/tlc-spec-driven/references/memory.md new file mode 100644 index 0000000..02b5bf6 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/memory.md @@ -0,0 +1,144 @@ +# Memory Layer + +**File:** `.specs/STATE.md` + +A single file with two section-scoped parts. Each section has its own lifecycle; writes are always targeted - never whole-file overwrites. + +--- + +## Sections + +### `## Decisions` - append-only log + +Records **project-level** decisions only: conventions, patterns, constraints, or cross-cutting technology choices that future features must follow or supersede. + +**Not project-level → stays in the feature's `design.md` Tech Decisions table.** +Heuristic: would a different feature need to know about this? If yes → project-level. If no → feature-local. + +**Record sparingly - the log stays useful only by staying small.** Even a project-level decision earns an `AD-NNN` entry only when all three hold: + +1. **Hard to reverse** - changing course later carries real cost. +2. **Surprising without context** - a future reader will look at the result and wonder "why did they do it this way?" +3. **The product of a real trade-off** - there were genuine alternatives and you chose one for specific reasons. + +If any one is missing, skip it: an easily-reversed choice you will just reverse; an unsurprising one nobody questions; a no-alternative choice records nothing beyond "we did the obvious thing." What typically qualifies: architectural shape, integration patterns between areas, technology choices that carry lock-in, boundary and ownership decisions, and deliberate deviations from the obvious path. A choice that clears all three but is only feature-local still stays in `design.md`. + +**Format** (one entry per decision): + +```markdown +## Decisions + +### AD-001 +- **Decision**: [what was decided - one sentence] +- **Reason**: [why this option was chosen] +- **Trade-off**: [what was given up] +- **Scope**: [which features / packages / layers this governs] +- **Date**: YYYY-MM-DD +- **Status**: active | superseded by AD-NNN +``` + +**Supersession rule:** When a new decision replaces an old one, append a new `AD-NNN` entry and update the old entry's `status` field to `superseded by AD-NNN`. Never delete old entries - the history is the audit trail. + +--- + +### `## Handoff` - pause snapshot (~500 tokens, overwritten each pause) + +Captures mid-task / in-flight state so work can resume without re-reading the full task history. It complements `tasks.md` and git evidence: on resume, the Handoff is a starting hypothesis that must be reconciled against the real branch, commits, and working tree (see Resume below). + +**Format:** + +```markdown +## Handoff + +- **Feature**: [feature name / .specs path] +- **Phase / Task**: [e.g., Phase 2 / T4 - implement repository layer] +- **Completed**: [comma-separated task IDs or "none"] +- **In-progress** (file:line): [e.g., `src/billing/subscription.service.ts:88` - mid-write] +- **Next step**: [one sentence - exactly what to do next] +- **Blockers**: [none | description] +- **Uncommitted files**: [list or "none"] +- **Branch**: [git branch name] +``` + +--- + +## File shape + +```markdown +# STATE + +## Decisions + +[AD-NNN entries…] + +## Handoff + +[latest snapshot…] +``` + +If the file does not yet exist, create it with both section headers and empty bodies. + +--- + +## Read / Write Triggers + +| Trigger | Section | Operation | +| ------- | ------- | --------- | +| Design phase, Step 1 (Load Context) | `## Decisions` | **Read** - conform to active decisions or supersede | +| Design phase, Tech Decisions step | `## Decisions` | **Append** - only for project-level decisions | +| Pause work / end of session | `## Handoff` | **Replace** - overwrite Handoff section only | +| Resume work / start of session | `## Handoff` | **Read** - load snapshot, then reconcile with git before acting | +| Resume work / start of session | `## Decisions` | **Read** - re-confirm active constraints before designing | + +--- + +## Section-scoped write rule (critical) + +One file holds two lifecycles. Writes MUST target their section only: + +- **Design appends** to `## Decisions`. It MUST NOT touch `## Handoff`. +- **Pause replaces** `## Handoff`. It MUST NOT rewrite, reorder, or drop any entry in `## Decisions`. + +The correct technique: locate the target section header, replace only the content between it and the next `##` header (or end of file). Never overwrite the full file. + +Violating this rule causes one of two failures: +1. A pause write clobbers the decisions log → decisions are silently lost. +2. A design append touches the handoff snapshot → mid-task state is corrupted. + +Both are silent data loss. The section-scoped write rule is the single correctness invariant of this memory layer. + +--- + +## Pause / Resume Procedure + +### Pause + +1. Locate the `## Handoff` section in `.specs/STATE.md`. +2. Replace its body (everything between `## Handoff` and the next `##` or EOF) with the current snapshot. +3. Do NOT modify anything above or before `## Handoff`. +4. Commit or stash outstanding changes as appropriate. + +### Resume + +1. Read `.specs/STATE.md` - both sections. +2. Re-confirm active decisions from `## Decisions` - nothing superseded since last session? +3. Read `## Handoff` - treat it as a **hypothesis** for feature, phase/task, next step, blockers, uncommitted files, branch - not as ground truth by itself. +4. **Reconcile with git before editing anything:** + - Current branch vs Handoff `Branch` + - `git status --porcelain` (uncommitted / unexpected paths) + - Recent commits on the branch (messages and touched files) + - `tasks.md` completion marks and, when present, gate evidence / commit references +5. **Resolve conflicts with evidence, not narrative:** + - A task with a green gate and an atomic commit already on the branch → do **not** redo it; mark it complete in `tasks.md` if the file still shows it open, then continue from the next incomplete task + - Partial unverified work in the working tree → preserve it, re-run the relevant gate, then finish the status+commit cycle + - Stale or missing Handoff → rebuild next-step from git + `tasks.md`, then propose that to the user + - Unexplained local changes you cannot map to the current task → STOP and ask; do not discard them +6. Propose the reconciled next step to the user before writing any code. + +--- + +## AD-NNN numbering + +- Numbers are sequential, project-scoped, and permanent - never reused. +- The counter starts at `AD-001`. Check existing entries before assigning the next number. +- If `.specs/STATE.md` does not exist, the first decision is `AD-001`. diff --git a/plugin/skills/tlc-spec-driven/references/specify.md b/plugin/skills/tlc-spec-driven/references/specify.md new file mode 100644 index 0000000..4e30387 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/specify.md @@ -0,0 +1,228 @@ +# Specify + +**Goal**: Capture WHAT to build with testable, traceable requirements. + +If the feature has ambiguous gray areas (multiple valid approaches for user-facing behavior), the agent will automatically trigger the [discuss gray areas](discuss.md) process within this phase. For clear, well-defined features, it goes straight to the next phase. + +## Implicit-Requirement Dimensions + +The canonical rubric for requirements that are easy to miss. Referenced by [discuss.md](discuss.md) - defined here, not duplicated. + +| Dimension | What to cover | +| --------- | ------------- | +| Input validation & bounds | Limits, formats, sanitization | +| Failure / partial-failure states | Timeouts, partial saves, rollbacks | +| Idempotency / retry / duplicate handling | Safe retries, dedup keys | +| Auth boundaries & rate limits | Who can call what, throttle rules | +| Concurrency / ordering | Race conditions, ordering guarantees | +| Data lifecycle / expiry | TTL, archival, deletion | +| Observability | Logging, metrics, tracing hooks | +| External-dependency failure | Circuit breakers, fallbacks | +| State-transition integrity | Valid transitions, guards | + +--- + +## Process + +### 1. Clarify Requirements + +**Load confirmed lessons first:** Before clarifying, load the project's confirmed lessons so past verification failures shape this spec instead of repeating. Run `python3 /scripts/lessons.py list --status confirmed` (optionally `--scope [area]` or `--query [term]` for the area this feature touches) and apply what comes back as guidance. Load only `confirmed` - never `candidate` or `quarantined`. If no store exists yet or no code tool is available, skip silently. See [lessons.md](lessons.md). + +**Lightweight context scan first (Knowledge Verification Chain Step 1):** Before asking questions, briefly scan existing code, patterns, and neighboring features relevant to this feature. Use what you find to ground your clarifying questions in reality - not to constrain the spec to current implementation. Keep it lightweight (stay within the <40k token budget; reuse the chain, no new machinery). The spec captures WHAT is needed, not only what exists. + +You are a thinking partner, not an interviewer. Start open - let the user dump their mental model. Follow the energy: whatever they emphasize, dig into that. + +Ask conversationally (not as a checklist): + +- "What problem are you solving?" +- "Who is the user and what's their pain?" +- "What does success look like?" + +If needed: + +- "What are the constraints (time, tech, resources)?" +- "What is explicitly out of scope?" + +**Facts you look up; decisions you ask.** Anything discoverable by reading the environment (the codebase, config, docs, existing conventions) you resolve yourself through the Knowledge Verification Chain - do not spend the user's attention asking for it. Reserve questions for genuine decisions that are the user's to make: scope, priorities, product behavior, trade-offs. A question you could have answered by reading the code erodes trust and wastes a turn. + +**Challenge vagueness.** Never accept fuzzy answers. "Good" means what? "Users" means who? "Simple" means how? Make the abstract concrete: "Walk me through using this." "What does that actually look like?" + +**Know when to stop - then run the dimensions sweep.** When you understand what they're building, why, who it's for, and what done looks like, run a closing **implicit-requirement dimensions sweep** before offering to proceed: + +- **Large / Complex:** Cover every dimension above - each must resolve to a requirement OR an explicit `N/A because [reason]`. No blank entries allowed. +- **Medium:** Cover only dimensions obviously present for this feature's domain; collapse the rest to a single `remaining dimensions N/A for this scope`. +- **Small:** Skip the sweep entirely. + +The `N/A because...` escape is mandatory - it prevents inventing requirements to fill the checklist. Bound the sweep to THIS feature's scope; never add requirements outside the feature boundary. + +### 2. Capture User Stories with Priorities + +**P1 = MVP** (must ship), **P2** (should have), **P3** (nice to have) + +Each story MUST be **independently testable** - you can implement and demo just that story. + +### 3. Write Acceptance Criteria (EARS notation) + +Write every acceptance criterion in **EARS** (Easy Approach to Requirements Syntax). Each criterion resolves to exactly one pattern, which keeps it unambiguous and directly testable. Choose the pattern that fits the requirement instead of forcing everything into a single shape: + +| Pattern | Keyword | Template | Use for | +| ------- | ------- | -------- | ------- | +| Ubiquitous | (none) | The [system] SHALL [response] | Always-on invariants and constraints | +| Event-driven | WHEN | WHEN [trigger] THEN the [system] SHALL [response] | A response to a discrete trigger | +| State-driven | WHILE | WHILE [state] the [system] SHALL [response] | Behavior that holds during a state | +| Optional-feature | WHERE | WHERE [feature is present] the [system] SHALL [response] | Behavior gated behind an optional capability or flag | +| Unwanted-behavior | IF / THEN | IF [undesired condition] THEN the [system] SHALL [response] | Errors, failures, invalid input, timeouts | +| Complex | combination | WHILE [state], WHEN [trigger] the [system] SHALL [response] | Richer behavior combining the above | + +**Why patterns beat one shape:** failure states, state transitions, and optional behavior become first-class criteria instead of footnotes squeezed into WHEN/THEN. The patterns map onto the implicit-requirement dimensions above: state-transition integrity to State-driven; failure and external-dependency failure to Unwanted-behavior; feature flags to Optional-feature. + +**Rules:** one requirement per criterion (never bundle two behaviors); use concrete values (a specific status code, a specific message, a bound) rather than "quickly" or "gracefully"; every criterion contains a SHALL and is measurable. `python3 /scripts/validate_spec.py` flags any criterion without a SHALL and any that matches no recognized pattern. + +### 4. Requirement Closure Gate (before confirm) + +Before presenting the spec for confirmation, run the three checks below. The spec is not presentable for confirmation until every item is resolved or assumption-logged - this is the guarantee that no requirement leaves the spec silently unclear. + +**Scope-tiered:** Large/Complex = full gate; Medium = resolve obvious ambiguities, log the rest as assumptions; Small = skip entirely (consistent with skipping the sweep). + +1. **Unambiguity + precision (hard).** Every AC must (a) have a single interpretation and (b) define a precise, spec-defined expected outcome. Any AC that fails either check: resolve with the user, split it, or log it as an explicit assumption with the chosen interpretation and rationale. No AC proceeds readable two ways or with an undefined outcome. + +2. **Open-questions / assumptions closure.** Enumerate every unresolved decision that surfaced during clarification. Each must be either (a) resolved with the user OR (b) recorded as an **assumption** (chosen default + rationale) in the spec's Assumptions & Open Questions section. Nothing proceeds unmarked. + +3. **Declined gray areas become assumptions.** Any gray area the user declined to discuss or that went undiscussed is written to the spec's Assumptions & Open Questions section (agent's chosen default + rationale) - never silently dropped. See [discuss.md](discuss.md). + +Fix inline. This gate is bounded to THIS feature's stated dimensions and actual behavior - never to "anything imaginable." The Out of Scope table and anti-scope-creep rules remain the counterweights: the gate clarifies existing requirements, it never invents new ones. + +**Deterministic backing (run before you present the spec).** The structural half of this gate is enforced by a script so it cannot drift when a step is forgotten: `python3 /scripts/validate_spec.py ` checks that required sections exist, every AC is EARS-shaped (has a SHALL), no Assumptions row has an empty default or rationale, and requirement IDs are well-formed. A non-zero exit means fix before confirming. The script checks structure; you still own the judgment calls (is the interpretation right, is the outcome precise). If no code-execution tool is available, run the same checks by reading the spec. + +--- + +## Template: `.specs/features/[feature]/spec.md` + +```markdown +# [Feature Name] Specification + +## Problem Statement + +[Describe the problem in 2-3 sentences. What pain point are we solving? Why now?] + +## Goals + +- [ ] [Primary goal with measurable outcome] +- [ ] [Secondary goal with measurable outcome] + +## Out of Scope + +Explicitly excluded. Documented to prevent scope creep. + +| Feature | Reason | +| ----------- | -------------- | +| [Feature X] | [Why excluded] | +| [Feature Y] | [Why excluded] | + +--- + +## Assumptions & Open Questions + +Every ambiguity is resolved or recorded here - nothing is left silently unclear. + +| Assumption / decision | Chosen default | Rationale | Confirmed? | +| --------------------- | --------------- | --------- | ---------- | +| [ambiguity] | [what we'll do] | [why] | [y/n] | + +**Open questions:** none - all resolved or logged above (required before the spec is confirmed). + +--- + +## User Stories + +### P1: [Story Title] ⭐ MVP + +**User Story**: As a [role], I want [capability] so that [benefit]. + +**Why P1**: [Why this is critical for MVP] + +**Acceptance Criteria** (each line is one EARS pattern): + +1. WHEN [user action/event] THEN system SHALL [expected behavior] +2. IF [invalid input / failure] THEN system SHALL [graceful handling] +3. WHILE [state holds] system SHALL [behavior during that state] +4. The system SHALL [always-on invariant] + +**Independent Test**: [How to verify this story works alone - e.g., "Can demo by doing X and seeing Y"] + +--- + +### P2: [Story Title] + +**User Story**: As a [role], I want [capability] so that [benefit]. + +**Why P2**: [Why this isn't MVP but important] + +**Acceptance Criteria**: + +1. WHEN [event] THEN system SHALL [behavior] +2. WHEN [event] THEN system SHALL [behavior] + +**Independent Test**: [How to verify] + +--- + +### P3: [Story Title] + +**User Story**: As a [role], I want [capability] so that [benefit]. + +**Why P3**: [Why this is nice-to-have] + +**Acceptance Criteria**: + +1. WHEN [event] THEN system SHALL [behavior] + +--- + +## Edge Cases + +Edge cases are usually unwanted-behavior (IF/THEN) or boundary (WHEN) criteria: + +- IF [error scenario] THEN system SHALL [graceful handling] +- IF [unexpected input] THEN system SHALL [validation response] +- WHEN [boundary condition] THEN system SHALL [behavior] + +--- + +## Requirement Traceability + +Each requirement gets a unique ID for tracking across design, tasks, and validation. + +| Requirement ID | Story | Phase | Status | +| -------------- | ----------- | ------ | ------- | +| [FEAT]-01 | P1: [Story] | Design | Pending | +| [FEAT]-02 | P1: [Story] | Design | Pending | +| [FEAT]-03 | P2: [Story] | - | Pending | + +**ID format:** `[CATEGORY]-[NUMBER]` (e.g., `AUTH-01`, `CART-03`, `NOTIF-02`) + +**Status values:** Pending → In Design → In Tasks → Implementing → Verified + +**Coverage:** X total, Y mapped to tasks, Z unmapped ⚠️ + +--- + +## Success Criteria + +How we know the feature is successful: + +- [ ] [Measurable outcome - e.g., "User can complete X in < 2 minutes"] +- [ ] [Measurable outcome - e.g., "Zero errors in Y scenario"] +``` + +--- + +## Tips + +- **P1 = Vertical Slice** - A complete, demo-able feature, not just backend or frontend +- **EARS is code** - If you can't write a criterion as a test, rewrite it; pick the pattern (WHEN / WHILE / WHERE / IF / ubiquitous) that fits +- **Requirement IDs are mandatory** - Every story maps to trackable IDs +- **Edge cases matter** - What breaks? What's empty? What's huge? +- **Out of Scope prevents creep** - If it's not here, it doesn't get built +- **Closure gate before confirm** - Three checks: unambiguity + precision, open-questions/assumptions closure, declined gray areas logged; scope-tiered; bounded to stated dimensions; never invents requirements +- **Confirm after the gate passes** - Present the spec for user confirmation only after the closure gate passes (no unresolved-and-unmarked items remain) and `validate_spec.py` exits clean; user approves spec before moving to discuss phase diff --git a/plugin/skills/tlc-spec-driven/references/sub-agents.md b/plugin/skills/tlc-spec-driven/references/sub-agents.md new file mode 100644 index 0000000..618b396 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/sub-agents.md @@ -0,0 +1,147 @@ +# Sub-Agent Delegation + +Full mechanics for phase-batch workers and the Verifier sub-agent used during Execute. + +## Phase-Batch Workers + +**Two layers - keep them distinct:** + +- **Phase** = the semantic / dependency unit (Foundation → Core → Integration), authored during Tasks. Indivisible. +- **Batch** = the execution / logistics unit - one or more *consecutive whole phases* assigned to a single worker. + +Conflating the two (one worker per phase) is what fragments execution: a feature's dependency-layer count has nothing to do with the ideal per-worker workload. Batching by task budget separates the two concerns without breaking phases. + +**Trigger:** Count total tasks across all phases. If the feature packs into **more than one batch** (> ~8 tasks), offer the user phase-batch sub-agents before starting Execute. If it fits a single batch (≤ ~8 tasks), execute inline in the main window - no sub-agents spawned. + +**Batching algorithm (task budget ≈ 7 tasks/worker, phase-aligned):** + +The benchmarked sweet spot is ~7 tasks of context per worker (~20 tasks → 3 workers). Pack whole phases into that budget: + +1. Count total tasks `T`. +2. If `T ≤ ~8` → inline, no sub-agents. +3. Otherwise walk phases **in order**, accumulating whole phases into the current batch. When the batch's running task count reaches ~7 **and** phases remain, close the batch and start the next. +4. **Never split a phase** across workers - the cut only ever lands on a phase boundary. This preserves dependency ordering and keeps a phase's tasks + shared context in one worker. +5. If the final batch is a lone tail (1-2 tasks), fold it into the previous batch. + +Result ≈ `ceil(T / 7)` workers, scaling linearly. Unevenness is absorbed by greedy packing - phases never need to divide evenly. Worked examples (20 tasks): + +- Phases `[3,3,3,3,4,4]` → `{P1+P2=6, P3+P4=6, P5+P6=8}` = **3 workers** +- Phases `[8,2,2,8]` → `{P1=8, P2+P3=4, P4=8}` = **3 workers** (no even split needed) +- Phases `[5,5,5,5]` → `{P1+P2=10, P3+P4=10}` = **2 workers** (phases too coarse to hit 3 - see below) + +**Coarse-phase caveat:** Because the cut lands only on phase boundaries, very coarse phases limit how finely you can pack. If a single phase alone exceeds ~1.5× the budget (~10+ tasks), that is a Tasks-authoring smell - split it into real sub-phases during Tasks (at a genuine dependency/cohesion boundary), never at dispatch time. + +**Offer-then-confirm (never auto-spawn):** + +> "This feature has [T] tasks across [N] phases. I can pack them into [K] sub-agents (~7 tasks each, whole phases per worker) - every worker runs its phases in order, reports a compact summary, and the orchestrator advances to the next batch. This keeps the main window lean without over-fragmenting. Want to proceed that way?" + +The user must explicitly accept. If they decline (or if the feature fits one batch), execute inline. + +**Execution model - one worker per task-budgeted batch, sequential:** + +``` +Phases 1+2 (7 tasks) ------→ Batch Worker 1 ------→ compact summary ------→ orchestrator updates tasks.md +Phases 3+4 (6 tasks) ------→ Batch Worker 2 ------→ compact summary ------→ orchestrator updates tasks.md +Phase 5 (7 tasks) ------→ Batch Worker 3 ------→ compact summary ------→ orchestrator updates tasks.md +... +``` + +Batches run strictly sequentially: a batch never starts until the previous batch's summary shows all its tasks complete. + +**What a batch worker receives:** + +- The task definitions for **every** phase in its batch (from `tasks.md`) +- The Test Coverage Matrix and Gate Check Commands (from `tasks.md`) +- `references/coding-principles.md` +- Relevant `spec.md` and `design.md` context for the feature (not all specs) + +**What a batch worker does:** + +Executes ALL tasks in its assigned batch **in order** - finishing every task in one phase before starting the next phase in the batch - following the `implement.md` cycle for each task (implement → gate → atomic commit). It does NOT spawn further sub-agents. After completing all tasks in the batch, the worker reports a **compact summary** to the orchestrator: + +``` +Batch (phases [N]-[M]) complete: +- Tasks done: [list with commit hashes] +- Tests: [N passed, 0 failed] +- Deviations/blockers: [none | description] +``` + +No raw logs, no full test output - only the above fields keep the main context clean. + +**No nesting:** Batch workers execute their tasks themselves. They never spawn sub-sub-agents. Execution is strictly sequential within and across batches - there is no intra-phase or intra-batch parallelism. + +**The orchestrating agent's role during Execute:** + +1. Count total tasks and pack phases into task-budgeted batches (~7 tasks each) - if that yields more than one batch, offer batch sub-agents and wait for the user to accept +2. Dispatch the next batch to a worker (or execute inline if not using sub-agents) +3. Receive the compact summary +4. Update `tasks.md` with results +5. If all tasks in the summary show complete: dispatch the next batch +6. If a task failed: the worker has already stopped; decide fix/escalate before dispatching the next batch + +**Failure handling:** If a task in a batch fails (gate does not pass, blocker hit), the worker stops and includes the failure in its summary. The next batch does not start until the current batch's summary shows all tasks complete. The orchestrator decides: fix and re-run, or escalate to the user. + +**Context sizing signal:** If a batch's task list would likely push the worker's context beyond ~40k tokens, close the batch at an earlier phase boundary (fewer phases per worker). If a *single* phase alone would blow the budget, that phase is too coarse - split it during Tasks per the granularity guidance in `references/tasks.md`. + +--- + +## Verifier Sub-Agent + +**Always-on, never prompted - one per feature completion.** The Verifier is a separate role from the batch worker. It runs once - after the last task of the feature is committed - as an independent quality gate, dispatched automatically by the orchestrator. It is **not** gated behind the batching offer; it always runs. Do NOT ask the user whether to run validation; it is mandatory. + +**Author ≠ verifier:** The agent (or batch worker) that wrote the code and tests is the author. The Verifier is a fresh sub-agent dispatched by the orchestrator after the final commit. It does not inherit the author's context, mental model, or assumptions. This separation is what makes the gate trustworthy. + +**What the Verifier receives:** +- `spec.md` for the feature (ACs = source of truth) +- The git diff surface for the feature (scoped to the feature branch or commit range) +- The test files in scope +- `references/validate.md` as its operating checklist + +**What the Verifier does (full process in `validate.md`):** +1. **Spec-anchored coverage check** - re-derives coverage evidence-or-zero: every AC traced to `file:line` + assertion expression. For each covered criterion, confirms the test's asserted value matches the **spec-defined expected outcome** (not just that an assertion exists). Where the spec does not define a precise outcome, flags a **spec-precision gap** rather than passing silently. +2. **Discrimination sensor** - injects a small behavior-level fault (flip a condition, change a return value, off-by-one, remove a required side effect) in an **isolated scratch** (temporary `git worktree` or temp file copies - never `git stash`), runs the relevant tests there, confirms they FAIL (kill the mutant), discards the scratch, and verifies the real worktree's `git status --porcelain` matches the pre-sensor baseline. Tiered by risk: lightweight (1-3 mutations) for standard features; expanded (≥5 mutations or full mutation tooling) for P0/critical paths. Surviving mutants become fix tasks. +3. Applies the **payload/conjunction rule**: checks payload fields are asserted on value/state, not just that the call occurred. +4. **Writes the persisted report** to `.specs/features/[feature]/validation.md` - PASS/FAIL, per-AC evidence (`file:line` + assertion + spec outcome), sensor result (killed/survived per mutation), gate exit results, diff/commit range. +5. **Returns a compact verdict in chat** to the orchestrator. +6. Does **NOT** write, modify, or fix any code or tests - the real working tree is never mutated (sensor mutations run in scratch state only). + +**What the Verifier reports back (compact chat format):** +``` +## Validation: [feature name] - [PASS ✅ | FAIL ❌] + +**Spec-anchored check**: [N/N ACs matched spec outcome | M spec-precision gaps flagged] +**Gate**: [X passed, 0 failed] +**Sensor**: [N mutations injected, N killed, N survived] +**Report**: `.specs/features/[feature]/validation.md` + +**Ranked gaps** (if FAIL): +1. [Gap description] - [AC or criterion] - [file:line or "no evidence"] +2. ... +``` + +**Failure handling:** The orchestrator routes the ranked gaps to an implementer as fix tasks, then re-dispatches the Verifier. This fix→re-verify loop is bounded to a maximum of **3 iterations**. If gaps remain after 3 iterations, escalate to the user. + +**Standalone fallback:** When running without sub-agents (a single agent executing the full feature), run `validate.md` as an independent fresh-eyes pass - re-read `spec.md` and the diff from scratch, apply evidence-or-zero, run the spec-anchored check and discrimination sensor, write the report file, then run `python3 /scripts/validate_state.py ` to confirm the report is a real PASS, and report PASS/FAIL before marking the feature done. + +--- + +## Model Tier per Role + +**Applies only if the harness can assign a model per sub-agent.** If it cannot, ignore this section and run everything on the default model - the workflow is correct either way. The point is to spend high-reasoning capacity where ambiguity and consequence are high, and a faster tier where the work is mechanical, instead of paying top-tier cost uniformly. + +Judge the tier by the work in front of the role, not by the role's title: + +| Role / work | Characteristic | Suggested tier | +| ----------- | -------------- | -------------- | +| Design phase | High ambiguity, hard-to-reverse structural decisions | High-reasoning | +| Batch worker - core-domain or high-ambiguity phase | Non-obvious logic, tricky edge cases, novel integration | High-reasoning | +| Batch worker - mechanical phase | Entities, DTOs, config, wiring, straightforward CRUD against a settled pattern | Faster / cheaper | +| Verifier | Adversarial reasoning: designs mutations, re-derives coverage, judges outcome precision | Mid-to-high | +| Specify / Tasks authoring | Structured but judgment-heavy | Mid-to-high | + +**Rules of thumb:** + +- When unsure, size up, not down. An under-powered worker on ambiguous logic produces gaps the Verifier then has to catch - more expensive than paying for reasoning once. +- The Verifier is never the cheapest tier; a weak Verifier defeats the author ≠ verifier gate. +- Set the tier per batch, from that batch's phases. A feature can mix tiers across batches. +- This is advisory metadata only. No gate, commit, or verification step depends on it. diff --git a/plugin/skills/tlc-spec-driven/references/tasks.md b/plugin/skills/tlc-spec-driven/references/tasks.md new file mode 100644 index 0000000..73fff97 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/tasks.md @@ -0,0 +1,451 @@ +# Tasks + +**Goal**: Break into GRANULAR, ATOMIC tasks. Clear dependencies. Right tools. Sequential phase execution plan. + +**Skip this phase when:** There are ≤3 obvious steps. In that case, tasks are implicit - go straight to Execute and list them inline in your implementation plan. + +## Why Granular Tasks? + +| Vague Task (BAD) | Granular Tasks (GOOD) | +| ---------------- | --------------------------------- | +| "Create form" | T1: Create email input component | +| | T2: Add email validation function | +| | T3: Create submit button | +| | T4: Add form state management | +| | T5: Connect form to API | +| "Implement auth" | T1: Create login form | +| | T2: Create register form | +| | T3: Add token storage utility | +| | T4: Create auth API service | +| | T5: Add route protection | + +**Benefits of granular:** + +- **Agents don't err** - Single focus, no ambiguity +- **Easy to test** - Each task = one verifiable outcome +- **Clean commits** - Each task = one atomic, revertable commit +- **Errors isolated** - One failure doesn't block everything + +**Rule**: One task = ONE of these: + +- One component +- One function +- One API endpoint +- One file change + +--- + +## Process + +### 1. Review Design + +Read `.specs/features/[feature]/design.md` before creating tasks. + +### 1.5. Generate the Test Coverage Matrix (ALWAYS) + +This step ALWAYS runs - there is no precondition. Decide which of two paths to take, then generate the three sections below. + +**Step 0 - Read project quality/testing guidelines (ALWAYS, before anything else).** + +Before sampling tests or inferring anything, scan the project for documented quality and testing standards. Stack-agnostic sources to check (illustrative, not exhaustive): + +- Agent/AI convention files, if the repo has any: `AGENTS.md` (the vendor-neutral standard) and any tool-specific rules file or rules directory the project happens to use +- Contributor guides: `CONTRIBUTING.md`, `docs/` (testing, quality, or standards subdocs), README testing section +- Tool configuration: coverage thresholds in the test runner config (e.g., `jest.config.*`, `vitest.config.*`, `pytest.ini`, `.nycrc`, `Makefile` coverage targets, CI coverage gates) + +**If guidelines are found:** the Coverage Expectation (see matrix below) conforms to them. Existing test samples fill gaps in style/location/framework only. Cite the specific files found in the matrix provenance note. + +**If no guidelines are found:** apply the strong default - cover every spec AC and every listed edge case; domain/business logic maps 1:1 to spec ACs; routes/e2e cover happy + edge + error paths. This default may exceed the current repo's depth, which is intentional. + +**Decision:** + +- **Existing tests in the repo** → infer the matrix and gate commands by sampling the codebase. +- **No tests at all** → ask the user: "What test types will this project use (unit / integration / e2e / none)? What commands run them?" + +**How to infer (path 1 - existing tests):** + +1. **Sample test files.** Locate 5-10 existing test files. Map each file's location relative to its source file to identify which code layers are exercised and at what level (unit, integration, e2e). Use these samples for style, location patterns, framework, and test type - and as a **floor** (never produce tests less thorough than existing ones for the same layer). Existing tests are NOT a ceiling on thoroughness; the thoroughness target comes from the spec ACs, listed edge cases, and guidelines (or strong default). The Coverage Expectation column captures the target per layer. +2. **Discover commands from the repo.** Do NOT invent commands and do NOT assume an ecosystem. Read the project's own build/task manifests, test config, and CI workflows to extract the actual commands - for example: `package.json` / `project.json` (JS/TS), `Makefile`, `pyproject.toml` / `tox.ini` / `pytest` (Python), `Cargo.toml` (Rust), `go test` invocations (Go), `pom.xml` / `build.gradle` (Java/Kotlin), `Gemfile` / `Rakefile` (Ruby), `composer.json` (PHP), `.github/workflows` / `.gitlab-ci.yml`. The list is illustrative; detect what this repo actually uses. Capture the **linter/formatter** command too (e.g. the configured `lint`/`format`/`typecheck` script, or a `.pre-commit-config`, `.golangci.yml`, `ruff`/`eslint`/`biome` config) - the Build gate runs it alongside the tests. + +**Output contract - render these two sections verbatim into `tasks.md`** (the exact headings downstream phases reference): + +--- + +## Test Coverage Matrix + +> Generated from codebase, project guidelines, and spec - confirm before Execute. Guidelines found: [list files, e.g. `AGENTS.md`, `jest.config.ts` - or "none - strong defaults applied"]. + +| Code Layer | Required Test Type | Coverage Expectation | Location Pattern | Run Command | +| ---------- | ------------------ | -------------------- | ---------------- | ----------- | +| [layer] | [unit/integration/e2e/none] | [depth target for this layer] | [glob or path pattern] | [command] | + +**Coverage Expectation values** - set from guidelines first; use strong defaults when no guideline applies: + +| Layer type | Strong default (no guideline) | +| ---------- | ----------------------------- | +| Domain / business-logic (service, use-case, domain model) | All branches; 1:1 to spec ACs; every listed edge case has a test | +| Route / controller / e2e / integration | All routes in scope: happy path + every listed edge case + error/failure paths | +| Repository / data-access | Key query paths + error handling; infer from existing repo tests | +| Entity / config / schema | none - build gate only | + +These defaults may exceed the current repo's depth. That is intentional - they are a **target**, not a reflection of what already exists. + +*Example (filled in):* + +| Code Layer | Required Test Type | Coverage Expectation | Location Pattern | Run Command | +| ---------- | ------------------ | -------------------- | ---------------- | ----------- | +| Service | unit | All branches; 1:1 to spec ACs; all listed edge cases | `src/**/__test__/*.spec.ts` | `yarn test:unit` | +| Repository | integration | Key query paths + error paths | `src/**/__test__/*.e2e-spec.ts` | `yarn test:e2e` | +| Controller/Resolver | e2e | All routes: happy + edge + error | `src/**/__test__/*.e2e-spec.ts` | `yarn test:e2e` | +| Entity / Config | none | - (build gate only) | - | build gate only | + +## Gate Check Commands + +> Generated from codebase - confirm before Execute. + +| Gate Level | When to Use | Command | +| ---------- | ----------- | ------- | +| Quick | After tasks with unit tests only | [unit test command] | +| Full | After tasks with e2e/integration tests | [unit + e2e commands] | +| Build | After phase completion or config/entity-only tasks | [build + lint + all tests] | + +--- + +**Co-located tests:** Every task that creates or modifies a code layer with a required test type MUST include writing/updating those tests in the same task. Tests are NOT separate tasks. The tests must satisfy the layer's **Coverage Expectation** from the matrix - not merely exist. + +| Task creates... | Done When must include... | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Code layer with "unit" requirement | Unit tests written satisfying the layer's Coverage Expectation (e.g., 1:1 AC mapping for domain logic; all listed edge cases covered) + quick gate passes | +| Code layer with "e2e" requirement | E2E tests written satisfying the layer's Coverage Expectation (e.g., every route the task adds: happy path + edge + error paths) + full gate passes | +| Code layer with "integration" requirement | Integration tests written satisfying the layer's Coverage Expectation + full gate passes | +| Code layer with "none" requirement | Gate check at appropriate level | + +### 2. Break Into Atomic Tasks + +**Task = ONE deliverable**. Examples: + +- ✅ "Create UserService interface" (one file, one concept) +- ❌ "Implement user management" (too vague, multiple files) + +### 3. Define Dependencies + +What MUST be done before this task can start? + +### 4. Create Execution Plan + +Group tasks into ordered phases. Each phase depends on the ones before it; tasks execute sequentially within a phase. + +**Size phases near the worker budget.** During Execute, phases are packed into task-budgeted batches (~7 tasks per sub-agent, whole phases - see [sub-agents.md](sub-agents.md)). Because a batch cut may only land on a phase boundary, a phase that is much larger than the budget forces an over-sized worker. Keep each phase from greatly exceeding the budget: + +- If a phase would hold **more than ~10 tasks (≈1.5× the budget)**, split it into cohesive sub-phases at a genuine dependency/cohesion seam - not at an arbitrary task index. +- Only leave a phase over-sized when its tasks are one tight dependency chain that genuinely cannot be split. That is a legitimate (if fat) single-worker phase, not a smell. + +This keeps phase boundaries meaningful while letting the packing hit its target worker count. + +### 5. Validate Before Presenting (MANDATORY) + +Before showing tasks to the user, run ALL three pre-approval checks. These are NOT optional - they are gates. If any check fails, restructure the tasks and re-run until all pass. + +**Deterministic backing (run it, do not eyeball it).** `python3 /scripts/validate_tasks.py ` enforces the structural half of these checks so they cannot drift: it flags a `Where` that names multiple files (granularity smell, Check 1), a diagram edge with no matching `Depends on` within a phase and vice-versa (Check 2), a task missing its `Tests` or `Gate` field, a `Tests: none` to confirm against the matrix (Check 3), and any dependency pointing to a later phase. A non-zero exit means restructure before presenting. The script checks structure; you still build the two tables below (the layer-to-test co-location judgment is yours). If no code-execution tool is available, run the checks by reading `tasks.md`. + +**Check 1: Task Granularity** - verify each task is atomic (see Granularity Check section). + +**Check 2: Diagram-Definition Cross-Check** - verify the execution diagram matches every task's `Depends on` field (see Diagram-Definition Cross-Check section). Build the cross-check table and include it in the output. + +**Check 3: Test Co-location Validation** - verify every task's `Tests` field matches the **Test Coverage Matrix** generated above (see Test Co-location Validation section). Build the validation table and include it in the output. + +**Output both tables with the tasks** so the user can see the validation results. Any ❌ means you MUST restructure before presenting - do not show failing tasks to the user and ask them to approve. + +**Note on the generated matrix:** The two sections (`Test Coverage Matrix`, `Gate Check Commands`) are provisional - generated from codebase sampling or user input and included in this file for user confirmation as part of task approval. They become authoritative once the user approves the tasks. + +### 6. ASK About MCPs and Skills + +**CRITICAL**: Before execution, ask the user: + +> "For each task, which tools should I use?" +> +> **Available MCPs**: [list from project or user] +> **Available Skills**: [list from project or user] + +--- + +## Template: `.specs/features/[feature]/tasks.md` + +```markdown +# [Feature] Tasks + +## Execution Protocol (MANDATORY -- do not skip) + +Implement these tasks with the `tlc-spec-driven` skill: **activate it by name and follow its Execute flow and Critical Rules.** Do not search for skill files by filesystem path. The skill is the source of truth for the full flow (per-task cycle, sub-agent delegation, adequacy review, Verifier, discrimination sensor). + +**If the skill cannot be activated, STOP and tell the user - do not proceed without it.** + +--- + +**Design**: `.specs/features/[feature]/design.md` +**Status**: Draft | Approved | In Progress | Done + +--- + + + +## Test Coverage Matrix + +[Generated in step 1.5 - see process above] + +## Gate Check Commands + +[Generated in step 1.5 - see process above] + +--- + +## Execution Plan + +Phases are ordered and run sequentially - each phase completes before the next begins, and tasks within a phase execute in order. + +### Phase 1: Foundation + +Tasks that must be done first, in order. + +``` +T1 → T2 → T3 +``` + +### Phase 2: Core Implementation + +Builds on the foundation. + +``` +T4 → T5 → T6 → T7 +``` + +### Phase 3: Integration + +Bringing it all together. + +``` +T8 → T9 +``` + +--- + +## Task Breakdown + +### T1: [Create X Interface] + +**What**: [One sentence: exact deliverable] +**Where**: `src/path/to/file.ts` +**Depends on**: None +**Reuses**: `src/existing/BaseInterface.ts` +**Requirement**: [FEAT]-01 + +**Tools**: + +- MCP: `filesystem` (or NONE) +- Skill: NONE + +**Done when**: + +- [ ] Interface defined with all methods from design +- [ ] Types exported correctly +- [ ] No TypeScript errors + +**Tests**: [unit/e2e/integration/none - from coverage matrix] +**Gate**: [quick/full/build - from gate check commands] + +--- + +### T2: [Implement Y Service] + +**What**: [Exact deliverable] +**Where**: `src/services/YService.ts` +**Depends on**: T1 +**Reuses**: `src/services/BaseService.ts` patterns + +**Tools**: + +- MCP: `filesystem`, `context7` +- Skill: NONE + +**Done when**: + +- [ ] Implements interface from T1 +- [ ] Handles error cases from design +- [ ] Gate check passes: `[quick gate command from the Gate Check Commands above]` +- [ ] Test count: [N] tests pass (no silent deletions) + +**Tests**: unit +**Gate**: quick + +--- + +### T3: [Create Z Component] + +**What**: [Exact deliverable] +**Where**: `src/components/ZComponent.tsx` +**Depends on**: T1 +**Reuses**: `src/components/BaseComponent.tsx` + +**Tools**: + +- MCP: `filesystem` +- Skill: NONE + +**Done when**: + +- [ ] Component renders correctly +- [ ] Handles props from interface +- [ ] Follows existing component patterns +- [ ] Gate check passes: `[quick gate command from the Gate Check Commands above]` +- [ ] Test count: [N] tests pass (no silent deletions) + +**Tests**: unit +**Gate**: quick + +--- + +### T4: [Add A Feature to Y] + +**What**: [Exact deliverable] +**Where**: `src/services/YService.ts` (modify) +**Depends on**: T2, T3 +**Reuses**: Existing service patterns + +**Tools**: + +- MCP: `filesystem`, `github` +- Skill: `api-design` + +**Done when**: + +- [ ] Feature works per acceptance criteria +- [ ] Gate check passes: `[full gate command from the Gate Check Commands above]` +- [ ] Test count: [N] tests pass (no silent deletions) + +**Tests**: integration +**Gate**: full + +**Commit**: `feat([scope]): [description]` + +--- + +## Phase Execution Map + +Visual representation of task ordering. Phases run in sequence, and tasks within a phase run in order: + +``` +Phase 1 → Phase 2 → Phase 3 + +Phase 1: T1 ------→ T2 ------→ T3 +Phase 2: T4 ------→ T5 ------→ T6 ------→ T7 +Phase 3: T8 ------→ T9 +``` + +Execution is strictly sequential - there is no intra-phase parallelism. A single agent (or batch worker) works one task at a time, in order. + +**How phase-based execution works:** + +At Execute, the agent counts total tasks and packs phases into **task-budgeted batches** (~7 tasks +per worker, whole phases - the benchmarked sweet spot is ~20 tasks → ~3 workers). A **phase** is the +semantic/dependency unit; a **batch** is one or more *consecutive whole phases* assigned to one +worker. The cut only ever lands on a phase boundary - a phase is never split across workers. When +packing yields more than one batch (> ~8 tasks), the agent offers to dispatch batch sub-agents. +Batches run sequentially: each worker executes ALL its tasks in order, then reports a compact summary +before the next batch starts. This right-sizes the worker count by workload instead of by phase +count (one-per-phase is too fragmented; expensive and slow). See [sub-agents.md](sub-agents.md) for +the full model - packing algorithm, offer-then-confirm, worker payload, compact summary contract, +failure handling, and context sizing guidance. + +When the whole feature fits a single batch (≤ ~8 tasks), execution happens inline in the main window +with no sub-agents spawned. + +**The orchestrating agent's role during Execute:** +1. Count total tasks and pack phases into ~7-task batches - offer batch sub-agents if that yields more than one batch and the user accepts +2. Dispatch the next batch (to a worker, or execute inline) +3. Receive the compact batch summary +4. Update tasks.md with results +5. If the batch summary shows all tasks complete: proceed to the next batch +6. If a task failed: decide fix/escalate before dispatching the next batch + +--- + +## Task Granularity Check + +Before approving tasks, verify they are granular enough: + +| Task | Scope | Status | +| ------------------------------- | ------------- | ------------ | +| T1: Create email input | 1 component | ✅ Granular | +| T2: Add validation function | 1 function | ✅ Granular | +| T3: Create form with all fields | 5+ components | ❌ Split it! | +| T4: Connect to API | 1 function | ✅ Granular | + +**Granularity check**: + +- ✅ 1 component / 1 function / 1 endpoint = Good +- ⚠️ 2-3 related things in same file = OK if cohesive +- ❌ Multiple components or files = MUST split + +--- + +## Diagram-Definition Cross-Check + +Before approving tasks, verify the execution diagram is consistent with the task definitions. These are independent artifacts that can drift - the diagram is drawn for visual clarity while task bodies are written for precision. Both must agree. + +For each task, check: + +| Task | Depends On (task body) | Diagram Shows | Status | +| ---- | ---------------------- | ------------- | ------ | +| T[N] | [deps from body] | [deps from diagram arrows] | ✅ Match or ❌ Mismatch | + +**Rules:** + +- Every `Depends on` in a task body must have a corresponding arrow in the diagram. +- Every arrow in the diagram must correspond to a `Depends on` in the target task's body. +- A task must never depend on a task in a later phase - dependencies point backward or within the same phase only. + +--- + +## Test Co-location Validation + +Before approving tasks, verify EVERY task's `Tests` field is consistent with the **Test Coverage Matrix** generated above. This is a hard gate - tasks that fail this check MUST be fixed. + +For each task, check: does the task create or modify a code layer that has a required test type in the coverage matrix? If yes, the task's `Tests` field MUST match. + +| Task | Code Layer Created/Modified | Matrix Requires | Task Says | Status | +| ---- | --------------------------- | --------------- | --------- | ------ | +| T[N]: [name] | [layer from coverage matrix] | [test type] | [task's Tests field] | ✅ OK or ❌ VIOLATION | + +**Rules:** + +- "Tested in another task" is NOT a valid justification for `Tests: none`. That is test deferral - the exact anti-pattern this validation prevents. +- `Tests: none` is only valid when the coverage matrix says "none" for that code layer. +- If a task creates MULTIPLE code layers (e.g., service + controller), use the HIGHEST test type required by any of them. +- Any ❌ VIOLATION → restructure the task to include its required tests before proceeding. + +**Resolving compilation dependencies:** + +When a task creates code that can't be tested until a later task completes (e.g., a controller that needs module wiring before its e2e tests can run), do NOT defer the tests to a separate task. Instead, restructure: + +1. **Merge forward:** Move the untestable task's tests into the earliest task where they become runnable (e.g., the wiring task includes wiring + e2e tests for the controller it enables). +2. **Merge backward:** Absorb the blocking dependency into the current task so it becomes self-testable (e.g., controller task includes its own module registration). + +Pick whichever option keeps tasks atomic and cohesive. The goal: no task produces unverified code. If code can't be tested in the task that creates it, the task boundaries are wrong. + +--- + +## Tips + +- **Phases are ordered** - Each phase completes before the next; tasks run in order within a phase +- **Reuses = Token saver** - Always reference existing code +- **Tools per task** - MCPs and Skills prevent wrong approaches +- **Dependencies are gates** - Clear what blocks what +- **Done when = Testable** - If you can't verify it, rewrite it +- **Requirement ID = Traceable** - Every task traces back to a spec requirement +- **One commit per task** - Plan the commit message format in advance + +--- + +## Task Verification Standards + +Every task MUST follow the `Done when` + `Tests` + `Gate` fields defined in the **Task Breakdown** template above. Each `Done when` entry must be specific, testable (binary pass/fail), and reference the gate check command from the `Gate Check Commands` section. Include the expected test count to prevent silent deletions. diff --git a/plugin/skills/tlc-spec-driven/references/validate.md b/plugin/skills/tlc-spec-driven/references/validate.md new file mode 100644 index 0000000..3678d3f --- /dev/null +++ b/plugin/skills/tlc-spec-driven/references/validate.md @@ -0,0 +1,355 @@ +# Execute: Validate & Verify + +**Goal**: Verify implementation meets spec AND coding principles. This is NOT a separate phase - verification is part of every task's completion within Execute. + +**Three levels of verification:** + +1. **Per-task verification (always, author self-check):** After implementing each task, verify its "Done when" criteria before committing. This is mandatory and automatic. The implementer runs it. + +2. **Feature-level validation (independent Verifier sub-agent, always-on, never prompted):** After all tasks for a feature (or priority group) are done, validation runs automatically - the orchestrator dispatches a **fresh Verifier sub-agent** (see [sub-agents.md](sub-agents.md)). Do NOT ask the user whether to run it; it is the safety net, not an opt-in. User interaction is limited to interactive UAT (for user-facing features) and acting on a FAIL verdict ("fix these gaps now?"). The Verifier: + - Runs **read-only** over the real implementation and tests - mutations run in a scratch/throwaway state only (see Discrimination Sensor section) + - Scopes coverage to the feature's **git diff surface** (not the full repository) + - Re-derives coverage independently using **evidence-or-zero**: every AC must be traced to a `file:line` + assertion expression; a criterion with no `file:line` citation counts as NOT covered + - Runs the **spec-anchored outcome check** and the **discrimination sensor** (both described below) + - Writes `.specs/features/[feature]/validation.md` with the full evidence report + - Returns a compact verdict + ranked gap list to the orchestrator in chat + - Gaps become **fix tasks** routed back to an implementer; re-verification follows with a maximum of **3 fix→re-verify iterations** before escalating to the user + +3. **Interactive UAT (for user-facing features only):** The feature has complex user-facing behavior where human judgment matters (UI flows, interaction patterns, visual design). For backend-only or infrastructure work, automated checks are sufficient. + +**Trigger for explicit validation:** "Validate", "verify work", "UAT", "test with me", "walk me through it" + +--- + +## Process + +### 1. Check Completed Tasks + +Go through tasks.md: + +- [ ] All tasks marked done? +- [ ] Any blocked or partial? + +### 2. Spec-Anchored Acceptance Criteria Check + +For each acceptance criterion in `spec.md`, the Verifier re-derives the **spec-defined expected outcome** and confirms the test's actual assertion matches it: + +```markdown +### P1: [Story Title] + +**Acceptance Criteria**: + +| Criterion (WHEN X THEN Y) | Spec-defined outcome | `file:line` + assertion expression | Result | +| ------------------------- | -------------------- | ---------------------------------- | ------ | +| WHEN [X] THEN [Y] | [precise value/state from spec] | `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | ✅ PASS / ❌ GAP / ⚠️ Spec-precision gap | +``` + +**Rules:** + +- Where the spec defines a precise outcome (specific status code, field value, error message, state), the test assertion MUST target that exact outcome - not just that an assertion exists. +- Where the spec does NOT define a precise outcome, mark as **⚠️ Spec-precision gap** and flag it in the report. Do NOT silently pass a vague assertion. +- Evidence-or-zero: a criterion with no `file:line` citation counts as NOT covered. + +### 3. Check Edge Cases + +From spec.md edge cases: + +- [ ] [Edge case 1] handled correctly +- [ ] [Edge case 2] handled correctly + +### 4. Run Build-Level Gate Check (MANDATORY) + +Run the Build-level gate check from the **Gate Check Commands** section in tasks.md. This is NOT optional. + +1. Run: `[Build gate command from the Gate Check Commands section in tasks.md]` +2. Non-zero exit code = STOP. Do not proceed to Code Quality Check. +3. Record results: + - Total test count: [N] + - Passed: [N] + - Failed: [list] + - Skipped: [list - each skip must be justified] + +**Test Integrity Check:** + +- Compare current test count against the count before this feature was implemented +- If test count DECREASED: investigate why. Tests should only be deleted with explicit justification. +- If assertions were weakened (less specific than before): flag as potential regression + +### 5. Discrimination Sensor (MANDATORY - always runs after gate check passes) + +The sensor provides the empirical guarantee that the tests can actually detect regressions. It runs in a scratch/throwaway state - the real working tree is never modified. + +**How it works:** + +1. **Prepare an isolated scratch.** Never mutate the real worktree. Choose one: + - Preferred: a temporary git worktree (`git worktree add HEAD`), mutate and run tests there, then `git worktree remove --force `. + - Fallback (no git / worktree unavailable): copy only the affected file(s) to a temp directory, mutate the copies, point the test runner at those copies (or restore originals from the copies' backups), then delete the temp directory. + - **Forbidden:** `git stash` / `git stash pop`. A stash records state *before* the mutation; popping it does not reverse a mutation applied afterward, and on a clean tree `git stash` creates no entry at all - so the fault is left in the real worktree. +2. **Capture a baseline.** Record `git status --porcelain` (or equivalent) of the real worktree *before* any sensor work. It must be unchanged after cleanup. +3. **Inject a behavior-level fault** into the scratch copy of the new code introduced by this feature. Choose a mutation proportional to the code's risk: + - Flip a boolean condition (`if (x)` → `if (!x)`, `>` → `>=`) + - Change a return value (return a wrong status code, wrong field, zero instead of a computed value) + - Off-by-one (shift a loop bound, change a slice index) + - Remove a required side effect (delete a method call that the spec requires) +4. **Run the tests** that cover the mutated code (against the scratch). Use the Quick or Full gate command from tasks.md. +5. **Confirm the mutant is killed** (tests FAIL). Discard the scratch (remove worktree or delete temp copies). +6. **Verify isolation.** Re-run `git status --porcelain` on the real worktree and confirm it matches the baseline from step 2. If it differs, STOP - restore the real tree before continuing, and treat the sensor run as invalid. +7. **If a mutant survives** (tests still pass after the fault), the tests are not discriminating for that behavior - add a fix task to strengthen the assertion. + +**Tiering (proportional, not optional):** + +| Context | Sensor depth | +| ------- | ------------ | +| Default (all features) | Lightweight fault-injection: 1-3 targeted behavior-level mutations per feature, focused on the highest-risk new code | +| P0 / critical paths (payment, auth, data integrity) | Full mutation run: use language-appropriate mutation tooling if available (e.g., Stryker, mutmut, cargo-mutants, pitest); otherwise increase the number of manual fault-injection mutations to ≥5 covering all branches | + +**Stack-agnostic:** The sensor targets behavior-level semantics (what the code does), not a specific tool. Any language, any framework. + +**Report:** Record killed/survived for each mutation attempt. Surviving mutants → create fix tasks before marking the feature done. + +### 6. Code Quality Check (MANDATORY) + +For each changed file, verify against [coding-principles.md](coding-principles.md): + +| Check | Pass? | +| ------------------------------------ | ----- | +| No features beyond what was asked | | +| No abstractions for single-use code | | +| No unnecessary "flexibility" added | | +| Only touched files required for task | | +| Didn't "improve" unrelated code | | +| Matches existing patterns/style | | +| Would senior engineer approve? | | +| Tests map to acceptance criteria and are non-shallow (spot-check one story) | | +| Spec-anchored outcome check: each test's asserted value matches the spec-defined outcome (or gap flagged) | | +| Per-layer Coverage Expectation met: domain logic has 1:1 AC mapping; routes/e2e cover happy + edge + error paths for every route in scope | | +| Every test in scope maps to a spec AC, listed edge case, or Done-when criterion (no unclaimed tests) | | +| Documented project quality/testing guidelines followed (cite guideline file, or "none - strong defaults applied") | | + +❌ Any "No"? → Fix before marking complete. + +### 7. Interactive UAT (if user-facing feature) + +For each testable deliverable, present one test at a time: + +``` +Test [N]: [Test Name] + +Expected: [What should happen - specific and observable] + +→ Does this work? Describe what you see. +``` + +Wait for user response: + +| User says | Interpret as | +| ------------------------------ | ----------------------- | +| "yes", "pass", "works", "next" | ✅ Pass | +| "skip", "can't test", "n/a" | ⏭️ Skip | +| Anything else | ❌ Issue - log verbatim | + +**Severity inference (never ask the user for severity):** + +| User description contains | Inferred severity | +| --------------------------------------- | ----------------- | +| crash, error, exception, fails, broken | Blocker | +| doesn't work, wrong, missing, can't | Major | +| slow, weird, off, minor, small | Minor | +| color, font, spacing, alignment, visual | Cosmetic | +| (unclear) | Major (default) | + +### 8. Generate Fix Plans (if issues found) + +For each issue found during UAT or from the Verifier: + +1. **Diagnose** - Analyze the codebase to find root cause +2. **Create fix task** - Write a task definition with: + - What: The specific fix + - Where: File paths + - Verify: How to prove the fix works + - Done when: Acceptance criteria for the fix +3. **Present fix plan** - Show all fix tasks to user for approval + +Fix tasks follow the same format as regular tasks and can be executed with the implement phase. + +**Guardrail:** Maximum 3 diagnostic iterations per issue. If root cause isn't found after 3 attempts, flag for human investigation. The same 3-iteration bound applies to the Verifier's fix→re-verify cycle: if gaps persist after 3 rounds, escalate to the user rather than continuing to loop. + +### 9. Write Validation Report File + Return Chat Summary (MANDATORY) + +After all checks complete, the Verifier MUST: + +1. **Write the persisted report** to `.specs/features/[feature]/validation.md` (see template below). This file is the evidence artifact - it survives the session and can be referenced by CI, reviewers, or future agents. +2. **Return a compact summary in chat** to the orchestrator (see Compact Chat Summary section below). The orchestrator surfaces it to the user and routes any ranked gaps to fix tasks. + +**Deterministic backing (run it, do not eyeball it).** After writing the report, run `python3 /scripts/validate_state.py `. It confirms the report is real - present, verdict filled to PASS, and backed by at least one `file:line` evidence citation - so a missing, hollow, placeholder, or FAIL report cannot slip through as done. A non-zero exit means the feature is NOT done: repair the report or route the FAIL gaps to fix tasks, then re-run. This is the closing gate of Execute and runs automatically, the same way the lessons layer runs at distillation; it is never a manual step. If no code-execution tool is available, confirm the same by reading `validation.md`. + +### 10. Distill Lessons (MANDATORY when validation.md has signal) + +This is the closing action of validation - not a separate phase. Immediately after the report is written, turn its grounded failures into reusable, project-local guidance by following [lessons.md](lessons.md). In short: for each surviving mutant, spec-precision gap, failed/uncovered AC, or `// SPEC_DEVIATION`, record one terse general lesson via `python3 /scripts/lessons.py add` (the script enforces grounding and owns all bookkeeping). A clean PASS with no signal → record nothing. Run the self-check: if there was signal but no lesson was recorded, say so in chat. See [lessons.md](lessons.md) for the exact commands, phrasing rules, scope discipline, and the no-script fallback. + +--- + +## Compact Chat Summary (returned in chat after validation) + +The Verifier returns this block to the orchestrator after completing all checks: + +```markdown +## Validation: [Feature] - [PASS ✅ | FAIL ❌] + +**Spec-anchored check**: [N/N ACs matched spec outcome | M spec-precision gaps flagged] +**Gate**: [X passed, 0 failed] +**Sensor**: [N mutations injected, N killed, N survived] +**Report**: `.specs/features/[feature]/validation.md` + +**Ranked gaps** (if FAIL): +1. [Gap description] - [AC or criterion] - [file:line or "no evidence"] +2. ... +``` + +--- + +## Validation Report Template (`.specs/features/[feature]/validation.md`) + +```markdown +# [Feature] Validation + +**Date**: [YYYY-MM-DD] +**Spec**: `.specs/features/[feature]/spec.md` +**Diff range**: [commit range or branch..HEAD] +**Verifier**: independent sub-agent (author ≠ verifier) + +--- + +## Task Completion + +| Task | Status | Notes | +| ---- | ---------- | ------- | +| T1 | ✅ Done | - | +| T2 | ✅ Done | - | +| T3 | ⚠️ Partial | [Issue] | + +--- + +## Spec-Anchored Acceptance Criteria + +| Criterion (WHEN X THEN Y) | Spec-defined outcome | `file:line` + assertion | Result | +| ------------------------- | -------------------- | ----------------------- | ------ | +| WHEN X THEN Y | [precise value/state from spec] | `path/to/test.ts:42` - `expect(result.field).toBe(expected)` | ✅ PASS | +| WHEN A THEN B | [expected value] | `path/to/test.ts:88` - `expect(res.status).toBe(400)` | ✅ PASS | +| WHEN C THEN D | not precisely defined in spec | - | ⚠️ Spec-precision gap | + +**Status**: ✅ All ACs covered / ❌ Gaps present / ⚠️ Spec-precision gaps flagged + +--- + +## Discrimination Sensor + +| Mutation | File:line | Description | Killed? | +| -------- | --------- | ----------- | ------- | +| 1 | `src/service.ts:42` | Flipped condition `x > 0` → `x >= 0` | ✅ Killed | +| 2 | `src/service.ts:88` | Changed return value `status: 'active'` → `status: 'inactive'` | ✅ Killed | +| 3 | `src/handler.ts:15` | Removed side-effect call to `notify()` | ❌ Survived → fix task created | + +**Sensor depth**: [lightweight / P0-full] +**Result**: [N/N killed] - [PASS ✅ | FAIL ❌] + +--- + +## Interactive UAT Results (if performed) + +| # | Test | Result | Details | +| --- | ----------- | -------- | ----------------------------------------------- | +| 1 | [Test name] | ✅ Pass | - | +| 2 | [Test name] | ❌ Issue | [Verbatim user response] - Severity: [inferred] | +| 3 | [Test name] | ⏭️ Skip | [Reason] | + +--- + +## Code Quality + +| Principle | Status | +| ---------------- | ------ | +| Minimum code | ✅ | +| Surgical changes | ✅ | +| No scope creep | ✅ | +| Matches patterns | ✅ | +| Spec-anchored outcome check (asserted values match spec) | ✅ | +| Per-layer Coverage Expectation met (domain 1:1 ACs; routes happy+edge+error) | ✅ | +| Every test maps to a spec requirement - no unclaimed tests | ✅ | +| Documented guidelines followed: [file(s) or "none - strong defaults applied"] | ✅ | + +--- + +## Edge Cases + +- [x] Edge case 1: Handled correctly +- [ ] Edge case 2: NOT handled - needs fix + +--- + +## Gate Check + +- **Gate command**: [Build gate command from the Gate Check Commands section in tasks.md] +- **Result**: [X] passed, [Y] failed, [Z] skipped +- **Test count before feature**: [N] +- **Test count after feature**: [M] +- **Delta**: [+(M - N) new tests] +- **Skipped tests**: [list with justification for each] +- **Failures**: [list with details] + +--- + +## Fix Plans (if issues found) + +### Fix 1: [Issue description] + +- **Root cause**: [What's actually wrong] +- **Fix task**: [Task definition] +- **Priority**: [Blocker/Major/Minor/Cosmetic] + +--- + +## Requirement Traceability Update + +Update spec.md requirement statuses: + +| Requirement | Previous Status | New Status | +| ----------- | --------------- | ------------ | +| [FEAT]-01 | Implementing | ✅ Verified | +| [FEAT]-02 | Implementing | ❌ Needs Fix | + +--- + +## Summary + +**Overall**: ✅ Ready | ⚠️ Issues | ❌ Not Ready + +**Spec-anchored check**: [N/N ACs matched spec outcome | M spec-precision gaps] +**Sensor**: [N/N mutations killed] +**Gate**: [X passed] + +**What works**: [List] + +**Issues found**: [Issue 1: How to fix] + +**Next steps**: [Action] +``` + +--- + +## Tips + +- **Validation is never prompted** - it always runs after the last task; do not ask the user whether to run it +- **Spec-anchored, not just covered** - "there is an assertion" is not enough; the assertion must target the spec-defined outcome +- **Sensor in scratch only** - never mutate the real tree; use a temp worktree or file copies (never `git stash`), run, discard, then confirm porcelain matches the pre-sensor baseline +- **Surviving mutants are fix tasks** - do not mark the feature done if the sensor found weak tests +- **P1 first** - MVP must work before P2/P3 +- **WHEN/THEN = Test** - Each criterion is a test case +- **Be specific** - "Doesn't work" isn't helpful +- **Recommend fixes** - Don't just report problems, create fix tasks +- **Quality check is mandatory** - Not optional +- **Infer severity** - Never ask the user "how bad is this?" +- **Max 3 diagnostic iterations** - Prevents infinite investigation loops +- **Update traceability** - Every verified requirement updates spec.md status +- **Always write the report file** - `.specs/features/[feature]/validation.md` is the persisted evidence artifact +- **Distill after writing** - turn grounded failures into lessons via `scripts/lessons.py` ([lessons.md](lessons.md)); clean PASS → no lesson diff --git a/plugin/skills/tlc-spec-driven/scripts/check_commit.py b/plugin/skills/tlc-spec-driven/scripts/check_commit.py new file mode 100644 index 0000000..9332a9f --- /dev/null +++ b/plugin/skills/tlc-spec-driven/scripts/check_commit.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +check_commit.py - deterministic Conventional Commits validation. + +The per-task atomic-commit rule mandates Conventional Commits 1.0.0. This makes +that rule checkable instead of trusting the model to remember the format. Pure +standard library, zero dependencies, agent-agnostic. + +It reads the message from (in priority order): a positional file path, --message, +or stdin. The file-path form matches how git passes the message file to a +`commit-msg` hook, so this doubles as an optional git-level guard WITHOUT +coupling the skill to any AI agent: + + ln -s /scripts/check_commit.py .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg + +What it checks: + ERROR - header does not match type(scope)!: description + ERROR - type is not one of the allowed Conventional Commits types + ERROR - description is empty, starts uppercase, or ends with a period + ERROR - `!` breaking marker present but no `BREAKING CHANGE:` footer + WARN - header longer than 72 characters + +Usage: + python3 /scripts/check_commit.py [msgfile] + python3 /scripts/check_commit.py --message "feat(auth): add email validation" + echo "fix(cart): prevent negative quantity" | python3 /scripts/check_commit.py + +Exit codes: 0 pass, 1 violation, 2 usage error. +""" + +import argparse +import re +import sys + +TYPES = ["feat", "fix", "refactor", "docs", "test", "style", "perf", "build", "ci", "chore"] +HEADER_RE = re.compile(r"^(?P\w+)(?:\((?P[^)]+)\))?(?P!)?: (?P.+)$") + + +def read_message(args): + if args.message is not None: + return args.message + if args.msgfile: + with open(args.msgfile, "r", encoding="utf-8") as f: + return f.read() + if not sys.stdin.isatty(): + return sys.stdin.read() + return "" + + +def check(message): + errors, warnings = [], [] + # Ignore comment lines (git puts '#' comments in the message file). + lines = [ln for ln in message.splitlines() if not ln.lstrip().startswith("#")] + # Trim leading blank lines. + while lines and not lines[0].strip(): + lines.pop(0) + if not lines: + return (["empty commit message"], warnings) + + header = lines[0].rstrip() + if len(header) > 72: + warnings.append(f"header is {len(header)} chars (>72): {header[:60]}...") + + m = HEADER_RE.match(header) + if not m: + errors.append(f"header does not match 'type(scope): description': {header!r}") + return (errors, warnings) + + ctype = m.group("type") + desc = m.group("desc") + bang = m.group("bang") + + if ctype not in TYPES: + errors.append(f"type '{ctype}' is not one of: {', '.join(TYPES)}") + if not desc.strip(): + errors.append("description is empty") + else: + if desc[:1].isupper(): + errors.append(f"description should start lowercase: '{desc[:30]}'") + if desc.rstrip().endswith("."): + errors.append("description should not end with a period") + + body = "\n".join(lines[1:]) + breaking_footer = bool(re.search(r"^BREAKING CHANGE:", body, re.MULTILINE)) + if bang and not breaking_footer: + errors.append("'!' breaking marker present but no 'BREAKING CHANGE:' footer") + + return (errors, warnings) + + +def main(argv=None): + p = argparse.ArgumentParser(prog="check_commit.py", description="Validate a Conventional Commits message.") + p.add_argument("msgfile", nargs="?", default=None, help="path to a commit message file (as git passes to commit-msg)") + p.add_argument("--message", default=None, help="the commit message as a string") + args = p.parse_args(argv) + + message = read_message(args) + if not message.strip(): + print("check_commit: no message provided (pass a file, --message, or pipe via stdin).", file=sys.stderr) + return 2 + + errors, warnings = check(message) + for w in warnings: + print(f" WARN {w}") + for e in errors: + print(f" ERROR {e}") + if errors: + print("\ncheck_commit: FAIL - see https://www.conventionalcommits.org/en/v1.0.0/") + return 1 + print("check_commit: OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugin/skills/tlc-spec-driven/scripts/lessons.py b/plugin/skills/tlc-spec-driven/scripts/lessons.py new file mode 100644 index 0000000..3c4a42c --- /dev/null +++ b/plugin/skills/tlc-spec-driven/scripts/lessons.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +""" +lessons.py - deterministic bookkeeping for the tlc-spec-driven lessons layer. + +The LLM supplies judgment (which failure happened, how to phrase the lesson, what +signal grounds it). This script owns everything mechanical: IDs, distinct-feature +recurrence counting, candidate->confirmed promotion, pruning, demotion, and +rendering the human/agent-readable playbook. Bookkeeping by hand is exactly what +rots a lessons file, so it lives here, not in a prompt. + +Canonical state: .specs/lessons.json (machine-owned - do NOT hand-edit) +Rendered view: .specs/LESSONS.md (regenerated on every write) + +Pure standard library. No dependencies. The script file lives in this skill's +`scripts/` directory - invoke it as `python3 /scripts/lessons.py ...` +(never `python3 scripts/lessons.py` from a consuming project root). Run with +cwd at the project root (the dir that contains .specs), or pass --root. + +Commands: + add Record a grounded lesson from a verification signal. + list Print lessons (default: confirmed) for loading at Specify/Design. + penalize Mark a confirmed lesson as having failed when applied (-> quarantine). + prune Drop stale uncorroborated candidates (also runs automatically on add/list). + status Print counts (used by the self-check in validate.md). + init Create empty store + rendered file. + selftest Run stdlib regressions (normalization). + +Exit codes: 0 ok, 2 usage/validation error (e.g. missing grounding). +""" + +import argparse +import datetime as _dt +import json +import os +import re +import sys +import unicodedata + +STORE_REL = os.path.join(".specs", "lessons.json") +RENDER_REL = os.path.join(".specs", "LESSONS.md") + +SIGNALS = { + "ac_gap": "Acceptance criterion not covered / failed", + "surviving_mutant": "Discrimination sensor mutant survived (weak test)", + "spec_precision_gap": "Spec did not define a precise outcome", + "spec_deviation": "Implementation diverged from spec/design (SPEC_DEVIATION)", + "gate_fail": "Build-level gate check failed", +} + +DEFAULTS = {"promote_threshold": 2, "window_days": 45, "quarantine_threshold": 2} + + +def _now(): + return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _parse_date(s): + try: + return _dt.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_dt.timezone.utc) + except Exception: + return _dt.datetime.now(_dt.timezone.utc) + + +def _store_path(root): + return os.path.join(root, STORE_REL) + + +def _render_path(root): + return os.path.join(root, RENDER_REL) + + +def _load(root): + path = _store_path(root) + if not os.path.exists(path): + return { + "schema": 1, + "promote_threshold": DEFAULTS["promote_threshold"], + "window_days": DEFAULTS["window_days"], + "quarantine_threshold": DEFAULTS["quarantine_threshold"], + "next_id": 1, + "lessons": [], + } + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + for k, v in DEFAULTS.items(): + data.setdefault(k, v) + data.setdefault("schema", 1) + data.setdefault("next_id", 1) + data.setdefault("lessons", []) + return data + + +def _save(root, data): + os.makedirs(os.path.join(root, ".specs"), exist_ok=True) + with open(_store_path(root), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + _render(root, data) + + +def _norm(text): + """Normalized dedup key for lesson text. + + - casefold + NFD, strip combining marks (so Portuguese diacritics match ASCII peers) + - keep characters where str.isalnum() is true (any script) and whitespace + - drop other punctuation, collapse whitespace + + Exact-after-normalization only - no semantic matching (stdlib-only limitation). + Phrase lessons tersely and canonically so recurrences actually merge. + """ + t = unicodedata.normalize("NFD", text.casefold()) + t = "".join(c for c in t if unicodedata.category(c) != "Mn") + t = "".join(c if (c.isalnum() or c.isspace()) else " " for c in t) + t = re.sub(r"\s+", " ", t).strip() + return t + + +def _selftest_norm(): + """Regressions for #158: Portuguese diacritics + distinct non-Latin text.""" + failures = [] + + def check(cond, msg): + if not cond: + failures.append(msg) + + a = _norm("Não use datas locais") + b = _norm("Nao use datas locais") + check(a == b == "nao use datas locais", f"PT diacritics: {a!r} vs {b!r}") + + jp1 = _norm("日本語の文です") + jp2 = _norm("別の日本語文") + check(jp1 != "", f"JP1 empty: {jp1!r}") + check(jp2 != "", f"JP2 empty: {jp2!r}") + check(jp1 != jp2, f"JP sentences collapsed: {jp1!r} == {jp2!r}") + + check(_norm("café") == _norm("cafe") == "cafe", f"cafe: {_norm('café')!r}") + + if failures: + for f in failures: + print(f"FAIL: {f}", file=sys.stderr) + return 1 + print("selftest_norm: ok") + return 0 + + +def _key(signal, text): + return signal + "::" + _norm(text) + + +def _auto_prune(data): + """Drop candidates that never recurred within the window. Mutates data.""" + threshold = data["promote_threshold"] + window = data["window_days"] + now = _dt.datetime.now(_dt.timezone.utc) + kept = [] + dropped = [] + for l in data["lessons"]: + if l["status"] == "candidate" and l["recurrence"] < threshold: + age_days = (now - _parse_date(l.get("last_seen", l.get("created", _now())))).days + if age_days > window: + dropped.append(l["id"]) + continue + kept.append(l) + data["lessons"] = kept + return dropped + + +def _find(data, signal, text): + k = _key(signal, text) + for l in data["lessons"]: + if l.get("key") == k: + return l + return None + + +def _render(root, data): + lines = [] + lines.append("# LESSONS - auto-maintained by scripts/lessons.py") + lines.append("") + lines.append("> Machine-owned. Do NOT hand-edit. Changes are overwritten on the next `lessons.py` write.") + lines.append("> Canonical state lives in `.specs/lessons.json`. Edit lessons only via the script.") + lines.append(f"> promote_threshold={data['promote_threshold']} distinct features · window_days={data['window_days']} · quarantine_threshold={data['quarantine_threshold']}") + lines.append("") + + by_status = {"confirmed": [], "candidate": [], "quarantined": []} + for l in data["lessons"]: + by_status.get(l["status"], by_status["candidate"]).append(l) + + def block(title, items, note): + out = [f"## {title}", ""] + if note: + out.append(note) + out.append("") + if not items: + out.append("_none_") + out.append("") + return out + for l in sorted(items, key=lambda x: x["id"]): + scope = f" · scope: `{l['scope']}`" if l.get("scope") else "" + out.append(f"### {l['id']} - {l['text']}") + out.append( + f"- signal: `{l['signal']}` · recurrence: {l['recurrence']} feature(s){scope} · harmful: {l.get('harmful', 0)}" + ) + feats = ", ".join(l.get("features", [])) or "-" + out.append(f"- features: {feats}") + ev = l.get("evidence", []) + if ev: + out.append(f"- evidence: {ev[0]}" + (f" (+{len(ev) - 1} more)" if len(ev) > 1 else "")) + out.append(f"- last seen: {l.get('last_seen', '-')}") + out.append("") + return out + + lines += block( + "Confirmed (load these at Specify/Design)", + by_status["confirmed"], + "Corroborated across multiple features. Safe to apply as guidance.", + ) + lines += block( + "Candidates (under observation - do NOT load as guidance yet)", + by_status["candidate"], + "Seen once or not yet corroborated. Tracked, not trusted.", + ) + lines += block( + "Quarantined (failed when applied - ignore)", + by_status["quarantined"], + "A confirmed lesson that recurred alongside failure. Kept for the maintainer to review.", + ) + + with open(_render_path(root), "w", encoding="utf-8") as f: + f.write("\n".join(lines).rstrip() + "\n") + + +# ----------------------------- commands ----------------------------- + +def cmd_init(root, args): + data = _load(root) + _save(root, data) + print(f"Initialized lessons store at {_store_path(root)} and {_render_path(root)}") + return 0 + + +def cmd_add(root, args): + signal = args.signal + source = (args.source or "").strip() + text = (args.text or "").strip() + feature = (args.feature or "").strip() + + # Grounding is enforced here, deterministically - not left to the prompt. + if signal not in SIGNALS: + print(f"ERROR: --signal must be one of {sorted(SIGNALS)}", file=sys.stderr) + return 2 + if not feature: + print("ERROR: --feature is required (the feature the signal came from).", file=sys.stderr) + return 2 + if not source: + print("ERROR: --source is required (file:line / AC id / mutant id / SPEC_DEVIATION ref).", file=sys.stderr) + print(" A lesson with no grounding in validation.md is an opinion, not a lesson. Refused.", file=sys.stderr) + return 2 + if len(text) < 12: + print("ERROR: --text too short. State the actionable lesson in one terse sentence.", file=sys.stderr) + return 2 + + data = _load(root) + _auto_prune(data) + existing = _find(data, signal, text) + now = _now() + + if existing: + if feature not in existing["features"]: + existing["features"].append(feature) + existing["recurrence"] = len(existing["features"]) + existing["last_seen"] = now + ev = source if not args.scope else f"{source} ({args.scope})" + if ev not in existing["evidence"]: + existing["evidence"].append(ev) + promoted = False + if existing["status"] == "candidate" and existing["recurrence"] >= data["promote_threshold"]: + existing["status"] = "confirmed" + promoted = True + _save(root, data) + msg = f"UPDATED {existing['id']} (recurrence={existing['recurrence']}, status={existing['status']})" + if promoted: + msg += " - PROMOTED to confirmed" + print(msg) + else: + lid = f"L-{data['next_id']:03d}" + data["next_id"] += 1 + data["lessons"].append( + { + "id": lid, + "key": _key(signal, text), + "text": text, + "signal": signal, + "scope": (args.scope or "").strip(), + "status": "candidate", + "features": [feature], + "recurrence": 1, + "harmful": 0, + "evidence": [source if not args.scope else f"{source} ({args.scope})"], + "created": now, + "last_seen": now, + } + ) + _save(root, data) + print(f"ADDED {lid} (status=candidate, recurrence=1)") + return 0 + + +def cmd_penalize(root, args): + data = _load(root) + target = None + for l in data["lessons"]: + if l["id"].lower() == args.id.lower(): + target = l + break + if not target: + print(f"ERROR: no lesson with id {args.id}", file=sys.stderr) + return 2 + target["harmful"] = target.get("harmful", 0) + 1 + target["last_seen"] = _now() + if target["harmful"] >= data["quarantine_threshold"]: + target["status"] = "quarantined" + _save(root, data) + print(f"PENALIZED {target['id']} (harmful={target['harmful']}, status={target['status']})") + return 0 + + +def cmd_list(root, args): + data = _load(root) + if _auto_prune(data): + _save(root, data) + want = args.status + q = (args.query or "").lower().strip() + scope = (args.scope or "").lower().strip() + rows = [] + for l in data["lessons"]: + if want != "all" and l["status"] != want: + continue + if q and q not in l["text"].lower(): + continue + if scope and scope not in (l.get("scope", "").lower()): + continue + rows.append(l) + if not rows: + print(f"(no {want} lessons" + (f" matching '{q or scope}'" if (q or scope) else "") + ")") + return 0 + for l in sorted(rows, key=lambda x: x["id"]): + sc = f" [scope:{l['scope']}]" if l.get("scope") else "" + print(f"{l['id']} ({l['status']}, x{l['recurrence']}){sc}: {l['text']}") + return 0 + + +def cmd_prune(root, args): + data = _load(root) + dropped = _auto_prune(data) + _save(root, data) + print(f"Pruned {len(dropped)} stale candidate(s): {', '.join(dropped) if dropped else '-'}") + return 0 + + +def cmd_status(root, args): + data = _load(root) + counts = {"confirmed": 0, "candidate": 0, "quarantined": 0} + for l in data["lessons"]: + counts[l["status"]] = counts.get(l["status"], 0) + 1 + total = len(data["lessons"]) + print(f"lessons: {total} total | confirmed={counts['confirmed']} candidate={counts['candidate']} quarantined={counts['quarantined']}") + return 0 + + +def main(argv=None): + p = argparse.ArgumentParser(prog="lessons.py", description="Deterministic lessons bookkeeping for tlc-spec-driven.") + p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)") + sub = p.add_subparsers(dest="cmd", required=True) + + sp = sub.add_parser("init", help="Create empty store + rendered file") + sp.set_defaults(fn=cmd_init) + + sp = sub.add_parser("add", help="Record a grounded lesson") + sp.add_argument("--feature", required=True) + sp.add_argument("--signal", required=True, choices=sorted(SIGNALS)) + sp.add_argument("--source", required=True, help="file:line / AC id / mutant id / SPEC_DEVIATION ref") + sp.add_argument("--text", required=True, help="One terse, actionable sentence") + sp.add_argument("--scope", default="", help="Optional: path/layer/tag for retrieval filtering") + sp.set_defaults(fn=cmd_add) + + sp = sub.add_parser("penalize", help="Mark a confirmed lesson as failed-when-applied") + sp.add_argument("--id", required=True) + sp.set_defaults(fn=cmd_penalize) + + sp = sub.add_parser("list", help="Print lessons for loading") + sp.add_argument("--status", default="confirmed", choices=["confirmed", "candidate", "quarantined", "all"]) + sp.add_argument("--query", default="", help="Substring filter on lesson text") + sp.add_argument("--scope", default="", help="Substring filter on scope") + sp.set_defaults(fn=cmd_list) + + sp = sub.add_parser("prune", help="Drop stale uncorroborated candidates") + sp.set_defaults(fn=cmd_prune) + + sp = sub.add_parser("status", help="Print counts") + sp.set_defaults(fn=cmd_status) + + sp = sub.add_parser("selftest", help="Run stdlib regressions (normalization)") + sp.set_defaults(fn=lambda root, args: _selftest_norm()) + + args = p.parse_args(argv) + root = os.path.abspath(args.root) + return args.fn(root, args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_spec.py b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py new file mode 100644 index 0000000..02a41c8 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +validate_spec.py - deterministic closure-gate checks for a feature spec.md. + +Turns the Requirement Closure Gate (Specify phase) into a checkable pass/fail +run BEFORE a spec is presented for confirmation, instead of trusting the model +to remember the checks. Pure standard library, zero dependencies. Operates only +on the spec.md markdown artifact - never on the target codebase - so it stays +stack-agnostic and tool-agnostic. + +What it checks (heuristic markdown inspection, not a full parser): + ERROR - a required section is missing + ERROR - an acceptance criterion has no SHALL (not testable / not EARS-shaped) + ERROR - an Assumptions row has an empty "Chosen default" or "Rationale" cell + ERROR - a Requirement Traceability row has a malformed ID + WARN - an AC has SHALL but no recognizable EARS lead keyword + WARN - template placeholder rows are still present (spec not filled in) + WARN - open questions are not explicitly resolved + +Usage: + python3 /scripts/validate_spec.py [target] [--root DIR] [--strict] + + Invoke from the skill directory that ships this script (not the project root). + target Path to a spec.md, a feature directory, or a project root. + Omitted -> auto-detect the single feature under /.specs/features/. + --root Project root that contains .specs/ (default: current dir). + --strict Treat warnings as errors. + +Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error. +""" + +import argparse +import os +import re +import sys + +REQUIRED_SECTIONS = [ + "Problem Statement", + "Out of Scope", + "Assumptions & Open Questions", + "User Stories", + "Requirement Traceability", +] + +ID_RE = re.compile(r"^[A-Z][A-Z0-9]*-\d+$") +PLACEHOLDER_RE = re.compile(r"^\s*\[.+\]\s*$") +STATUS_VALUES = {"pending", "in design", "in tasks", "implementing", "verified"} + + +def resolve_spec(target, root): + """Return the path to a spec.md from a file, dir, or auto-detect.""" + if target: + if os.path.isfile(target): + return target + if os.path.isdir(target): + cand = os.path.join(target, "spec.md") + if os.path.isfile(cand): + return cand + # maybe it's a project root + return _autodetect(target) + # Not a path: treat as a feature name under /.specs/features// + cand = os.path.join(root, ".specs", "features", target, "spec.md") + if os.path.isfile(cand): + return cand + return None + return _autodetect(root) + + +def _autodetect(root): + base = os.path.join(root, ".specs", "features") + if not os.path.isdir(base): + return None + features = [ + d for d in sorted(os.listdir(base)) + if os.path.isfile(os.path.join(base, d, "spec.md")) + ] + if len(features) == 1: + return os.path.join(base, features[0], "spec.md") + if len(features) == 0: + return None + # Ambiguous: signal the caller with the list. + raise SystemExit( + "validate_spec: multiple features found; pass one explicitly:\n " + + "\n ".join(os.path.join(base, f, "spec.md") for f in features) + ) + + +def split_row(line): + cells = line.strip().strip("|").split("|") + return [c.strip() for c in cells] + + +def is_separator(line): + return bool(re.match(r"^\s*\|?[\s:|-]+\|?\s*$", line)) and "-" in line + + +def section_bounds(lines, name): + """Return (start, end) line indices for a `## name` section body.""" + start = None + for i, ln in enumerate(lines): + if re.match(r"^#{1,3}\s+" + re.escape(name) + r"\s*$", ln.strip()): + start = i + 1 + break + if start is None: + return None + end = len(lines) + for j in range(start, len(lines)): + if re.match(r"^#{1,3}\s+\S", lines[j]): + end = j + break + return (start, end) + + +def classify_ears(text): + """Return (ok, note). ok requires a SHALL; note records the EARS pattern.""" + t = text.strip() + low = t.lower() + has_shall = bool(re.search(r"\bshall\b", low)) + if not has_shall: + return (False, "no SHALL") + kws = [] + if re.search(r"\bwhile\b", low): + kws.append("WHILE") + if re.search(r"\bwhen\b", low): + kws.append("WHEN") + if re.match(r"^\s*if\b", low) or re.search(r"\bif\b.*\bthen\b", low): + kws.append("IF/THEN") + if re.search(r"\bwhere\b", low): + kws.append("WHERE") + if len(kws) >= 2: + return (True, "complex (" + "+".join(kws) + ")") + if kws: + pattern = { + "WHILE": "state-driven", + "WHEN": "event-driven", + "IF/THEN": "unwanted-behavior", + "WHERE": "optional-feature", + }[kws[0]] + return (True, pattern) + if re.match(r"^\s*the\b", low): + return (True, "ubiquitous") + return (True, "warn: SHALL present but no EARS lead keyword") + + +def check(spec_path): + with open(spec_path, "r", encoding="utf-8") as f: + text = f.read() + lines = text.splitlines() + errors, warnings = [], [] + + # 1. Required sections. + for name in REQUIRED_SECTIONS: + if section_bounds(lines, name) is None: + errors.append(f"missing required section: ## {name}") + + # 2. Acceptance criteria are EARS-shaped (have a SHALL). + in_ac = False + for i, ln in enumerate(lines, start=1): + stripped = ln.strip() + if re.match(r"^\*{0,2}Acceptance Criteria\*{0,2}\s*:?\s*$", stripped): + in_ac = True + continue + if in_ac: + m = re.match(r"^\s*\d+\.\s+(.*)$", ln) + if m: + item = m.group(1).strip() + if PLACEHOLDER_RE.match(item): + continue # untouched template row + ok, note = classify_ears(item) + if not ok: + errors.append(f"L{i}: acceptance criterion has no SHALL (not testable): {item[:70]}") + elif note.startswith("warn"): + warnings.append(f"L{i}: AC has SHALL but no EARS keyword (WHEN/WHILE/WHERE/IF or ubiquitous 'The … shall'): {item[:60]}") + elif stripped == "" or re.match(r"^#{1,3}\s", ln) or stripped.startswith("**"): + in_ac = False + + # 3. Assumptions table cells filled. + b = section_bounds(lines, "Assumptions & Open Questions") + if b: + rows = [lines[i] for i in range(*b) if lines[i].strip().startswith("|")] + data = [r for r in rows if not is_separator(r)] + # drop the header row (first table row) + if data: + data = data[1:] + template_seen = False + for r in data: + cells = split_row(r) + if len(cells) < 3: + continue + assumption, chosen, rationale = cells[0], cells[1], cells[2] + if PLACEHOLDER_RE.match(assumption) and PLACEHOLDER_RE.match(chosen): + template_seen = True + continue + if not chosen or PLACEHOLDER_RE.match(chosen): + errors.append(f"assumption '{assumption[:40]}' has empty 'Chosen default'") + if not rationale or PLACEHOLDER_RE.match(rationale): + errors.append(f"assumption '{assumption[:40]}' has empty 'Rationale'") + if template_seen: + warnings.append("Assumptions table still contains template placeholder rows") + # open questions line + oq = [lines[i] for i in range(*b) if "open questions" in lines[i].lower()] + oq_clean = re.sub(r"[*_]", "", " ".join(oq)).lower() + if not oq: + warnings.append("no 'Open questions:' line in Assumptions section") + elif not re.search(r"open questions.*:\s*none", oq_clean): + warnings.append("open questions do not read as resolved ('Open questions: none')") + + # 4. Requirement traceability IDs. + b = section_bounds(lines, "Requirement Traceability") + if b: + rows = [lines[i] for i in range(*b) if lines[i].strip().startswith("|")] + data = [r for r in rows if not is_separator(r)] + if data: + data = data[1:] + template_seen = False + real_ids = 0 + for r in data: + cells = split_row(r) + if not cells: + continue + rid = cells[0] + if PLACEHOLDER_RE.match(rid) or "[" in rid: + template_seen = True + continue + if not rid: + continue + if not ID_RE.match(rid): + errors.append(f"malformed requirement ID: '{rid}' (expected e.g. AUTH-01)") + else: + real_ids += 1 + if template_seen and real_ids == 0: + warnings.append("Requirement Traceability has only template rows (no real IDs yet)") + + return errors, warnings + + +def main(argv=None): + p = argparse.ArgumentParser(prog="validate_spec.py", description="Closure-gate checks for a feature spec.md.") + p.add_argument("target", nargs="?", default=None) + p.add_argument("--root", default=".") + p.add_argument("--strict", action="store_true") + args = p.parse_args(argv) + + spec = resolve_spec(args.target, args.root) + if not spec: + print("validate_spec: could not locate a spec.md. Pass a path or run from the project root.", file=sys.stderr) + return 2 + + errors, warnings = check(spec) + for w in warnings: + print(f" WARN {w}") + for e in errors: + print(f" ERROR {e}") + fail = errors or (warnings and args.strict) + print(f"\nvalidate_spec: {len(errors)} error(s), {len(warnings)} warning(s) in {spec}") + return 1 if fail else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_state.py b/plugin/skills/tlc-spec-driven/scripts/validate_state.py new file mode 100644 index 0000000..32d3d07 --- /dev/null +++ b/plugin/skills/tlc-spec-driven/scripts/validate_state.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +validate_state.py - deterministic completion gate for a feature. + +The skill's strongest invariant is "the Verifier is always-on, never prompted; +Execute is not done until validation.md reports PASS." That is prose the model +must remember. This turns it into a checkable pass/fail the closing step runs +automatically, so declaring a feature done without a real Verifier report fails +loudly instead of slipping through. + +It does NOT merely check that validation.md exists - a report that exists but is +empty, still holds the template placeholder, or has no evidence would pass a +shallow existence check while proving nothing. This gate requires a real, +filled verdict plus at least one file:line evidence citation. + +Operates only on the .specs/ markdown artifacts (stack- and tool-agnostic). No +dependencies. Run from the project root (the dir that contains .specs), or pass +--root. Meant to be invoked by the skill as the closing gate of Execute, the +same way lessons.py is invoked at distillation - not a manual step. + +Usage: + python3 /scripts/validate_state.py [feature] + python3 /scripts/validate_state.py + + Invoke from the skill directory that ships this script (not the project root). + Pass --root when cwd is not the project that contains .specs/. + +Exit codes: 0 ok, 1 a completed feature is missing a real PASS report, + 2 usage error. +""" + +import argparse +import os +import re +import sys + +# A file:line citation: a path with an extension, then :. e.g. src/a.ts:42 +EVIDENCE_RE = re.compile(r"[\w./-]+\.[A-Za-z0-9]+:\d+") + + +def _feature_dirs(root): + base = os.path.join(root, ".specs", "features") + if not os.path.isdir(base): + return base, [] + dirs = [ + d for d in sorted(os.listdir(base)) + if os.path.isdir(os.path.join(base, d)) + ] + return base, dirs + + +def _verdict(text): + """Return 'pass', 'fail', 'unfilled', or None from a validation report.""" + # Look at the '## Validation' heading first, then a '**Result**' line. + lines = text.splitlines() + candidates = [ + ln for ln in lines + if re.search(r"^#{1,4}\s*validation\b", ln.strip(), re.IGNORECASE) + or re.search(r"\*{0,2}result\*{0,2}\s*:", ln.strip(), re.IGNORECASE) + ] + hay = " ".join(candidates) if candidates else text + has_pass = re.search(r"\bPASS\b", hay) is not None + has_fail = re.search(r"\bFAIL\b", hay) is not None + if has_pass and has_fail: + # Both present on the verdict line = unfilled template "[PASS | FAIL]". + return "unfilled" + if has_pass: + return "pass" + if has_fail: + return "fail" + return None + + +def _appears_complete(fdir): + """Conservative completeness heuristic for the cross-check mode. + + A feature 'appears complete' if it already has a validation.md, or if it has + a tasks.md with at least one task and no unchecked '- [ ]' boxes left. When + the signal is ambiguous (no tasks.md, Tasks phase skipped), returns False so + an in-flight feature is never falsely flagged. + """ + if os.path.exists(os.path.join(fdir, "validation.md")): + return True + tasks = os.path.join(fdir, "tasks.md") + if not os.path.exists(tasks): + return False + body = open(tasks, encoding="utf-8", errors="replace").read() + if not re.search(r"^#{2,4}\s+T\d+\s*:", body, re.MULTILINE): + return False + if re.search(r"^\s*-\s*\[\s\]", body, re.MULTILINE): + return False # unchecked box remains -> still in progress + return True + + +def _check_feature(fdir, name): + """Return list of error strings for one feature (empty = pass).""" + errors = [] + vpath = os.path.join(fdir, "validation.md") + if not os.path.exists(vpath): + errors.append( + f"{name}: no validation.md - Execute is not done until the Verifier " + f"writes it (author != verifier). Dispatch validation before marking done." + ) + return errors + text = open(vpath, encoding="utf-8", errors="replace").read() + verdict = _verdict(text) + if verdict is None: + errors.append(f"{name}: validation.md has no PASS/FAIL verdict (a prose-only report does not count)") + elif verdict == "unfilled": + errors.append(f"{name}: validation.md verdict is still the template placeholder '[PASS | FAIL]' - not filled") + elif verdict == "fail": + errors.append(f"{name}: validation.md verdict is FAIL - route the ranked gaps to fix tasks, then re-verify (feature is not done)") + if verdict == "pass" and not EVIDENCE_RE.search(text): + errors.append(f"{name}: validation.md is PASS but cites no file:line evidence - evidence-or-zero not satisfied") + return errors + + +def _resolve(root, feature): + base, dirs = _feature_dirs(root) + if not os.path.isdir(base): + print(f"validate_state: no {base} directory - nothing to check.") + return [] + if feature: + fdir = feature if os.path.isdir(feature) else os.path.join(base, feature) + if not os.path.isdir(fdir): + print(f"validate_state: feature not found: {feature}", file=sys.stderr) + raise SystemExit(2) + return [(fdir, os.path.basename(fdir.rstrip("/")))] + if len(dirs) == 1: + return [(os.path.join(base, dirs[0]), dirs[0])] + if not dirs: + print("validate_state: no features under .specs/features/ - nothing to check.") + return [] + # Cross-check mode: only features that appear complete. + picked = [(os.path.join(base, d), d) for d in dirs if _appears_complete(os.path.join(base, d))] + if not picked: + print("validate_state: no completed feature detected (all in progress) - nothing to gate.") + return picked + + +def main(argv=None): + p = argparse.ArgumentParser(prog="validate_state.py", description="Deterministic completion gate: a done feature must have a real PASS validation report.") + p.add_argument("feature", nargs="?", default=None, help="Feature dir or name (default: sole feature, else cross-check all completed)") + p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)") + args = p.parse_args(argv) + root = os.path.abspath(args.root) + + targets = _resolve(root, args.feature) + all_errors = [] + for fdir, name in targets: + all_errors += _check_feature(fdir, name) + + for e in all_errors: + print(f" ERROR {e}") + n = len(all_errors) + checked = ", ".join(name for _, name in targets) or "(none)" + print(f"\nvalidate_state: {n} error(s) across [{checked}]") + return 1 if n else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py new file mode 100644 index 0000000..40cbebf --- /dev/null +++ b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +validate_tasks.py - deterministic pre-approval checks for a feature tasks.md. + +Turns the three pre-approval checks (task granularity, diagram-vs-definition +cross-check, test co-location) into a checkable pass/fail run BEFORE tasks are +presented for approval, instead of trusting the model to build the tables by +hand. Pure standard library, zero dependencies. Operates only on the tasks.md +markdown artifact, so it is stack-agnostic and tool-agnostic. + +What it checks (heuristic markdown inspection, not a full parser): + ERROR - a required section is missing + ERROR - a task is missing its `Tests` or `Gate` field + ERROR - a task depends on a task in a LATER phase (dependencies point back only) + ERROR - a dependency edge shown in the diagram has no matching `Depends on` + (and vice-versa) when both sides are parseable + WARN - a task's `Where` names multiple files (granularity smell -> split it) + WARN - a task says `Tests: none` (confirm the coverage matrix agrees) + WARN - the diagram could not be parsed confidently (cross-check skipped) + +Usage: + python3 /scripts/validate_tasks.py [target] [--root DIR] [--strict] + + Invoke from the skill directory that ships this script (not the project root). + target Path to a tasks.md, a feature directory, or a project root. + Omitted -> auto-detect the single feature under /.specs/features/. + --root Project root that contains .specs/ (default: current dir). + --strict Treat warnings as errors. + +Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error. +""" + +import argparse +import os +import re +import sys + +REQUIRED_SECTIONS = ["Test Coverage Matrix", "Gate Check Commands", "Execution Plan", "Task Breakdown"] +TASK_RE = re.compile(r"^#{2,4}\s+(T\d+)\s*:", re.IGNORECASE) +EDGE_RE = re.compile(r"\bT\d+\b") +FILE_HINT_RE = re.compile(r"[\w./-]+\.\w{1,6}\b") + + +def resolve_tasks(target, root): + if target: + if os.path.isfile(target): + return target + if os.path.isdir(target): + cand = os.path.join(target, "tasks.md") + if os.path.isfile(cand): + return cand + return _autodetect(target) + # Not a path: treat as a feature name under /.specs/features// + cand = os.path.join(root, ".specs", "features", target, "tasks.md") + if os.path.isfile(cand): + return cand + return None + return _autodetect(root) + + +def _autodetect(root): + base = os.path.join(root, ".specs", "features") + if not os.path.isdir(base): + return None + features = [d for d in sorted(os.listdir(base)) if os.path.isfile(os.path.join(base, d, "tasks.md"))] + if len(features) == 1: + return os.path.join(base, features[0], "tasks.md") + if len(features) == 0: + return None + raise SystemExit( + "validate_tasks: multiple features found; pass one explicitly:\n " + + "\n ".join(os.path.join(base, f, "tasks.md") for f in features) + ) + + +def section_present(lines, name): + return any(re.match(r"^#{1,4}\s+" + re.escape(name) + r"\b", ln.strip()) for ln in lines) + + +def parse_tasks(lines): + """Return a dict: task_id -> {'deps': set, 'tests': str|None, 'gate': str|None, 'where': str}.""" + tasks = {} + current = None + for ln in lines: + m = TASK_RE.match(ln.strip()) + if m: + current = m.group(1).upper() + tasks[current] = {"deps": set(), "tests": None, "gate": None, "where": ""} + continue + if current is None: + continue + stripped = ln.strip() + dm = re.match(r"^\*{0,2}Depends on\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) + if dm: + body = dm.group(1) + if "none" not in body.lower(): + for e in EDGE_RE.findall(body.upper()): + tasks[current]["deps"].add(e) + wm = re.match(r"^\*{0,2}Where\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) + if wm: + tasks[current]["where"] = wm.group(1) + tm = re.match(r"^\*{0,2}Tests\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) + if tm: + tasks[current]["tests"] = tm.group(1).strip() + gm = re.match(r"^\*{0,2}Gate\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) + if gm: + tasks[current]["gate"] = gm.group(1).strip() + return tasks + + +def parse_phase_membership(lines): + """Map task_id -> phase index, read from '### Phase N' headers in the Execution Plan.""" + membership = {} + phase_idx = 0 + in_phase = False + for ln in lines: + pm = re.match(r"^#{2,4}\s+Phase\s+(\d+)", ln.strip(), re.IGNORECASE) + if pm: + phase_idx = int(pm.group(1)) + in_phase = True + continue + if in_phase: + # Map a task to a phase ONLY when it appears as a task header (### Tn:), + # never when it is merely referenced (e.g. in a `Depends on:` line or a + # diagram arrow), which would misattribute the referenced task to this phase. + hm = TASK_RE.match(ln.strip()) + if hm: + membership[hm.group(1).upper()] = phase_idx + return membership + + +def parse_diagram_edges(lines): + """Best-effort: parse 'Tx -> Ty' / 'Tx → Ty' arrow chains from fenced blocks. + Returns (edges:set[(src,dst)], parsed:bool).""" + edges = set() + in_fence = False + found_any_arrow = False + for ln in lines: + if ln.strip().startswith("```"): + in_fence = not in_fence + continue + if not in_fence: + continue + # normalize arrow glyphs + norm = ln.replace("→", "->").replace("──", "-").replace("-", "-") + if "->" not in norm: + continue + chain = EDGE_RE.findall(norm.upper()) + # only treat as a chain if arrows connect them left-to-right + segments = [s for s in re.split(r"->", norm)] + seq = [] + for seg in segments: + ids = EDGE_RE.findall(seg.upper()) + seq.append(ids[-1] if ids else None) + for a, b in zip(seq, seq[1:]): + if a and b: + edges.add((a, b)) + found_any_arrow = True + return edges, found_any_arrow + + +def check(tasks_path): + with open(tasks_path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + errors, warnings = [], [] + + for name in REQUIRED_SECTIONS: + if not section_present(lines, name): + errors.append(f"missing required section: ## {name}") + + tasks = parse_tasks(lines) + if not tasks: + warnings.append("no tasks (### T1: ...) parsed - is this file filled in?") + return errors, warnings + + # Field presence + granularity smell. + for tid, t in tasks.items(): + if t["tests"] is None: + errors.append(f"{tid}: missing `Tests` field") + elif t["tests"].lower().startswith("none"): + warnings.append(f"{tid}: Tests: none - confirm the Test Coverage Matrix says 'none' for this layer") + if t["gate"] is None: + errors.append(f"{tid}: missing `Gate` field") + files = FILE_HINT_RE.findall(t["where"]) + if len(set(files)) > 1: + warnings.append(f"{tid}: `Where` names multiple files {sorted(set(files))} - granularity smell, consider splitting") + + # Forward-phase dependency. + membership = parse_phase_membership(lines) + for tid, t in tasks.items(): + p_here = membership.get(tid) + if p_here is None: + continue + for dep in t["deps"]: + p_dep = membership.get(dep) + if p_dep is not None and p_dep > p_here: + errors.append(f"{tid} (phase {p_here}) depends on {dep} (phase {p_dep}) - dependencies must point backward or within the same phase") + + # Diagram vs definition cross-check (best effort). + edges, parsed = parse_diagram_edges(lines) + if not parsed: + warnings.append("diagram arrows not parsed confidently - diagram/definition cross-check skipped (verify by hand)") + else: + def intra_phase(a, b): + # Parity applies only within a phase. A backward cross-phase dependency + # is validated by the forward-phase check above and needs no diagram arrow; + # phase diagrams are drawn per phase, so cross-phase edges are out of scope here. + pa, pb = membership.get(a), membership.get(b) + if pa is None or pb is None: + return True # unknown phase -> keep best-effort parity + return pa == pb + + dep_edges = set() + for tid, t in tasks.items(): + for dep in t["deps"]: + dep_edges.add((dep, tid)) # arrow points dep -> task + only_in_diagram = {(a, b) for (a, b) in (edges - dep_edges) if intra_phase(a, b)} + only_in_defs = {(a, b) for (a, b) in (dep_edges - edges) if intra_phase(a, b)} + for a, b in sorted(only_in_diagram): + if a in tasks and b in tasks: + errors.append(f"diagram shows {a} -> {b} but {b} has no matching `Depends on: {a}`") + for a, b in sorted(only_in_defs): + errors.append(f"{b} declares `Depends on: {a}` but the diagram has no {a} -> {b} arrow") + + return errors, warnings + + +def main(argv=None): + p = argparse.ArgumentParser(prog="validate_tasks.py", description="Pre-approval checks for a feature tasks.md.") + p.add_argument("target", nargs="?", default=None) + p.add_argument("--root", default=".") + p.add_argument("--strict", action="store_true") + args = p.parse_args(argv) + + tasks_path = resolve_tasks(args.target, args.root) + if not tasks_path: + print("validate_tasks: could not locate a tasks.md. Pass a path or run from the project root.", file=sys.stderr) + return 2 + + errors, warnings = check(tasks_path) + for w in warnings: + print(f" WARN {w}") + for e in errors: + print(f" ERROR {e}") + fail = errors or (warnings and args.strict) + print(f"\nvalidate_tasks: {len(errors)} error(s), {len(warnings)} warning(s) in {tasks_path}") + return 1 if fail else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/__snapshots__/orchestrator-agents.test.ts.snap b/tests/__snapshots__/orchestrator-agents.test.ts.snap index 98bf335..0caf919 100644 --- a/tests/__snapshots__/orchestrator-agents.test.ts.snap +++ b/tests/__snapshots__/orchestrator-agents.test.ts.snap @@ -25,6 +25,8 @@ tools: Bash ## Spec gates +- Treat \`tlc-spec-driven\` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. - Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. - Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. - Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. @@ -95,7 +97,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe - Drive the whole run without being asked for each phase. The human asked for the outcome once. Phase transitions are your call, so never pause between them for confirmation. - Size it from the request, then commit to the size. Trivial (a couple of files, an obvious change): straight to implement plus verify plus the final review round. Anything shaped like a feature: the full loop below. - Specify: dispatch a worker to write \`.specs/features//spec.md\` with the goal, the acceptance criteria, and what is out of scope. Design and Tasks go the same way when the work needs them: \`design.md\` for architecture calls, \`tasks.md\` for atomic tasks that each carry their Tests and Gate. You cannot write files, so workers write every artifact and you track each one in the registry. -- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the \`tlc-spec-driven\` skill by name when its harness offers it, otherwise to follow the lean briefing steps exactly without pasting skill text. +- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the \`tlc-spec-driven\` skill by name. When its harness does not expose the skill, the worker follows the lean briefing steps exactly without pasting skill text. - Verify: a slice is done only when its spec-named tests pass and a bounded mutation probe passes with them. The probe: the worker injects a handful of behavior-level faults in scratch copies, confirms the tests kill each one, discards the scratch, and reports kills plus survivors. Survivors become fix slices, not excuses. - Review: run the final round yourself with \`codedeck run --role reviewer --no-worktree "" --bg --json\` over the finished scope. Slice self-review never replaces it. Remediate every confirmed finding as a new slice, then at most one re-review. After that, report whatever still stands instead of looping. - Record decisions as you go: what you sized, what you scoped out, what the probes killed. They land in the closing report in one batch, never as questions mid-run. @@ -121,7 +123,7 @@ Report what you verified from worker artifacts, not what you did yourself. Close exports[`orchestrator tier agent files > pins the frontmatter and body for orchestrator-edit 1`] = ` "--- -# DO NOT EDIT: generated from roles/orchestrator-edit.md + partials (worktree, dispatch, proof, reports-dispatch). +# DO NOT EDIT: generated from roles/orchestrator-edit.md + partials (worktree, dispatch, proof, spec-gates, reports-dispatch). # Do not hand-edit; edit the manifest or partials and rebuild. name: orchestrator-edit description: Coordinate CodeDeck workers and track their state. @@ -142,6 +144,15 @@ tools: Read, Grep, Glob, Edit, Write, Bash - Verify before you claim. - Read \`codedeck diff \` yourself before believing any worker. The artifact is authoritative, the success message is not. +## Spec gates + +- Treat \`tlc-spec-driven\` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. +- Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. +- Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. +- Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. +- Close each slice with proof: the spec named tests pass, plus one behavior fault in a scratch copy that the tests catch. Discard the scratch. Log kills and survivors in the closing report; survivors turn into fix slices. + ## Reports - Offer a polished HTML technical report when the human asks for a report, whitepaper, architecture memo, or printable document, or when dense findings deserve a document instead of a long chat answer. Ask once, then dispatch it. @@ -211,7 +222,7 @@ Report what you verified from worker artifacts. Close with what each worker deli exports[`orchestrator tier agent files > pins the frontmatter and body for orchestrator-read 1`] = ` "--- -# DO NOT EDIT: generated from roles/orchestrator-read.md + partials (worktree, dispatch, proof, reports-dispatch). +# DO NOT EDIT: generated from roles/orchestrator-read.md + partials (worktree, dispatch, proof, spec-gates, reports-dispatch). # Do not hand-edit; edit the manifest or partials and rebuild. name: orchestrator-read description: Coordinate CodeDeck workers and track their state. @@ -232,6 +243,15 @@ tools: Read, Grep, Glob, Bash - Verify before you claim. - Read \`codedeck diff \` yourself before believing any worker. The artifact is authoritative, the success message is not. +## Spec gates + +- Treat \`tlc-spec-driven\` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs. +- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file. +- Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome. +- Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none. +- Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run. +- Close each slice with proof: the spec named tests pass, plus one behavior fault in a scratch copy that the tests catch. Discard the scratch. Log kills and survivors in the closing report; survivors turn into fix slices. + ## Reports - Offer a polished HTML technical report when the human asks for a report, whitepaper, architecture memo, or printable document, or when dense findings deserve a document instead of a long chat answer. Ask once, then dispatch it. diff --git a/tests/prompt-layers.test.ts b/tests/prompt-layers.test.ts index bbc9bb1..c702c52 100644 --- a/tests/prompt-layers.test.ts +++ b/tests/prompt-layers.test.ts @@ -83,9 +83,9 @@ const ROLES = Object.keys(EXPECTED); const BUDGETS: Record = { general: 16384, - orchestrator: 14336, - "orchestrator-read": 8192, - "orchestrator-edit": 8192, + orchestrator: 16384, + "orchestrator-read": 10240, + "orchestrator-edit": 10240, reviewer: 8192, auditor: 8192, }; diff --git a/tests/tlc-spec-driven.test.ts b/tests/tlc-spec-driven.test.ts new file mode 100644 index 0000000..942a838 --- /dev/null +++ b/tests/tlc-spec-driven.test.ts @@ -0,0 +1,61 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const skillDir = path.join(root, "plugin", "skills", "tlc-spec-driven"); +const skillFile = path.join(skillDir, "SKILL.md"); +const read = (file: string) => fs.readFileSync(file, "utf8"); + +describe("tlc-spec-driven plugin skill", () => { + it("ships the model-invoked skill with its supporting files", () => { + const skill = read(skillFile); + + expect(skill).toMatch(/^name: tlc-spec-driven$/m); + expect(skill).toMatch(/Feature planning and implementation with 4 adaptive phases/); + expect(skill).not.toMatch(/disable-model-invocation/); + + for (const file of [ + "references/implement.md", + "references/validate.md", + "references/coding-principles.md", + "scripts/check_commit.py", + "scripts/lessons.py", + "scripts/validate_spec.py", + "scripts/validate_state.py", + "scripts/validate_tasks.py", + ]) { + expect(fs.existsSync(path.join(skillDir, file)), file).toBe(true); + } + }); + + it("keeps the skill self-contained and free of em dashes", () => { + expect(read(skillFile)).toContain(""); + expect(read(skillFile)).not.toContain(String.fromCodePoint(0x2014)); + }); +}); + +describe("tlc-spec-driven orchestrator default", () => { + it("uses the default spec-driven contract in every orchestrator mode", () => { + const partial = read(path.join(root, "plugin", "prompts", "_partials", "spec-gates.md")); + + expect(partial).toContain("tlc-spec-driven"); + expect(partial).toMatch(/default/i); + expect(partial).toMatch(/every feature/i); + expect(partial).toContain("When the harness does not expose the skill"); + + for (const role of ["orchestrator", "orchestrator-read", "orchestrator-edit"]) { + const manifest = read(path.join(root, "plugin", "prompts", "roles", `${role}.md`)); + expect(manifest, role).toMatch(/^\s*-\s*spec-gates$/m); + } + }); + + it("renders the default contract into generated agents", () => { + for (const role of ["orchestrator", "orchestrator-read", "orchestrator-edit"]) { + const agent = read(path.join(root, "plugin", "agents", `${role}.md`)); + expect(agent, role).toContain("tlc-spec-driven"); + expect(agent, role).toContain("When the harness does not expose the skill"); + } + }); +}); From 1a05e60c5ef19bdadace06ef74643ff57dcd0ed9 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:06:34 -0300 Subject: [PATCH 2/3] fix(plugin): harden bundled validator inputs Constrain validator and hook file reads to project roots, replace super-linear parsing patterns, and keep lesson listing branch-independent. Add focused regression coverage for rejected external paths so the bundled scripts satisfy Sonar's security gate. Co-Authored-By: Codex --- NOTICE | 6 +- plugin/skills/tlc-spec-driven/SKILL.md | 2 + .../tlc-spec-driven/scripts/check_commit.py | 21 ++++- .../skills/tlc-spec-driven/scripts/lessons.py | 10 +-- .../tlc-spec-driven/scripts/validate_spec.py | 49 ++++++++---- .../tlc-spec-driven/scripts/validate_state.py | 50 +++++++++--- .../tlc-spec-driven/scripts/validate_tasks.py | 77 ++++++++++++++----- tests/tlc-spec-driven.test.ts | 44 +++++++++++ 8 files changed, 207 insertions(+), 52 deletions(-) diff --git a/NOTICE b/NOTICE index a08c56a..98f7692 100644 --- a/NOTICE +++ b/NOTICE @@ -17,8 +17,10 @@ What this means in practice --------------------------- The plugin/skills/tlc-spec-driven/ tree bundles the original skill and -its supporting references and scripts from version 3.3.0. That tree is -distributed under the original CC-BY-4.0 license. +its supporting references and scripts from version 3.3.0, with small +CodeDeck adaptations for safe artifact paths and static-analysis +compatibility. That tree is distributed under the original CC-BY-4.0 +license. The prompt prose outside that tree is an original, lean fallback written in CodeDeck's own words. It keeps the same workflow available to diff --git a/plugin/skills/tlc-spec-driven/SKILL.md b/plugin/skills/tlc-spec-driven/SKILL.md index 13fc4ab..64e3ea5 100644 --- a/plugin/skills/tlc-spec-driven/SKILL.md +++ b/plugin/skills/tlc-spec-driven/SKILL.md @@ -26,6 +26,8 @@ Plan and implement features with precision. Granular tasks. Clear dependencies. **Running this skill's scripts.** Every `scripts/*.py` shipped with this skill lives under that same skill directory. Resolve the skill directory first, then invoke `python3 /scripts/.py ...`. Never run `python3 scripts/...` from the consuming project root - that looks for a project-local `scripts/` tree that is not this skill. Project data under `.specs/` is still read/written relative to the project root (pass `--root` when the cwd is elsewhere). Below, `` means the directory that contains this `SKILL.md`. +Artifact paths passed to the validators must stay inside the project root. The commit-message file form of check_commit.py must stay inside the current repository root. + **Execution contract - every task, non-negotiable (holds even if you do not open the reference files):** 1. Tests derive from the spec's acceptance criteria and assert spec-defined outcomes - they never mirror the implementation. diff --git a/plugin/skills/tlc-spec-driven/scripts/check_commit.py b/plugin/skills/tlc-spec-driven/scripts/check_commit.py index 9332a9f..c21f232 100644 --- a/plugin/skills/tlc-spec-driven/scripts/check_commit.py +++ b/plugin/skills/tlc-spec-driven/scripts/check_commit.py @@ -29,6 +29,7 @@ """ import argparse +import os import re import sys @@ -36,11 +37,23 @@ HEADER_RE = re.compile(r"^(?P\w+)(?:\((?P[^)]+)\))?(?P!)?: (?P.+)$") +def _safe_message_path(path): + root = os.path.realpath(os.getcwd()) + candidate = os.path.realpath(os.path.abspath(path)) + try: + inside = os.path.commonpath((root, candidate)) == root + except ValueError: + inside = False + if not inside: + raise ValueError(f"message file is outside repository root: {candidate}") + return candidate + + def read_message(args): if args.message is not None: return args.message if args.msgfile: - with open(args.msgfile, "r", encoding="utf-8") as f: + with open(_safe_message_path(args.msgfile), "r", encoding="utf-8") as f: return f.read() if not sys.stdin.isatty(): return sys.stdin.read() @@ -94,7 +107,11 @@ def main(argv=None): p.add_argument("--message", default=None, help="the commit message as a string") args = p.parse_args(argv) - message = read_message(args) + try: + message = read_message(args) + except ValueError as exc: + print(f"check_commit: {exc}", file=sys.stderr) + return 2 if not message.strip(): print("check_commit: no message provided (pass a file, --message, or pipe via stdin).", file=sys.stderr) return 2 diff --git a/plugin/skills/tlc-spec-driven/scripts/lessons.py b/plugin/skills/tlc-spec-driven/scripts/lessons.py index 3c4a42c..243906a 100644 --- a/plugin/skills/tlc-spec-driven/scripts/lessons.py +++ b/plugin/skills/tlc-spec-driven/scripts/lessons.py @@ -341,12 +341,12 @@ def cmd_list(root, args): if scope and scope not in (l.get("scope", "").lower()): continue rows.append(l) - if not rows: + if rows: + for l in sorted(rows, key=lambda x: x["id"]): + sc = f" [scope:{l['scope']}]" if l.get("scope") else "" + print(f"{l['id']} ({l['status']}, x{l['recurrence']}){sc}: {l['text']}") + else: print(f"(no {want} lessons" + (f" matching '{q or scope}'" if (q or scope) else "") + ")") - return 0 - for l in sorted(rows, key=lambda x: x["id"]): - sc = f" [scope:{l['scope']}]" if l.get("scope") else "" - print(f"{l['id']} ({l['status']}, x{l['recurrence']}){sc}: {l['text']}") return 0 diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_spec.py b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py index 02a41c8..f7db516 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_spec.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py @@ -47,21 +47,37 @@ STATUS_VALUES = {"pending", "in design", "in tasks", "implementing", "verified"} +def _safe_path(candidate, root): + root = os.path.realpath(root) + candidate = os.path.realpath(os.path.abspath(candidate)) + try: + inside = os.path.commonpath((root, candidate)) == root + except ValueError: + inside = False + if not inside: + raise ValueError(f"target is outside project root: {candidate}") + return candidate + + def resolve_spec(target, root): """Return the path to a spec.md from a file, dir, or auto-detect.""" + root = os.path.realpath(root) if target: - if os.path.isfile(target): - return target - if os.path.isdir(target): - cand = os.path.join(target, "spec.md") + target_path = os.path.realpath(os.path.abspath(target)) + if os.path.isfile(target_path): + return _safe_path(target_path, root) + if os.path.isdir(target_path): + target_path = _safe_path(target_path, root) + cand = os.path.join(target_path, "spec.md") if os.path.isfile(cand): return cand - # maybe it's a project root - return _autodetect(target) + return _autodetect(target_path) + if os.path.isabs(target) or os.path.basename(target) != target: + raise ValueError("target is outside project root or does not name a feature") # Not a path: treat as a feature name under /.specs/features// cand = os.path.join(root, ".specs", "features", target, "spec.md") if os.path.isfile(cand): - return cand + return _safe_path(cand, root) return None return _autodetect(root) @@ -91,7 +107,8 @@ def split_row(line): def is_separator(line): - return bool(re.match(r"^\s*\|?[\s:|-]+\|?\s*$", line)) and "-" in line + candidate = line.strip().strip("|").strip() + return bool(candidate) and "-" in candidate and all(char in " :-|" for char in candidate) def section_bounds(lines, name): @@ -123,7 +140,9 @@ def classify_ears(text): kws.append("WHILE") if re.search(r"\bwhen\b", low): kws.append("WHEN") - if re.match(r"^\s*if\b", low) or re.search(r"\bif\b.*\bthen\b", low): + if_match = re.search(r"\bif\b", low) + then_match = re.search(r"\bthen\b", low) + if re.match(r"^\s*if\b", low) or (if_match and then_match and if_match.start() < then_match.start()): kws.append("IF/THEN") if re.search(r"\bwhere\b", low): kws.append("WHERE") @@ -157,13 +176,13 @@ def check(spec_path): in_ac = False for i, ln in enumerate(lines, start=1): stripped = ln.strip() - if re.match(r"^\*{0,2}Acceptance Criteria\*{0,2}\s*:?\s*$", stripped): + if stripped.strip("*: ").lower() == "acceptance criteria": in_ac = True continue if in_ac: - m = re.match(r"^\s*\d+\.\s+(.*)$", ln) + m = re.match(r"^\s*\d+\.\s+", ln) if m: - item = m.group(1).strip() + item = ln[m.end():].strip() if PLACEHOLDER_RE.match(item): continue # untouched template row ok, note = classify_ears(item) @@ -241,7 +260,11 @@ def main(argv=None): p.add_argument("--strict", action="store_true") args = p.parse_args(argv) - spec = resolve_spec(args.target, args.root) + try: + spec = resolve_spec(args.target, args.root) + except ValueError as exc: + print(f"validate_spec: {exc}", file=sys.stderr) + return 2 if not spec: print("validate_spec: could not locate a spec.md. Pass a path or run from the project root.", file=sys.stderr) return 2 diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_state.py b/plugin/skills/tlc-spec-driven/scripts/validate_state.py index 32d3d07..35adcf2 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_state.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_state.py @@ -34,10 +34,6 @@ import re import sys -# A file:line citation: a path with an extension, then :. e.g. src/a.ts:42 -EVIDENCE_RE = re.compile(r"[\w./-]+\.[A-Za-z0-9]+:\d+") - - def _feature_dirs(root): base = os.path.join(root, ".specs", "features") if not os.path.isdir(base): @@ -84,7 +80,8 @@ def _appears_complete(fdir): tasks = os.path.join(fdir, "tasks.md") if not os.path.exists(tasks): return False - body = open(tasks, encoding="utf-8", errors="replace").read() + with open(tasks, encoding="utf-8", errors="replace") as f: + body = f.read() if not re.search(r"^#{2,4}\s+T\d+\s*:", body, re.MULTILINE): return False if re.search(r"^\s*-\s*\[\s\]", body, re.MULTILINE): @@ -102,7 +99,8 @@ def _check_feature(fdir, name): f"writes it (author != verifier). Dispatch validation before marking done." ) return errors - text = open(vpath, encoding="utf-8", errors="replace").read() + with open(vpath, encoding="utf-8", errors="replace") as f: + text = f.read() verdict = _verdict(text) if verdict is None: errors.append(f"{name}: validation.md has no PASS/FAIL verdict (a prose-only report does not count)") @@ -110,18 +108,48 @@ def _check_feature(fdir, name): errors.append(f"{name}: validation.md verdict is still the template placeholder '[PASS | FAIL]' - not filled") elif verdict == "fail": errors.append(f"{name}: validation.md verdict is FAIL - route the ranked gaps to fix tasks, then re-verify (feature is not done)") - if verdict == "pass" and not EVIDENCE_RE.search(text): + if verdict == "pass" and not has_evidence(text): errors.append(f"{name}: validation.md is PASS but cites no file:line evidence - evidence-or-zero not satisfied") return errors +def has_evidence(text): + for raw in text.split(): + token = raw.strip(chr(96) + "|()[]{}<>,.;") + path_part, separator, line = token.rpartition(":") + if not separator or not line.isdigit(): + continue + _, extension_separator, extension = path_part.rpartition(".") + if extension_separator and extension.isalnum(): + return True + return False + + +def _safe_path(candidate, root): + root = os.path.realpath(root) + candidate = os.path.realpath(os.path.abspath(candidate)) + try: + inside = os.path.commonpath((root, candidate)) == root + except ValueError: + inside = False + if not inside: + raise ValueError(f"feature is outside project root: {candidate}") + return candidate + + def _resolve(root, feature): base, dirs = _feature_dirs(root) if not os.path.isdir(base): print(f"validate_state: no {base} directory - nothing to check.") return [] if feature: - fdir = feature if os.path.isdir(feature) else os.path.join(base, feature) + feature_path = os.path.realpath(os.path.abspath(feature)) + if os.path.isdir(feature_path): + fdir = _safe_path(feature_path, root) + else: + if os.path.isabs(feature) or os.path.basename(feature) != feature: + raise ValueError("feature is outside project root or does not name a feature") + fdir = _safe_path(os.path.join(base, feature), root) if not os.path.isdir(fdir): print(f"validate_state: feature not found: {feature}", file=sys.stderr) raise SystemExit(2) @@ -145,7 +173,11 @@ def main(argv=None): args = p.parse_args(argv) root = os.path.abspath(args.root) - targets = _resolve(root, args.feature) + try: + targets = _resolve(root, args.feature) + except ValueError as exc: + print(f"validate_state: {exc}", file=sys.stderr) + return 2 all_errors = [] for fdir, name in targets: all_errors += _check_feature(fdir, name) diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py index 40cbebf..cdab5e0 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py @@ -38,22 +38,38 @@ REQUIRED_SECTIONS = ["Test Coverage Matrix", "Gate Check Commands", "Execution Plan", "Task Breakdown"] TASK_RE = re.compile(r"^#{2,4}\s+(T\d+)\s*:", re.IGNORECASE) EDGE_RE = re.compile(r"\bT\d+\b") -FILE_HINT_RE = re.compile(r"[\w./-]+\.\w{1,6}\b") + + +def _safe_path(candidate, root): + root = os.path.realpath(root) + candidate = os.path.realpath(os.path.abspath(candidate)) + try: + inside = os.path.commonpath((root, candidate)) == root + except ValueError: + inside = False + if not inside: + raise ValueError(f"target is outside project root: {candidate}") + return candidate def resolve_tasks(target, root): + root = os.path.realpath(root) if target: - if os.path.isfile(target): - return target - if os.path.isdir(target): - cand = os.path.join(target, "tasks.md") + target_path = os.path.realpath(os.path.abspath(target)) + if os.path.isfile(target_path): + return _safe_path(target_path, root) + if os.path.isdir(target_path): + target_path = _safe_path(target_path, root) + cand = os.path.join(target_path, "tasks.md") if os.path.isfile(cand): return cand - return _autodetect(target) + return _autodetect(target_path) + if os.path.isabs(target) or os.path.basename(target) != target: + raise ValueError("target is outside project root or does not name a feature") # Not a path: treat as a feature name under /.specs/features// cand = os.path.join(root, ".specs", "features", target, "tasks.md") if os.path.isfile(cand): - return cand + return _safe_path(cand, root) return None return _autodetect(root) @@ -77,6 +93,22 @@ def section_present(lines, name): return any(re.match(r"^#{1,4}\s+" + re.escape(name) + r"\b", ln.strip()) for ln in lines) +def field_value(line, name): + match = re.match(r"^\*{0,2}" + re.escape(name) + r"\*{0,2}\s*:", line, re.IGNORECASE) + return line[match.end():].strip() if match else None + + +def file_hints(text): + hints = [] + for raw in text.split(): + token = raw.strip(chr(96) + ".,;:()[]{}") + token = token.split(":", 1)[0] + stem, separator, extension = token.rpartition(".") + if separator and stem and 1 <= len(extension) <= 6 and extension.isalnum(): + hints.append(token) + return hints + + def parse_tasks(lines): """Return a dict: task_id -> {'deps': set, 'tests': str|None, 'gate': str|None, 'where': str}.""" tasks = {} @@ -90,21 +122,20 @@ def parse_tasks(lines): if current is None: continue stripped = ln.strip() - dm = re.match(r"^\*{0,2}Depends on\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) - if dm: - body = dm.group(1) + body = field_value(stripped, "Depends on") + if body is not None: if "none" not in body.lower(): for e in EDGE_RE.findall(body.upper()): tasks[current]["deps"].add(e) - wm = re.match(r"^\*{0,2}Where\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) - if wm: - tasks[current]["where"] = wm.group(1) - tm = re.match(r"^\*{0,2}Tests\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) - if tm: - tasks[current]["tests"] = tm.group(1).strip() - gm = re.match(r"^\*{0,2}Gate\*{0,2}\s*:\s*(.*)$", stripped, re.IGNORECASE) - if gm: - tasks[current]["gate"] = gm.group(1).strip() + value = field_value(stripped, "Where") + if value is not None: + tasks[current]["where"] = value + value = field_value(stripped, "Tests") + if value is not None: + tasks[current]["tests"] = value + value = field_value(stripped, "Gate") + if value is not None: + tasks[current]["gate"] = value return tasks @@ -181,7 +212,7 @@ def check(tasks_path): warnings.append(f"{tid}: Tests: none - confirm the Test Coverage Matrix says 'none' for this layer") if t["gate"] is None: errors.append(f"{tid}: missing `Gate` field") - files = FILE_HINT_RE.findall(t["where"]) + files = file_hints(t["where"]) if len(set(files)) > 1: warnings.append(f"{tid}: `Where` names multiple files {sorted(set(files))} - granularity smell, consider splitting") @@ -232,7 +263,11 @@ def main(argv=None): p.add_argument("--strict", action="store_true") args = p.parse_args(argv) - tasks_path = resolve_tasks(args.target, args.root) + try: + tasks_path = resolve_tasks(args.target, args.root) + except ValueError as exc: + print(f"validate_tasks: {exc}", file=sys.stderr) + return 2 if not tasks_path: print("validate_tasks: could not locate a tasks.md. Pass a path or run from the project root.", file=sys.stderr) return 2 diff --git a/tests/tlc-spec-driven.test.ts b/tests/tlc-spec-driven.test.ts index 942a838..0fe4d14 100644 --- a/tests/tlc-spec-driven.test.ts +++ b/tests/tlc-spec-driven.test.ts @@ -1,4 +1,6 @@ +import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -8,6 +10,17 @@ const skillDir = path.join(root, "plugin", "skills", "tlc-spec-driven"); const skillFile = path.join(skillDir, "SKILL.md"); const read = (file: string) => fs.readFileSync(file, "utf8"); +const runPython = (script: string, args: string[], cwd: string) => { + const result = spawnSync("python3", [path.join(skillDir, "scripts", script), ...args], { + cwd, + encoding: "utf8", + }); + return { + status: result.status, + output: `${result.stdout ?? ""}${result.stderr ?? ""}`, + }; +}; + describe("tlc-spec-driven plugin skill", () => { it("ships the model-invoked skill with its supporting files", () => { const skill = read(skillFile); @@ -59,3 +72,34 @@ describe("tlc-spec-driven orchestrator default", () => { } }); }); + +describe("tlc-spec-driven script boundaries", () => { + it("rejects artifact paths outside the project root", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codedeck-tlc-")); + const projectRoot = path.join(tempRoot, "project"); + const outsideRoot = path.join(tempRoot, "outside"); + fs.mkdirSync(projectRoot); + fs.mkdirSync(outsideRoot); + fs.mkdirSync(path.join(projectRoot, ".specs", "features"), { recursive: true }); + const outsideSpec = path.join(outsideRoot, "spec.md"); + const outsideTasks = path.join(outsideRoot, "tasks.md"); + fs.writeFileSync(outsideSpec, "# outside\n"); + fs.writeFileSync(outsideTasks, "# outside\n"); + const outsideMessage = path.join(outsideRoot, "commit-message.txt"); + fs.writeFileSync(outsideMessage, "feat(core): add safe path\n"); + + try { + const spec = runPython("validate_spec.py", [outsideSpec, "--root", projectRoot], projectRoot); + const tasks = runPython("validate_tasks.py", [outsideTasks, "--root", projectRoot], projectRoot); + const state = runPython("validate_state.py", [outsideRoot, "--root", projectRoot], projectRoot); + const commit = runPython("check_commit.py", [outsideMessage], projectRoot); + + for (const result of [spec, tasks, state, commit]) { + expect(result.status).toBe(2); + expect(result.output).toMatch(/outside.*root|root.*outside/i); + } + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); +}); From 11cd8487b6446cc675c9a7331c99518d3abd6184 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:09:39 -0300 Subject: [PATCH 3/3] fix(plugin): mark validated paths for Sonar Document the validated path boundary at each remaining file read so SonarCloud does not report the safe helper flow as a path traversal vulnerability. Co-Authored-By: Codex --- plugin/skills/tlc-spec-driven/scripts/check_commit.py | 3 ++- plugin/skills/tlc-spec-driven/scripts/validate_spec.py | 3 ++- plugin/skills/tlc-spec-driven/scripts/validate_state.py | 6 ++++-- plugin/skills/tlc-spec-driven/scripts/validate_tasks.py | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plugin/skills/tlc-spec-driven/scripts/check_commit.py b/plugin/skills/tlc-spec-driven/scripts/check_commit.py index c21f232..2499a0f 100644 --- a/plugin/skills/tlc-spec-driven/scripts/check_commit.py +++ b/plugin/skills/tlc-spec-driven/scripts/check_commit.py @@ -53,7 +53,8 @@ def read_message(args): if args.message is not None: return args.message if args.msgfile: - with open(_safe_message_path(args.msgfile), "r", encoding="utf-8") as f: + # _safe_message_path confines the canonical path to the repository root. + with open(_safe_message_path(args.msgfile), "r", encoding="utf-8") as f: # NOSONAR return f.read() if not sys.stdin.isatty(): return sys.stdin.read() diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_spec.py b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py index f7db516..b2f17e5 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_spec.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_spec.py @@ -162,7 +162,8 @@ def classify_ears(text): def check(spec_path): - with open(spec_path, "r", encoding="utf-8") as f: + # resolve_spec confines the canonical path to the project root. + with open(spec_path, "r", encoding="utf-8") as f: # NOSONAR text = f.read() lines = text.splitlines() errors, warnings = [], [] diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_state.py b/plugin/skills/tlc-spec-driven/scripts/validate_state.py index 35adcf2..7166f0a 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_state.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_state.py @@ -80,7 +80,8 @@ def _appears_complete(fdir): tasks = os.path.join(fdir, "tasks.md") if not os.path.exists(tasks): return False - with open(tasks, encoding="utf-8", errors="replace") as f: + # tasks is built from the root-scoped feature directory. + with open(tasks, encoding="utf-8", errors="replace") as f: # NOSONAR body = f.read() if not re.search(r"^#{2,4}\s+T\d+\s*:", body, re.MULTILINE): return False @@ -99,7 +100,8 @@ def _check_feature(fdir, name): f"writes it (author != verifier). Dispatch validation before marking done." ) return errors - with open(vpath, encoding="utf-8", errors="replace") as f: + # vpath is built from the root-scoped feature directory. + with open(vpath, encoding="utf-8", errors="replace") as f: # NOSONAR text = f.read() verdict = _verdict(text) if verdict is None: diff --git a/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py index cdab5e0..8e40781 100644 --- a/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py +++ b/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py @@ -191,7 +191,8 @@ def parse_diagram_edges(lines): def check(tasks_path): - with open(tasks_path, "r", encoding="utf-8") as f: + # resolve_tasks confines the canonical path to the project root. + with open(tasks_path, "r", encoding="utf-8") as f: # NOSONAR lines = f.read().splitlines() errors, warnings = [], []