@antoneeo/agentic-sdlc-skill 1.8.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +6 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -1
- package/skills/agentic-sdlc-skill/SKILL.md +9 -2
- package/skills/agentic-sdlc-skill/debugging.md +50 -0
- package/skills/agentic-sdlc-skill/dispatch.md +77 -0
- package/skills/agentic-sdlc-skill/elicitation.md +45 -0
- package/skills/agentic-sdlc-skill/guides.md +14 -4
- package/skills/agentic-sdlc-skill/review.md +57 -0
- package/skills/agentic-sdlc-skill/scripts/sdlc_check.py +241 -9
- package/skills/agentic-sdlc-skill/tdd.md +59 -0
- package/skills/agentic-sdlc-skill/templates.md +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Tutte le modifiche significative a questa skill saranno documentate in questo file.
|
|
4
4
|
|
|
5
|
+
## [1.10.0] - 2026-07-03 (M3: Subagent Execution / Feature A — opt-in executable plan)
|
|
6
|
+
### Added
|
|
7
|
+
- **`dispatch.md`**: subagent-execution doctrine (opt-in for L3). The dispatch loop — validate the plan → per-task brief → economy-tier implementer → one-shot review → ledger — with client-relative model tiers (no provider names), one-shot review slots (no iterative loops), degradation to same-session where subagents do not exist, and guides injected by pointer (never pasted).
|
|
8
|
+
- **`sdlc_check.py plan` subcommand**: `plan validate` (schema check of the executable plan, fail-closed path/guide confinement, sidecar-ledger cross-check — "no valid plan, no dispatch") and `plan brief --task <id>` (emits, to stdout, the task + prior-task interfaces + guide pointers). The validator is **zero-execution**: a task's `verify` command is emitted as text, never run.
|
|
9
|
+
- **Executable-plan template** (`ai_docs/solutions/PLAN_[feature].md`) in `templates.md`: Markdown frontmatter (`status`, `derived-from`) + a fenced `json` task array + the sidecar `PLAN_[feature].ledger.json` shape (`task_id -> {status, verify_result, timestamp}`, git-tracked, survives compaction).
|
|
10
|
+
- **`SKILL.md` §4** opt-in subagent-execution hook + the Hybrid `derived-from` seam (the plan is derived from the accepted E-TDD, never independently authored).
|
|
11
|
+
- `test_plan.py`: stdlib-`unittest` battery for the `plan` subcommand (32 cases: schema, confinement, ledger, fail-fast JSON, zero-execution poka-yoke).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **`confine_under(base, rel)` extracted** in `sdlc_check.py`: the fail-closed path-confinement pattern (absolute/`..`/resolve-escape → reject), previously inlined twice (the `overrides:` and `distilled_from` checks), is now a single helper reused by both plus the new plan-path / guide-pointer confinement. Behavior-preserving (catches `(ValueError, OSError)`).
|
|
15
|
+
|
|
16
|
+
### Process note
|
|
17
|
+
- 4th live **model-per-dispatch** run (economy implementer from the E-TDD shadow, battery 32/32, deep code review PASS zero BLOCK). Governance: M-VISION → D-UC → P-TM → E-ISP → E-TDD, all through the independent review gate — which killed 3 real BLOCKs at design time (T1 subprocess-invariant misstatement, a missing impacted file, a `confine_under` OSError-crash regression). ADR `adr_2026-07-03_executable_plan_json_in_md`.
|
|
18
|
+
|
|
19
|
+
## [1.9.0] - 2026-07-03 (M2 execution disciplines + Feature B unit 2 agent KB)
|
|
20
|
+
### Added
|
|
21
|
+
- `tdd.md`: TDD discipline (RED/GREEN/REFACTOR, increment rule, AAA test shape, documented exemptions) — the L2/L3 default for implementation work.
|
|
22
|
+
- `debugging.md`: systematic debugging method (reproduce, isolate, root cause, fix, regression test, collateral check) with circuit-breaker integration.
|
|
23
|
+
- `elicitation.md`: spec elicitation round (goal/benefit, scope boundaries, non-goals, constraints, acceptance signals) run before drafting the analysis in phase 3.
|
|
24
|
+
- `review.md`: single definition of requesting, receiving, and performing code review, reused by the Hybrid review gates.
|
|
25
|
+
- `SKILL.md` wiring: pointers to the four new files added at phase 3 (elicitation), phase 4 (TDD, debugging, circuit breaker), and phase 5 (review).
|
|
26
|
+
- Agent-global KB (fixed root, project-wins precedence, `overrides:` with fail-closed confinement, collision warnings).
|
|
27
|
+
|
|
5
28
|
## [1.8.1] - 2026-07-02
|
|
6
29
|
### Fixed
|
|
7
30
|
- **Guide freshness hash is now line-ending independent**: `sha256_file` in `sdlc_check.py` normalizes CRLF → LF before hashing. Previously the raw-byte hash made a fresh Windows checkout with `core.autocrlf=true` rewrite `.sources/` snapshots and flag every guide `[stale]` (false positive). Backward compatible: recorded hashes were computed on LF content, and normalization maps CRLF copies back to the same digest. (Edge case: a hash recorded pre-1.8.1 on a snapshot that genuinely contained CRLF bytes will flag `[stale]` once — regenerate the hash.)
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- **Vision-guided governance**: Standalone projects use `ai_docs/vision/`; Hybrid projects use devPNT `M-VISION` as the milestone north star.
|
|
9
9
|
- **Standalone complete**: works fully with local `ai_docs/` without requiring devPNT.
|
|
10
10
|
- **devPNT symbiosis**: when devPNT is available, Master Plan, Action Plan, M-VISION, and governed artifacts become the authoritative planning layer.
|
|
11
|
-
- **Installed support files**: Claude, Codex, and Gemini receive the full skill folder, including `templates.md`, `guides.md`, `ENFORCEMENT.md`, and `scripts/sdlc_check.py
|
|
11
|
+
- **Installed support files**: Claude, Codex, and Gemini receive the full skill folder, including `templates.md`, `guides.md`, `tdd.md`, `debugging.md`, `elicitation.md`, `review.md`, `dispatch.md`, `ENFORCEMENT.md`, and `scripts/sdlc_check.py`; an agent-global KB at `~/.agentic-sdlc` shares the same engine cross-project.
|
|
12
12
|
- **Mechanical checks**: optional validator for document structure, generated feature history, stale audit areas, and protected-path gates.
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -45,6 +45,11 @@ skills/agentic-sdlc-skill/
|
|
|
45
45
|
├── SKILL.md
|
|
46
46
|
├── templates.md
|
|
47
47
|
├── guides.md
|
|
48
|
+
├── tdd.md
|
|
49
|
+
├── debugging.md
|
|
50
|
+
├── elicitation.md
|
|
51
|
+
├── review.md
|
|
52
|
+
├── dispatch.md
|
|
48
53
|
├── ENFORCEMENT.md
|
|
49
54
|
└── scripts/
|
|
50
55
|
└── sdlc_check.py
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/agentic-sdlc-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"skills/agentic-sdlc-skill/SKILL.md",
|
|
29
29
|
"skills/agentic-sdlc-skill/templates.md",
|
|
30
30
|
"skills/agentic-sdlc-skill/guides.md",
|
|
31
|
+
"skills/agentic-sdlc-skill/tdd.md",
|
|
32
|
+
"skills/agentic-sdlc-skill/debugging.md",
|
|
33
|
+
"skills/agentic-sdlc-skill/elicitation.md",
|
|
34
|
+
"skills/agentic-sdlc-skill/review.md",
|
|
35
|
+
"skills/agentic-sdlc-skill/dispatch.md",
|
|
31
36
|
"skills/agentic-sdlc-skill/ENFORCEMENT.md",
|
|
32
37
|
"skills/agentic-sdlc-skill/scripts/sdlc_check.py",
|
|
33
38
|
"gemini-extension.json",
|
|
@@ -176,6 +176,8 @@ Hybrid:
|
|
|
176
176
|
|
|
177
177
|
### 3. Request Analysis
|
|
178
178
|
|
|
179
|
+
For any L3, run the spec elicitation round in `elicitation.md` BEFORE drafting the analysis (skip path inside — one-line note when the spec is already complete).
|
|
180
|
+
|
|
179
181
|
Standalone L3:
|
|
180
182
|
- Before creating a new `ANALYSIS_[feature].md`, search `ai_docs/solutions/` with glob/grep for an existing analysis on the same topic: if there is one, update it instead of duplicating it.
|
|
181
183
|
- Create or update `ai_docs/solutions/ANALYSIS_[feature].md`.
|
|
@@ -191,14 +193,17 @@ Hybrid L3:
|
|
|
191
193
|
|
|
192
194
|
- Implement only after the documentation gate required by the level.
|
|
193
195
|
- Modify surgically, consistently with the plan.
|
|
194
|
-
-
|
|
196
|
+
- Implementation work follows the TDD discipline in `tdd.md` (RED/GREEN/REFACTOR — the L2/L3 default; record the reason when it does not apply).
|
|
195
197
|
- If the environment does not allow automated tests, declare the alternative verification and the reason.
|
|
196
|
-
-
|
|
198
|
+
- For bugs (L2/L3), follow the systematic debugging method in `debugging.md`.
|
|
199
|
+
- Circuit breaker: after 3 consecutive runs without progress on the tests, stop, switch to the systematic method in `debugging.md`, and ask for instructions if still stuck.
|
|
197
200
|
- Update the ANALYSIS Diary or the Action Plan when you complete milestones, hit blockers or change decisions.
|
|
201
|
+
- **Opt-in subagent execution**: for an L3 with an approved design, the orchestrator MAY execute the work via subagents per `dispatch.md`, gated by `sdlc_check.py plan validate` ("no valid plan, no dispatch"); default stays same-session. Hybrid: the executable `PLAN_[feature].md` is `derived-from` the accepted E-TDD, never independently authored.
|
|
198
202
|
|
|
199
203
|
### 5. Closure
|
|
200
204
|
|
|
201
205
|
- Run the relevant tests/lint/smoke checks.
|
|
206
|
+
- For the review itself follow `review.md` (requesting and receiving findings) — the single definition, intended for reuse by the Hybrid review gates (devPNT-side wiring out of this unit's scope).
|
|
202
207
|
- Verify alignment with the local Vision or the devPNT M-VISION.
|
|
203
208
|
- Update only the documents actually impacted.
|
|
204
209
|
- **Aligned indexes (Poka-Yoke)**: if you created, moved or removed canonical documents (`vision/`, `reference/`, `architecture/`, `functional/`, `strategic/`):
|
|
@@ -253,6 +258,8 @@ forbidden. Full pipeline, DRY rule, snapshotting and maintenance: `guides.md`.
|
|
|
253
258
|
`ai_docs/reference/INDEX.md` is generated (the guide router) — never edit by hand,
|
|
254
259
|
regenerate with `sdlc_check.py index`.
|
|
255
260
|
|
|
261
|
+
**Agent-global KB.** A second, cross-project guide root lives at the fixed path `~/.agentic-sdlc/` (same `ai_docs/` structure, same validator/router/freshness engine via `sdlc_check.py --root ~/.agentic-sdlc`). Project guides win on topic collision; a project guide that overrides a KB guide MUST declare `overrides: GUIDE_<topic>.md` — the validator warns on undeclared collisions (error under `--strict`) and fail-closes on an `overrides:` value that escapes the KB. Discovery is this paragraph: agents and subagents reach KB guides by path, exactly like project guides.
|
|
262
|
+
|
|
256
263
|
## Mechanical Enforcement
|
|
257
264
|
|
|
258
265
|
The prompt is not enforcement. When the project needs repeatable guarantees:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Systematic Debugging
|
|
2
|
+
|
|
3
|
+
Applies to bugs classified L2 or L3. Entered either from phase 4
|
|
4
|
+
(Development and Testing) when a defect surfaces during implementation, or
|
|
5
|
+
from the circuit breaker in `SKILL.md` §4 after repeated no-progress runs.
|
|
6
|
+
|
|
7
|
+
## Method
|
|
8
|
+
|
|
9
|
+
Work the steps in order; do not skip ahead to a fix before step 3 names the
|
|
10
|
+
mechanism.
|
|
11
|
+
|
|
12
|
+
1. **Reproduce deterministically.** Find the smallest input or scenario that
|
|
13
|
+
triggers the bug every time. A bug you cannot reliably reproduce cannot be
|
|
14
|
+
verified as fixed.
|
|
15
|
+
2. **Isolate.** Narrow the failing case to its minimal form — strip
|
|
16
|
+
unrelated setup, unrelated data, unrelated code paths. Bisect (recent
|
|
17
|
+
commits, code paths, input ranges) when the boundary is unclear.
|
|
18
|
+
3. **Root cause — name the mechanism.** State in one sentence why the
|
|
19
|
+
observed behavior happens, tracing the actual execution path. **MUST NOT
|
|
20
|
+
patch a symptom without naming the mechanism; why: symptom patches recur
|
|
21
|
+
and stack** — the same underlying defect resurfaces elsewhere, and each
|
|
22
|
+
unexplained patch makes the next investigation harder, not easier.
|
|
23
|
+
4. **Fix at the cause**, not at the symptom identified in step 1.
|
|
24
|
+
5. **Write a regression test that FAILS on the old code** (verify this
|
|
25
|
+
before applying the fix, or by temporarily reverting it) and passes after.
|
|
26
|
+
A regression test that was never confirmed to fail proves nothing.
|
|
27
|
+
6. **Run the relevant suite for collateral.** A root-cause fix can change
|
|
28
|
+
behavior other callers depend on; check them before closing.
|
|
29
|
+
|
|
30
|
+
## Circuit breaker integration
|
|
31
|
+
|
|
32
|
+
After 3 consecutive runs without progress: STOP. Do not attempt a 4th
|
|
33
|
+
variation of the same guess. Audit your assumptions from step 1 — is the
|
|
34
|
+
reproduction actually deterministic, is the isolated case actually minimal,
|
|
35
|
+
is the "root cause" from step 3 actually confirmed or just plausible? Then
|
|
36
|
+
restart from step 1 with the corrected assumption.
|
|
37
|
+
|
|
38
|
+
If still stuck after the restart, ask the user for instructions, and hand
|
|
39
|
+
over the evidence gathered so far: the minimal reproduction, what was ruled
|
|
40
|
+
out, and the current best hypothesis.
|
|
41
|
+
|
|
42
|
+
## Anti-patterns
|
|
43
|
+
|
|
44
|
+
- **Shotgun debugging**: changing several things at once hoping one fixes it
|
|
45
|
+
— when it "works," you do not know which change mattered or why.
|
|
46
|
+
- **Stacking speculative fixes**: adding a second guess on top of an
|
|
47
|
+
unverified first one instead of confirming or discarding it first.
|
|
48
|
+
- **"Fixed but can't say why"**: closing the bug without a step-3 mechanism
|
|
49
|
+
statement. If you cannot name the mechanism, the fix is not confirmed —
|
|
50
|
+
it may just be masking the trigger.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Subagent Execution Discipline
|
|
2
|
+
|
|
3
|
+
Opt-in orchestration for L3 work with an approved design: the orchestrator
|
|
4
|
+
drives a `PLAN_[feature].md` through subagents instead of implementing every
|
|
5
|
+
task in the same session. Default stays same-session; this is an escalation,
|
|
6
|
+
never a requirement.
|
|
7
|
+
|
|
8
|
+
## Trigger
|
|
9
|
+
|
|
10
|
+
Only for L3 (an approved E-TDD in Hybrid, or an ANALYSIS Action Plan in
|
|
11
|
+
Standalone). Never for L1/L2 — the plan/ledger machinery is overhead a small
|
|
12
|
+
change does not need. The plan is always `derived-from` the accepted design:
|
|
13
|
+
it is never independently authored, exactly like an E-TDD is never authored
|
|
14
|
+
without an E-ISP.
|
|
15
|
+
|
|
16
|
+
## The loop
|
|
17
|
+
|
|
18
|
+
1. `sdlc_check.py plan validate PLAN_[feature].md` — zero-execution schema +
|
|
19
|
+
confinement + ledger cross-check. Non-zero exit = **no dispatch**. This is
|
|
20
|
+
the hard gate: "no valid plan, no dispatch."
|
|
21
|
+
2. For each task, in plan order:
|
|
22
|
+
- Read the task's status from the sidecar ledger
|
|
23
|
+
(`PLAN_[feature].ledger.json`). `status: done` (exact sentinel) → skip,
|
|
24
|
+
never re-dispatch. Anything else (pending, failed, missing, or a
|
|
25
|
+
corrupt-but-parseable entry) → treat as pending and dispatch.
|
|
26
|
+
- `sdlc_check.py plan brief PLAN_[feature].md --task <id>` — prints the
|
|
27
|
+
task block, the `produces` of prior-order tasks (interfaces), and
|
|
28
|
+
`guides` pointers (paths, never pasted content) to stdout.
|
|
29
|
+
- Spawn the subagent with that brief as its entire context window.
|
|
30
|
+
- Run `task.verify` out of band (the orchestrator executes it — the
|
|
31
|
+
validator only ever prints it, never runs it) plus the one-shot review
|
|
32
|
+
below.
|
|
33
|
+
- Write `{status, verify_result, timestamp}` back to the ledger. The
|
|
34
|
+
validator never writes the ledger — single-writer, orchestrator-owned.
|
|
35
|
+
|
|
36
|
+
## Model tiers (client-relative, no provider names)
|
|
37
|
+
|
|
38
|
+
- Default dispatch: **economy** implementer tier.
|
|
39
|
+
- After **two** consecutive `verify_result: fail` on the same task: escalate
|
|
40
|
+
to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
|
|
41
|
+
first failure — a single fail is often a brief or environment issue, not a
|
|
42
|
+
capability gap.
|
|
43
|
+
|
|
44
|
+
## Review slots — one-shot, not iterative
|
|
45
|
+
|
|
46
|
+
Exactly three review touches per task, never a loop:
|
|
47
|
+
|
|
48
|
+
1. Inline self-review by the implementer subagent before it reports done
|
|
49
|
+
(the standard critical-review pass, not a separate call).
|
|
50
|
+
2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
|
|
51
|
+
Standalone: the `review.md` discipline).
|
|
52
|
+
3. One broad final pass over the whole plan at closure, after all tasks are
|
|
53
|
+
DONE — catches cross-task drift a per-task review cannot see.
|
|
54
|
+
|
|
55
|
+
If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
|
|
56
|
+
via the ledger's fail path, not an extra review slot.
|
|
57
|
+
|
|
58
|
+
## Ledger protocol summary
|
|
59
|
+
|
|
60
|
+
Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
|
|
61
|
+
memory the loop needs across sessions or context compaction: a resumed
|
|
62
|
+
orchestrator re-reads it and picks up exactly where it left off, never
|
|
63
|
+
re-running a DONE task.
|
|
64
|
+
|
|
65
|
+
## Degradation
|
|
66
|
+
|
|
67
|
+
No subagent-spawning tool available → the orchestrator runs each task in the
|
|
68
|
+
same session, against the same plan and ledger, with the same one-shot review
|
|
69
|
+
slots. No capability is lost, only the parallelism/isolation subagents would
|
|
70
|
+
have added.
|
|
71
|
+
|
|
72
|
+
## Hybrid note
|
|
73
|
+
|
|
74
|
+
The plan's `derived-from` points at the accepted E-TDD document key. Per-task
|
|
75
|
+
review reuses the devPNT independent reviewers (§4.6 code review gate) rather
|
|
76
|
+
than restating review doctrine — see `review.md` for the single definition
|
|
77
|
+
both modes share.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Spec Elicitation
|
|
2
|
+
|
|
3
|
+
Applies when an L3 request enters phase 3 (Request Analysis), BEFORE
|
|
4
|
+
drafting the ANALYSIS document (Standalone) or the D-UC/E-ISP (Hybrid).
|
|
5
|
+
|
|
6
|
+
Skip path: if the spec is already complete — an approved Vision or explicit
|
|
7
|
+
user requirements already answer goal, scope, and constraints — skip the
|
|
8
|
+
round and add a one-line note in the analysis stating why it was skipped.
|
|
9
|
+
Do not run the round as a formality when the answers are already on record.
|
|
10
|
+
|
|
11
|
+
## The round
|
|
12
|
+
|
|
13
|
+
Ask ONE structured set of questions, not a drip of follow-ups. Keep each
|
|
14
|
+
question short and numbered; offer concrete options where a real choice
|
|
15
|
+
exists (this narrows the reply and speeds up the round). Cover:
|
|
16
|
+
|
|
17
|
+
1. **Goal / benefit** — what problem this closes, for whom, and why now.
|
|
18
|
+
2. **Scope boundaries** — what is explicitly included in this unit of work.
|
|
19
|
+
3. **Non-goals** — what is explicitly excluded, so scope does not silently
|
|
20
|
+
creep in later.
|
|
21
|
+
4. **Constraints** — technical, compatibility, and security constraints that
|
|
22
|
+
bound the solution space.
|
|
23
|
+
5. **Acceptance signals** — how you and the user will both recognize the
|
|
24
|
+
work is done and correct.
|
|
25
|
+
|
|
26
|
+
## Reflect
|
|
27
|
+
|
|
28
|
+
Fold the answers into the ANALYSIS `## Objective` / `## Vision-Alignment`
|
|
29
|
+
sections (Standalone) or into the D-UC/E-ISP (Hybrid) — do not leave them
|
|
30
|
+
sitting only in the chat transcript. The written document, not the
|
|
31
|
+
conversation, is what the next session and the next reviewer will read.
|
|
32
|
+
|
|
33
|
+
Run a second round only when an answer opens a real fork in the design (a
|
|
34
|
+
genuinely new question the first round could not have anticipated). Do not
|
|
35
|
+
run a second round to double-check answers that were already clear.
|
|
36
|
+
|
|
37
|
+
## Anti-patterns
|
|
38
|
+
|
|
39
|
+
- **Interrogation**: an endless list of questions, or drip-feeding one
|
|
40
|
+
question at a time across many turns instead of one structured round.
|
|
41
|
+
- **Asking what the approved vision already answers**: re-asking goal or
|
|
42
|
+
non-goals that a `Status: APPROVED` Vision or M-VISION already states.
|
|
43
|
+
- **Collecting answers without folding them in**: getting replies in chat
|
|
44
|
+
and proceeding to design without writing them into the analysis document —
|
|
45
|
+
the next reader has no record of why the scope is what it is.
|
|
@@ -39,8 +39,11 @@ guide with no `distilled_from` is not this pipeline's output.
|
|
|
39
39
|
0. **Search before creating (DRY — one CURRENT guide per topic).** Before
|
|
40
40
|
proposing anything, read `ai_docs/reference/INDEX.md` and grep
|
|
41
41
|
`reference/GUIDE_*.md` for topic overlap with the new material. Never end
|
|
42
|
-
up with two CURRENT guides on the same topic.
|
|
43
|
-
|
|
42
|
+
up with two CURRENT guides on the same topic. Search BOTH routers: project
|
|
43
|
+
`ai_docs/reference/INDEX.md` AND the agent KB router
|
|
44
|
+
`~/.agentic-sdlc/ai_docs/reference/INDEX.md` (if present) — one CURRENT
|
|
45
|
+
guide per topic PER SCOPE; a project guide on a KB topic requires the
|
|
46
|
+
explicit `overrides:` declaration. On overlap, pick by provenance:
|
|
44
47
|
- **Same source, evolved** → UPDATE the existing guide in place: new
|
|
45
48
|
snapshot, new `source_hash`, same file (history lives in git).
|
|
46
49
|
- **Different source replacing the old one** → NEW guide + mark the old
|
|
@@ -75,8 +78,13 @@ guide with no `distilled_from` is not this pipeline's output.
|
|
|
75
78
|
- Heterogeneous sources (unrelated policies handed over together) are the
|
|
76
79
|
legitimate split case; a single coherent document about one subsystem
|
|
77
80
|
almost never is.
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
- **Also decide SCOPE per proposed guide**: project-scope
|
|
82
|
+
(`ai_docs/reference/`) or agent-scope (`~/.agentic-sdlc/ai_docs/reference/`,
|
|
83
|
+
governs the agent across ALL projects; origin+purpose test unchanged,
|
|
84
|
+
scope is a LOCATION decision by the user, never a content taxonomy; KB
|
|
85
|
+
created lazily with `.sources/` on the first agent-scope guide).
|
|
86
|
+
2. **User confirms** the topic decomposition — including the fragmentation-risk
|
|
87
|
+
assessment and scope decision — before any file is written.
|
|
80
88
|
3. **Snapshot each source verbatim** into
|
|
81
89
|
`ai_docs/reference/.sources/<slug>-<hash8>.md`:
|
|
82
90
|
- `slug` derives from the topic (lowercase, hyphenated).
|
|
@@ -158,3 +166,5 @@ state it explicitly when handing off a newly created guide.
|
|
|
158
166
|
compares each guide's recorded `source_hash` against the live snapshot file
|
|
159
167
|
and reports `[stale]` when they diverge — that is the signal to regenerate,
|
|
160
168
|
not a manual freshness check.
|
|
169
|
+
- **Agent-global KB guides** use the same pipeline and validator via
|
|
170
|
+
`--root ~/.agentic-sdlc`; freshness via the same `stale` engine.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Code Review Discipline
|
|
2
|
+
|
|
3
|
+
Applies at closure of L2/L3 work, and to any independent review slot in this
|
|
4
|
+
skill or in a connected governance layer — devPNT's §4.5/§4.6 review gates,
|
|
5
|
+
and any future review step added to the workflow. This is the single
|
|
6
|
+
definition of how to request, receive, and perform a review; other places
|
|
7
|
+
that need review behavior point here instead of restating it (DRY).
|
|
8
|
+
|
|
9
|
+
## Requesting
|
|
10
|
+
|
|
11
|
+
When you hand work to a reviewer (human or agent), give them:
|
|
12
|
+
|
|
13
|
+
- **Scope**: what changed and why, in one or two lines.
|
|
14
|
+
- **The authoritative design artifact**: the ANALYSIS, E-TDD, or equivalent
|
|
15
|
+
the change was built against — not a paraphrase of it.
|
|
16
|
+
- **The actual diff**: the real changed files, not a description of them.
|
|
17
|
+
|
|
18
|
+
Never ask a reviewer to "review my session" or "review what I just did"
|
|
19
|
+
without the artifacts above — that forces them to reconstruct scope from
|
|
20
|
+
conversation instead of reviewing the change itself. Say which finding
|
|
21
|
+
classes you want covered (correctness, security, conformance to the design,
|
|
22
|
+
test coverage) if the default scope is not obvious.
|
|
23
|
+
|
|
24
|
+
## Receiving
|
|
25
|
+
|
|
26
|
+
**MUST answer findings one by one — fix, or justify with evidence; why:
|
|
27
|
+
silent drops turn review into theater** — a review whose findings are not
|
|
28
|
+
tracked to a resolution gives the appearance of quality control without its
|
|
29
|
+
substance.
|
|
30
|
+
|
|
31
|
+
If you disagree with a finding, say so explicitly with your reasoning; never
|
|
32
|
+
resolve a disagreement by rewording the finding until it goes away. When the
|
|
33
|
+
project keeps a `REVIEW_LOG` (or equivalent), log the outcome of each
|
|
34
|
+
finding there.
|
|
35
|
+
|
|
36
|
+
## Reviewing
|
|
37
|
+
|
|
38
|
+
When you are the reviewer:
|
|
39
|
+
|
|
40
|
+
- Verify claims against the real source, not against the diff's own
|
|
41
|
+
description of itself.
|
|
42
|
+
- Cite evidence as `file:line` for every finding — a finding without a
|
|
43
|
+
location is not actionable.
|
|
44
|
+
- Keep severity honest: do not inflate a style preference to a blocker, and
|
|
45
|
+
do not soften a real correctness or security issue to a nit.
|
|
46
|
+
- No praise padding. A review reports problems and their fixes, not a
|
|
47
|
+
summary of what looks fine.
|
|
48
|
+
|
|
49
|
+
## Anti-patterns
|
|
50
|
+
|
|
51
|
+
- **Batch-dismissal**: closing out a whole findings list with one blanket
|
|
52
|
+
reply instead of addressing each finding individually.
|
|
53
|
+
- **Rewording instead of addressing**: editing the finding's text to look
|
|
54
|
+
resolved without changing the code or providing evidence it is a
|
|
55
|
+
non-issue.
|
|
56
|
+
- **Scope-creep findings**: raising issues unrelated to the change under
|
|
57
|
+
review instead of filing them separately.
|
|
@@ -55,6 +55,13 @@ GUIDE_INDEX_HEADER = ("<!-- GENERATED by sdlc_check.py index - do not edit by ha
|
|
|
55
55
|
GUIDE_PROVENANCE_KEYS = ("source", "distilled_from", "source_hash") # source_version optional
|
|
56
56
|
# a guide section is "covered" when it carries a source marker or an explicit gap marker
|
|
57
57
|
GUIDE_MARKER_RE = re.compile(r"\[(?:source:[^\]]+|not covered by source)\]")
|
|
58
|
+
# Agent-global KB (Feature B unit 2): ONE client-agnostic root under home.
|
|
59
|
+
# AGENTIC_SDLC_KB_ROOT env var is a TEST/CI seam only (scenario battery must
|
|
60
|
+
# not touch the real user KB); the documented product path is fixed.
|
|
61
|
+
DEFAULT_KB_ROOT = Path(os.environ.get("AGENTIC_SDLC_KB_ROOT", "")) if os.environ.get("AGENTIC_SDLC_KB_ROOT") else Path.home() / ".agentic-sdlc"
|
|
62
|
+
# Subagent Execution (Feature A): a PLAN_[feature].md task must carry these keys,
|
|
63
|
+
# plus at least one of paths/produces (checked separately in cmd_plan).
|
|
64
|
+
PLAN_TASK_REQUIRED = ("id", "title", "verify")
|
|
58
65
|
|
|
59
66
|
# Deprecated Italian frontmatter keys, mapped to the canonical English ones.
|
|
60
67
|
LEGACY_KEYS = {"stato": "status", "livello": "level",
|
|
@@ -102,6 +109,25 @@ def require_ai_docs(root, command):
|
|
|
102
109
|
return True
|
|
103
110
|
|
|
104
111
|
|
|
112
|
+
def confine_under(base, rel):
|
|
113
|
+
"""Fail-closed path confinement: resolve `rel` under `base` and require the
|
|
114
|
+
result to stay inside `base`. Returns None (reject) if `rel` is absolute,
|
|
115
|
+
contains a '..' part, or resolves outside `base` (including an OSError
|
|
116
|
+
during resolution, e.g. an unresolvable/reparse-point path on Windows).
|
|
117
|
+
Single source for path confinement (T2/T3): reused by check_kb_collisions'
|
|
118
|
+
`overrides:` check and cmd_validate's `distilled_from` check, and by the
|
|
119
|
+
new `plan` command's paths/consumes/produces/guides confinement."""
|
|
120
|
+
p = Path(rel)
|
|
121
|
+
if p.is_absolute() or ".." in p.parts:
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
t = (base / rel).resolve()
|
|
125
|
+
t.relative_to(base.resolve())
|
|
126
|
+
return t
|
|
127
|
+
except (ValueError, OSError):
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
105
131
|
def read_text(path):
|
|
106
132
|
# utf-8-sig: strips a leading BOM (files authored on Windows) so the
|
|
107
133
|
# frontmatter '---' on line 0 stays recognizable; reads plain utf-8 otherwise.
|
|
@@ -387,6 +413,33 @@ def list_guides(root):
|
|
|
387
413
|
return out
|
|
388
414
|
|
|
389
415
|
|
|
416
|
+
def check_kb_collisions(root, project_guides, errors, warnings):
|
|
417
|
+
"""Cross-root awareness (unit 2): project-wins precedence, declared via 'overrides:'."""
|
|
418
|
+
kb_root = DEFAULT_KB_ROOT
|
|
419
|
+
kb_ref = (kb_root / "ai_docs" / "reference")
|
|
420
|
+
try:
|
|
421
|
+
if root.resolve() == kb_root.resolve():
|
|
422
|
+
return # validating the KB itself: no self-comparison
|
|
423
|
+
except OSError:
|
|
424
|
+
return
|
|
425
|
+
if not kb_ref.is_dir():
|
|
426
|
+
return # no KB on this machine: zero behavior change
|
|
427
|
+
kb_names = {p.name for _, p, _, _ in list_guides(kb_root)}
|
|
428
|
+
for rel, p, meta, _ in project_guides:
|
|
429
|
+
ov = (meta.get("overrides") or "").strip()
|
|
430
|
+
if ov:
|
|
431
|
+
# T6: untrusted cross-root pointer — distilled_from parity, fail closed
|
|
432
|
+
target = confine_under(kb_ref, ov)
|
|
433
|
+
if target is None:
|
|
434
|
+
errors.append(f"{rel}: overrides '{ov}' is absolute, contains '..', or escapes the KB "
|
|
435
|
+
"reference dir — rejected (fail closed)")
|
|
436
|
+
continue
|
|
437
|
+
if not target.is_file():
|
|
438
|
+
warnings.append(f"{rel}: overrides target '{ov}' not found in KB ({kb_ref})")
|
|
439
|
+
if p.name in kb_names and ov != p.name:
|
|
440
|
+
warnings.append(f"{rel}: undeclared collision with KB guide '{p.name}' (project wins) — declare overrides: {p.name}")
|
|
441
|
+
|
|
442
|
+
|
|
390
443
|
def build_guide_index(root):
|
|
391
444
|
lines = [GUIDE_INDEX_HEADER,
|
|
392
445
|
"# Operative guides (generated router)",
|
|
@@ -536,15 +589,10 @@ def cmd_validate(root, strict=False):
|
|
|
536
589
|
+ "; ".join(unmarked[:5]))
|
|
537
590
|
# (c) distilled_from confinement — fail closed (P-TM T6, distilled_from vector)
|
|
538
591
|
df = meta.get("distilled_from", "")
|
|
539
|
-
if df:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
target = (root / df).resolve()
|
|
544
|
-
try:
|
|
545
|
-
target.relative_to(root.resolve())
|
|
546
|
-
except ValueError:
|
|
547
|
-
errors.append(f"{rel}: distilled_from '{df}' resolves outside the project root: rejected")
|
|
592
|
+
if df and confine_under(root, df) is None:
|
|
593
|
+
errors.append(f"{rel}: distilled_from '{df}' is absolute, contains '..', or resolves "
|
|
594
|
+
"outside the project root: rejected")
|
|
595
|
+
check_kb_collisions(root, guides, errors, warnings)
|
|
548
596
|
# guide-router alignment (mirror of the root-manifest check)
|
|
549
597
|
gidx = root / "ai_docs" / "reference" / "INDEX.md"
|
|
550
598
|
if guides:
|
|
@@ -769,6 +817,179 @@ def cmd_gate(args):
|
|
|
769
817
|
return 2
|
|
770
818
|
|
|
771
819
|
|
|
820
|
+
# --------------------------------------------------------------------- plan
|
|
821
|
+
# Subagent Execution (Feature A). Zero-execution surface: this section and
|
|
822
|
+
# everything it calls MUST NOT spawn a process (no subprocess/os.system/eval/
|
|
823
|
+
# exec, no git_* helper). It validates a PLAN_[feature].md and prints a task
|
|
824
|
+
# brief as text; the orchestrator (dispatch.md) is the sole executor.
|
|
825
|
+
|
|
826
|
+
_PLAN_JSON_RE = re.compile(r"```json\s*\n(.*?)```", re.DOTALL)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def extract_plan_json(text):
|
|
830
|
+
"""Extract the first fenced ```json block from a PLAN_[feature].md body.
|
|
831
|
+
Returns (data, "") on success, or (None, reason) on any failure. Never
|
|
832
|
+
raises: a malformed or missing block is a validation failure, not a crash."""
|
|
833
|
+
m = _PLAN_JSON_RE.search(text or "")
|
|
834
|
+
if not m:
|
|
835
|
+
return None, "no fenced ```json block found in the plan file"
|
|
836
|
+
try:
|
|
837
|
+
data = json.loads(m.group(1))
|
|
838
|
+
except (ValueError, TypeError) as e:
|
|
839
|
+
return None, f"malformed JSON in the plan block: {e}"
|
|
840
|
+
if not isinstance(data, dict):
|
|
841
|
+
return None, "plan JSON block must be a JSON object"
|
|
842
|
+
return data, ""
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def load_ledger(path):
|
|
846
|
+
"""Read the sidecar ledger {"<task_id>": {"status", "verify_result",
|
|
847
|
+
"timestamp"}}. Absent file -> ({}, ""). Malformed/unreadable -> ({}, reason).
|
|
848
|
+
Never raises, never hangs: the ledger is untrusted state read on every call."""
|
|
849
|
+
if not path.is_file():
|
|
850
|
+
return {}, ""
|
|
851
|
+
try:
|
|
852
|
+
raw = read_text(path)
|
|
853
|
+
data = json.loads(raw)
|
|
854
|
+
except (ValueError, TypeError, OSError) as e:
|
|
855
|
+
return {}, f"ledger '{path}' unreadable/malformed, treating as empty: {e}"
|
|
856
|
+
if not isinstance(data, dict):
|
|
857
|
+
return {}, f"ledger '{path}' is not a JSON object, treating as empty"
|
|
858
|
+
return data, ""
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _confine_or_reject(base, rel, label, rel_label, errors):
|
|
862
|
+
t = confine_under(base, rel)
|
|
863
|
+
if t is None:
|
|
864
|
+
errors.append(f"{rel_label}: {label} '{rel}' is absolute, contains '..', or escapes "
|
|
865
|
+
f"'{base}' — rejected (fail closed)")
|
|
866
|
+
return t
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _validate_plan_tasks(root, data, rel_label, errors, warnings):
|
|
870
|
+
"""Shared core of `plan validate`/`plan brief`: schema + confinement checks.
|
|
871
|
+
Returns the task list (possibly empty) on success; errors/warnings are
|
|
872
|
+
appended in place. Callers decide the exit code."""
|
|
873
|
+
tasks = data.get("tasks")
|
|
874
|
+
if not isinstance(tasks, list) or not tasks:
|
|
875
|
+
errors.append(f"{rel_label}: 'tasks' must be a non-empty JSON array")
|
|
876
|
+
return []
|
|
877
|
+
ref_dir = root / "ai_docs" / "reference"
|
|
878
|
+
kb_ref = DEFAULT_KB_ROOT / "ai_docs" / "reference"
|
|
879
|
+
seen_ids = set()
|
|
880
|
+
for i, task in enumerate(tasks):
|
|
881
|
+
loc = f"{rel_label}: task[{i}]"
|
|
882
|
+
if not isinstance(task, dict):
|
|
883
|
+
errors.append(f"{loc}: not a JSON object")
|
|
884
|
+
continue
|
|
885
|
+
missing = [k for k in PLAN_TASK_REQUIRED if not task.get(k)]
|
|
886
|
+
if missing:
|
|
887
|
+
errors.append(f"{loc}: missing required field(s): {', '.join(missing)}")
|
|
888
|
+
if not task.get("paths") and not task.get("produces"):
|
|
889
|
+
errors.append(f"{loc}: must declare at least one of 'paths'/'produces'")
|
|
890
|
+
tid = task.get("id")
|
|
891
|
+
if tid:
|
|
892
|
+
if tid in seen_ids:
|
|
893
|
+
errors.append(f"{loc}: duplicate task id '{tid}'")
|
|
894
|
+
seen_ids.add(tid)
|
|
895
|
+
for key in ("paths", "consumes", "produces"):
|
|
896
|
+
for p in (task.get(key) or []):
|
|
897
|
+
_confine_or_reject(root, p, key, loc, errors)
|
|
898
|
+
for g in (task.get("guides") or []):
|
|
899
|
+
in_project = confine_under(ref_dir, g)
|
|
900
|
+
in_kb = confine_under(kb_ref, g)
|
|
901
|
+
if in_project is None and in_kb is None:
|
|
902
|
+
errors.append(f"{loc}: guide '{g}' is not confined under the project reference "
|
|
903
|
+
f"dir ({ref_dir}) or the agent KB reference dir ({kb_ref}) — rejected")
|
|
904
|
+
return tasks
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def cmd_plan(root, args):
|
|
908
|
+
"""Zero-execution: validates/briefs a PLAN_[feature].md. Never spawns a
|
|
909
|
+
process, never calls a git_* helper, never runs the opaque `verify` text —
|
|
910
|
+
it is printed, not executed."""
|
|
911
|
+
plan_path = Path(args.file)
|
|
912
|
+
if not plan_path.is_absolute():
|
|
913
|
+
plan_path = root / plan_path
|
|
914
|
+
if not plan_path.is_file():
|
|
915
|
+
sys.stderr.write(f"[plan] plan file not found: {plan_path}\n")
|
|
916
|
+
return 2
|
|
917
|
+
rel_label = str(plan_path)
|
|
918
|
+
data, reason = extract_plan_json(read_text(plan_path))
|
|
919
|
+
if data is None:
|
|
920
|
+
sys.stderr.write(f"[plan] {rel_label}: {reason}\n")
|
|
921
|
+
return 2
|
|
922
|
+
|
|
923
|
+
errors, warnings = [], []
|
|
924
|
+
tasks = _validate_plan_tasks(root, data, rel_label, errors, warnings)
|
|
925
|
+
|
|
926
|
+
ledger_path = plan_path.with_name(plan_path.stem + ".ledger.json")
|
|
927
|
+
ledger, ledger_reason = load_ledger(ledger_path)
|
|
928
|
+
if ledger_reason:
|
|
929
|
+
warnings.append(ledger_reason)
|
|
930
|
+
if not errors:
|
|
931
|
+
task_ids = {t.get("id") for t in tasks if isinstance(t, dict)}
|
|
932
|
+
for lid in ledger:
|
|
933
|
+
if lid not in task_ids:
|
|
934
|
+
warnings.append(f"ledger id '{lid}' not found in {rel_label}: orphaned entry (not fatal)")
|
|
935
|
+
|
|
936
|
+
for w in warnings:
|
|
937
|
+
sys.stderr.write(f"[warn] {w}\n")
|
|
938
|
+
for e in errors:
|
|
939
|
+
sys.stderr.write(f"[ERROR] {e}\n")
|
|
940
|
+
|
|
941
|
+
if args.plan_cmd == "validate":
|
|
942
|
+
if errors:
|
|
943
|
+
sys.stderr.write(f"\n[plan] validate: {len(errors)} errors, {len(warnings)} warnings.\n")
|
|
944
|
+
return 2
|
|
945
|
+
print(f"[ok] {rel_label}: plan valid ({len(tasks)} task(s), {len(warnings)} warning(s)).")
|
|
946
|
+
return 0
|
|
947
|
+
|
|
948
|
+
# brief
|
|
949
|
+
if errors:
|
|
950
|
+
sys.stderr.write(f"\n[plan] brief: plan is invalid, refusing to brief ({len(errors)} errors).\n")
|
|
951
|
+
return 2
|
|
952
|
+
target = None
|
|
953
|
+
for t in tasks:
|
|
954
|
+
if isinstance(t, dict) and t.get("id") == args.task:
|
|
955
|
+
target = t
|
|
956
|
+
break
|
|
957
|
+
if target is None:
|
|
958
|
+
sys.stderr.write(f"[plan] brief: task id '{args.task}' not found in {rel_label}\n")
|
|
959
|
+
return 2
|
|
960
|
+
|
|
961
|
+
print(f"# Task: {target.get('id')} — {target.get('title', '')}")
|
|
962
|
+
print()
|
|
963
|
+
print("## Task block")
|
|
964
|
+
print(json.dumps(target, indent=2))
|
|
965
|
+
print()
|
|
966
|
+
print("## Produces of prior-order tasks (interfaces)")
|
|
967
|
+
prior_produces = []
|
|
968
|
+
for t in tasks:
|
|
969
|
+
if not isinstance(t, dict):
|
|
970
|
+
continue
|
|
971
|
+
if t.get("id") == target.get("id"):
|
|
972
|
+
break
|
|
973
|
+
prior_produces.extend(t.get("produces") or [])
|
|
974
|
+
if prior_produces:
|
|
975
|
+
for p in prior_produces:
|
|
976
|
+
print(f"- {p}")
|
|
977
|
+
else:
|
|
978
|
+
print("(none)")
|
|
979
|
+
print()
|
|
980
|
+
print("## Guide pointers (paths, not content)")
|
|
981
|
+
guides = target.get("guides") or []
|
|
982
|
+
if guides:
|
|
983
|
+
for g in guides:
|
|
984
|
+
print(f"- {g}")
|
|
985
|
+
else:
|
|
986
|
+
print("(none)")
|
|
987
|
+
print()
|
|
988
|
+
print("## Verify (opaque text — orchestrator runs this out of band, NOT executed here)")
|
|
989
|
+
print(target.get("verify", ""))
|
|
990
|
+
return 0
|
|
991
|
+
|
|
992
|
+
|
|
772
993
|
# --------------------------------------------------------------------- main
|
|
773
994
|
|
|
774
995
|
def main(argv=None):
|
|
@@ -799,6 +1020,15 @@ def main(argv=None):
|
|
|
799
1020
|
gp.add_argument("--file", help="file path to evaluate (alternative to --hook)")
|
|
800
1021
|
gp.add_argument("--protected", default="", help="protected prefixes separated by ';' (e.g. \"src/auth;src/crypto\")")
|
|
801
1022
|
|
|
1023
|
+
pp = sub.add_parser("plan", parents=[common],
|
|
1024
|
+
help="Subagent Execution: validate/brief a PLAN_[feature].md (zero-execution)")
|
|
1025
|
+
pp_sub = pp.add_subparsers(dest="plan_cmd", required=True)
|
|
1026
|
+
pv = pp_sub.add_parser("validate", help="schema + confinement + ledger cross-check (exit 2 on error)")
|
|
1027
|
+
pv.add_argument("file", help="path to the PLAN_[feature].md file")
|
|
1028
|
+
pb = pp_sub.add_parser("brief", help="print a task's brief to stdout (verify text is NOT executed)")
|
|
1029
|
+
pb.add_argument("file", help="path to the PLAN_[feature].md file")
|
|
1030
|
+
pb.add_argument("--task", required=True, help="task id to brief")
|
|
1031
|
+
|
|
802
1032
|
args = ap.parse_args(argv)
|
|
803
1033
|
if args.cmd == "gate":
|
|
804
1034
|
return cmd_gate(args)
|
|
@@ -814,6 +1044,8 @@ def main(argv=None):
|
|
|
814
1044
|
return cmd_stale(root, hybrid=args.hybrid)
|
|
815
1045
|
if args.cmd == "mark":
|
|
816
1046
|
return cmd_mark(root, args.paths)
|
|
1047
|
+
if args.cmd == "plan":
|
|
1048
|
+
return cmd_plan(root, args)
|
|
817
1049
|
return 0
|
|
818
1050
|
|
|
819
1051
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# TDD Discipline
|
|
2
|
+
|
|
3
|
+
Applies to L2/L3 implementation work. L1, doc-only changes, and Spikes are
|
|
4
|
+
exempt (a Spike records its outcome note only, per `SPIKE_[topic].md`).
|
|
5
|
+
|
|
6
|
+
## The loop
|
|
7
|
+
|
|
8
|
+
RED, GREEN, REFACTOR, in that order, every time.
|
|
9
|
+
|
|
10
|
+
- **RED**: write ONE failing test first, run it, and watch it fail. **MUST:
|
|
11
|
+
no implementation code before the failing test exists; why: a test written
|
|
12
|
+
after the code passes vacuously and proves nothing** — it cannot
|
|
13
|
+
distinguish a correct implementation from a broken one, because it was
|
|
14
|
+
shaped to match whatever the code already does.
|
|
15
|
+
- **GREEN**: write the minimum code needed to make that test pass. Resist
|
|
16
|
+
adding behavior the test does not require yet — that belongs to the next
|
|
17
|
+
loop.
|
|
18
|
+
- **REFACTOR**: clean up implementation and test code while the suite stays
|
|
19
|
+
green. Re-run the tests after every refactor step, not just at the end.
|
|
20
|
+
|
|
21
|
+
## Increment rule
|
|
22
|
+
|
|
23
|
+
One behavior per loop. If you notice the failing test actually covers two
|
|
24
|
+
behaviors, split it before writing implementation code — a test that asserts
|
|
25
|
+
two unrelated things fails ambiguously and slows down the next RED step.
|
|
26
|
+
|
|
27
|
+
## Test shape
|
|
28
|
+
|
|
29
|
+
Unit tests follow AAA — arrange, act, assert:
|
|
30
|
+
|
|
31
|
+
- **Arrange**: set up inputs, fixtures, and collaborators.
|
|
32
|
+
- **Act**: invoke the one behavior under test.
|
|
33
|
+
- **Assert**: check the outcome, and only that outcome.
|
|
34
|
+
|
|
35
|
+
Keep the three parts visually separable (blank line or comment) so a reader
|
|
36
|
+
can tell what is setup, what is the trigger, and what is being checked
|
|
37
|
+
without tracing the whole test body. This is the single home of the AAA
|
|
38
|
+
guidance — do not restate it elsewhere in the skill.
|
|
39
|
+
|
|
40
|
+
## When TDD does not apply
|
|
41
|
+
|
|
42
|
+
Legitimate exemptions: no test harness exists for the target environment,
|
|
43
|
+
the change is pure documentation, or the work is a time-boxed Spike.
|
|
44
|
+
|
|
45
|
+
**MUST record the explicit reason in the ANALYSIS Diary or Action Plan node;
|
|
46
|
+
why: an unrecorded exemption is indistinguishable from forgetting** — a
|
|
47
|
+
reviewer (or a future you) cannot tell "skipped on purpose, here is why" from
|
|
48
|
+
"skipped by accident" unless the reason is written down at the time.
|
|
49
|
+
|
|
50
|
+
## Anti-patterns
|
|
51
|
+
|
|
52
|
+
- **Tests-after as the unexplained default**: writing implementation first
|
|
53
|
+
and tests afterward without an entry under "When TDD does not apply" above.
|
|
54
|
+
- **Testing implementation details**: asserting on private state or call
|
|
55
|
+
internals instead of observable behavior — the test breaks on refactors
|
|
56
|
+
that change nothing externally.
|
|
57
|
+
- **One giant test covering everything**: a single test that exercises
|
|
58
|
+
multiple behaviors is slow to diagnose when it fails and violates the
|
|
59
|
+
Increment rule above.
|
|
@@ -40,6 +40,7 @@ source: Human-readable name of what the user provided.
|
|
|
40
40
|
source_version: v1.2 # optional — only when the origin is versioned
|
|
41
41
|
distilled_from: ai_docs/reference/.sources/topic-a1b2c3d4.md
|
|
42
42
|
source_hash: <sha256 of the snapshot file>
|
|
43
|
+
overrides: GUIDE_topic.md # optional — only for a project guide overriding an agent-KB guide
|
|
43
44
|
---
|
|
44
45
|
# Guide: [Topic]
|
|
45
46
|
|
|
@@ -188,6 +189,54 @@ Allowed frontmatter states: `PLANNED` | `IN_PROGRESS` | `COMPLETED` | `CANCELLED
|
|
|
188
189
|
<!-- max 1 page. Spike code is NOT mergeable: for production reclassify L2/L3. -->
|
|
189
190
|
```
|
|
190
191
|
|
|
192
|
+
## ai_docs/solutions/PLAN_[feature].md
|
|
193
|
+
|
|
194
|
+
Opt-in, L3 only (see `dispatch.md`): the executable task list an orchestrator
|
|
195
|
+
drives through subagents. It is `derived-from` the accepted E-TDD (Hybrid) or
|
|
196
|
+
the ANALYSIS Action Plan (Standalone) — never independently authored. The
|
|
197
|
+
validator gate is `sdlc_check.py plan validate PLAN_[feature].md` ("no valid
|
|
198
|
+
plan, no dispatch").
|
|
199
|
+
|
|
200
|
+
````markdown
|
|
201
|
+
---
|
|
202
|
+
status: DRAFT
|
|
203
|
+
derived-from: e_tdd_[feature] vX.Y
|
|
204
|
+
---
|
|
205
|
+
# Plan: [Feature]
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"tasks": [
|
|
210
|
+
{
|
|
211
|
+
"id": "T1",
|
|
212
|
+
"title": "Add the confine_under helper",
|
|
213
|
+
"paths": ["skills/agentic-sdlc-skill/scripts/sdlc_check.py"],
|
|
214
|
+
"consumes": [],
|
|
215
|
+
"produces": ["skills/agentic-sdlc-skill/scripts/sdlc_check.py#confine_under"],
|
|
216
|
+
"verify": "python skills/agentic-sdlc-skill/scripts/test_plan.py",
|
|
217
|
+
"guides": ["GUIDE_python_style.md"]
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
````
|
|
223
|
+
|
|
224
|
+
Task fields: `id`/`title`/`verify` are required; at least one of `paths`/
|
|
225
|
+
`produces` is required. `paths` are files the task touches; `consumes`/
|
|
226
|
+
`produces` declare interfaces between tasks (what an earlier task hands to a
|
|
227
|
+
later one); `guides` are pointers (paths, not pasted content) into
|
|
228
|
+
`ai_docs/reference/` or the agent-global KB. All path-shaped fields are
|
|
229
|
+
confined fail-closed under the project root (or the reference/KB root for
|
|
230
|
+
`guides`) — an absolute path or a `..` escape is rejected. `verify` is opaque
|
|
231
|
+
text: the validator only prints it (`plan brief`), never runs it — the
|
|
232
|
+
orchestrator executes it out of band.
|
|
233
|
+
|
|
234
|
+
Sidecar ledger `ai_docs/solutions/PLAN_[feature].ledger.json` (orchestrator-
|
|
235
|
+
owned, validator-read-only): `{ "<task_id>": {"status": "done", "verify_result":
|
|
236
|
+
"pass", "timestamp": "2026-07-03T00:00:00Z"} }`. Only the exact `status: done`
|
|
237
|
+
sentinel skips re-dispatch; any other value (or a missing `status`) is treated
|
|
238
|
+
as pending. A ledger id absent from the plan is a non-fatal orphan warning.
|
|
239
|
+
|
|
191
240
|
## ai_docs/audit/audit_plan.md (Standalone mode only)
|
|
192
241
|
|
|
193
242
|
The `Reference` field (git hash or ISO UTC timestamp) is managed by `sdlc_check.py mark` — do not fill it by hand. Freshness is verified with `sdlc_check.py stale`.
|