@antoneeo/agentic-sdlc-skill 1.9.0 → 1.11.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 CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  Tutte le modifiche significative a questa skill saranno documentate in questo file.
4
4
 
5
+ ## [1.11.0] - 2026-07-03 (M4: Consolidation & Proactive Activation — self-activating, self-consulting, self-testing skill)
6
+ ### Added
7
+ - **SessionStart orientation hook** (`sdlc_check.py orient`): emits a bounded, repo-sourced orientation (README + INDEX + guide router + handoff + Rule-Zero triage) at session start. Zero-execution, **fail-OPEN** (a missing/empty `ai_docs/` never blocks the session), size-capped. Manual per-client wiring in `ENFORCEMENT.md` §4; `--hybrid` points at the devPNT bootstrap instead of duplicating plan/KL. `test_session_start.py` (9 cases).
8
+ - **Guide-layer consumption** (closes the write-only gap — Layer D "point to them"): a **consult trigger** (before operative L2/L3 work, targeted router match, L1 exempt, never blanket) and a **proactive-creation trigger** (propose a guide after user-indication-governed reusable work; never silent, never from model knowledge). Mechanics in `guides.md` §0/§1; hooked from `SKILL.md` Operative Guides + Phase 4/5; reconciled with subagent dispatch in `dispatch.md`.
9
+ - **Worktree/branch hygiene** in the closure discipline (`SKILL.md` Phase 4 isolate-on-branch / Phase 5 merge-decision + cleanup).
10
+ - **Skill eval harness** (dev-only, not shipped): `test_skill_invariants.py` is the deterministic static release gate (`python -m unittest discover -s scripts -p "test_*.py"` — asserts the skill's own doctrine invariants: triggers/hook/worktree present and wired, indexes idempotent, support pointers resolve; zero LLM/network/subprocess). Opt-in behavioral corpus `evals/scenarios/` + `run_behavioral.py` (non-CI, never gates). `ENFORCEMENT.md` §5.
11
+
12
+ ### Process note
13
+ - Full governance per unit: M-VISION v2.1 (revised — added the guide-consumption unit) → D-UC + P-TM → per-unit E-ISP (deep review) + E-TDD (light review) → implement → §4.6 code review. The independent-review gate caught real defects at design time (incomplete eval invariant set, the M3↔M4 dispatch interaction, a REPO path off-by-one, a P-TM overclaim of unbuilt guards). Battery 51/51 green. ADR `adr_2026-07-03_skill_eval_harness`; KL architecture v1.4 + principles v1.2.
14
+
15
+ ## [1.10.0] - 2026-07-03 (M3: Subagent Execution / Feature A — opt-in executable plan)
16
+ ### Added
17
+ - **`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).
18
+ - **`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.
19
+ - **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).
20
+ - **`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).
21
+ - `test_plan.py`: stdlib-`unittest` battery for the `plan` subcommand (32 cases: schema, confinement, ledger, fail-fast JSON, zero-execution poka-yoke).
22
+
23
+ ### Changed
24
+ - **`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)`).
25
+
26
+ ### Process note
27
+ - 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`.
28
+
5
29
  ## [1.9.0] - 2026-07-03 (M2 execution disciplines + Feature B unit 2 agent KB)
6
30
  ### Added
7
31
  - `tdd.md`: TDD discipline (RED/GREEN/REFACTOR, increment rule, AAA test shape, documented exemptions) — the L2/L3 default for implementation work.
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`, `tdd.md`, `debugging.md`, `elicitation.md`, `review.md`, `ENFORCEMENT.md`, and `scripts/sdlc_check.py`; an agent-global KB at `~/.agentic-sdlc` shares the same engine cross-project.
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
@@ -49,6 +49,7 @@ skills/agentic-sdlc-skill/
49
49
  ├── debugging.md
50
50
  ├── elicitation.md
51
51
  ├── review.md
52
+ ├── dispatch.md
52
53
  ├── ENFORCEMENT.md
53
54
  └── scripts/
54
55
  └── sdlc_check.py
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-sdlc-skill",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Documentation-First SDLC protocol with triage, Vision governance and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/agentic-sdlc-skill",
3
- "version": "1.9.0",
3
+ "version": "1.11.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",
@@ -32,6 +32,7 @@
32
32
  "skills/agentic-sdlc-skill/debugging.md",
33
33
  "skills/agentic-sdlc-skill/elicitation.md",
34
34
  "skills/agentic-sdlc-skill/review.md",
35
+ "skills/agentic-sdlc-skill/dispatch.md",
35
36
  "skills/agentic-sdlc-skill/ENFORCEMENT.md",
36
37
  "skills/agentic-sdlc-skill/scripts/sdlc_check.py",
37
38
  "gemini-extension.json",
@@ -54,3 +54,55 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
54
54
  - `ai_docs/`, `tests/` and `test/` are always excluded from blocking.
55
55
  - The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
56
56
  - **Hybrid/devPNT projects**: add `--hybrid` to the gate command. Governed designs live in the devPNT DB, so the gate also unlocks when an approved E-TDD shadow (`ai_docs/solutions/SHADOW_*tdd*.md`, exported before implementation — see the SKILL.md shadow discipline) is present. Without the flag the gate would block legitimate governed work. The flag is deliberately explicit: never auto-detected.
57
+
58
+ ## 4. SessionStart hook (orientation, optional)
59
+
60
+ Emits the `ai_docs/` orientation — reading guide (`README.md`), manifest (`INDEX.md`), guide router (`reference/INDEX.md`) and last `handoff.md` — plus the Rule-Zero triage reminder to stdout at session start, so the agent begins already oriented instead of reading them only if it remembers to. It is **fail-open**: a missing, unreadable or oversized doc is skipped, the output is size-capped, and it always exits 0 — a broken or empty `ai_docs/` never blocks the session. It is **zero-execution** (it reads and prints, never runs anything) and opt-in.
61
+
62
+ Wire it via each client's SessionStart mechanism — the same command everywhere (add `--hybrid` on devPNT/Hybrid projects):
63
+
64
+ Claude Code — in the project's `.claude/settings.json`:
65
+
66
+ ```json
67
+ {
68
+ "hooks": {
69
+ "SessionStart": [
70
+ {
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "python \"C:\\Users\\<user>\\.claude\\skills\\agentic-sdlc\\scripts\\sdlc_check.py\" orient"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ Codex — in `.codex/hooks.json`, the same `SessionStart` → `{"type":"command","command":"… orient"}` shape (this replaces the legacy static-echo protocol some fixtures still carry).
84
+
85
+ Gemini CLI — wire the same command into its startup-hook mechanism if present; otherwise the step simply degrades to the manual Phase-1 reads (no capability lost).
86
+
87
+ **Usage notes:**
88
+ - The hook assumes the working directory is the project root (standard Claude Code hook behavior); it also accepts `--root <path>`.
89
+ - **Hybrid/devPNT projects**: add `--hybrid` — the hook then appends a one-line pointer to run `devpnt_mcp_get_bootstrap` for the Master Plan / Knowledge Layer and does not replicate them; the filesystem orientation (router + handoff + README) still emits.
90
+ - Like the CI gate (§2), if you copied `sdlc_check.py` into the repo, the hook references that copy — keep it current when you update the skill.
91
+
92
+ ## 5. Skill eval battery (release gate)
93
+
94
+ The skill self-tests its own doctrine invariants. Two layers over one scenario corpus:
95
+
96
+ **Static battery — the deterministic release gate.** Run before any publish:
97
+
98
+ ```
99
+ python -m unittest discover -s skills/agentic-sdlc-skill/scripts -p "test_*.py"
100
+ ```
101
+
102
+ It aggregates the three test files (`test_plan.py` + `test_session_start.py` + `test_skill_invariants.py`) and asserts the skill's invariants: the M4 triggers/hook/worktree doctrine is present and wired, support-file pointers resolve, and the generated indexes are idempotent. A non-zero exit **blocks the release** — a failing eval is always a real regression, never flakiness: the battery is stdlib-only, makes no model/network/subprocess call (deterministic by construction). If `test_indexes_idempotent` fails, run `sdlc_check.py index` and re-run.
103
+
104
+ **Behavioral corpus — opt-in, non-CI.** `evals/scenarios/*.md` (declarative, model-neutral) + `evals/run_behavioral.py` seed a fixture and print a prompt + pass criteria for a human/agent to run and self-assess. Because live adherence is nondeterministic, this layer **never gates** — it is the reproducible way to check that, e.g., the consult trigger actually fires on a seeded repo (the "demonstrably" artifact).
105
+
106
+ **Optional CI** (same shape as §2, not mandatory): add a `run:` step invoking the `unittest discover` command above.
107
+
108
+ **T10 note:** if you copied `sdlc_check.py` and the `test_*.py` battery into the repo for CI, that copy is authoritative — keep it current when you update the skill.
@@ -156,6 +156,7 @@ gate) instead of requiring an IN_PROGRESS ANALYSIS.
156
156
 
157
157
  - Read `ai_docs/audit/handoff.md` if it exists; if its Date/Branch are inconsistent, treat it as history.
158
158
  - Read `ai_docs/README.md` (curated must-reads) and `ai_docs/INDEX.md` (generated manifest of all canonical docs) to know what exists before exploring the code. `solutions/` and `audit/` are not indexed per file: search them with glob/grep.
159
+ - Optional: a SessionStart hook (`ENFORCEMENT.md` §4) can emit this orientation automatically at session start (README + INDEX + guide router + handoff + triage reminder); when it is not wired, do these reads manually as above. The hook is a convenience, never a requirement — it introduces no Python dependency for the process itself and fails open (a missing/empty `ai_docs/` never blocks the session).
159
160
  - If `ai_docs/` is missing or incomplete, create the structure and minimal documents by analyzing the project in batches.
160
161
  - In Standalone use `ai_docs/audit/audit_plan.md` for mapping and state.
161
162
  - In Hybrid prefer the devPNT/KL mapping when available; do not duplicate plan governance.
@@ -192,18 +193,22 @@ Hybrid L3:
192
193
  ### 4. Development and Testing
193
194
 
194
195
  - Implement only after the documentation gate required by the level.
196
+ - Isolate the work: run an L3 change on its own branch. In Hybrid, prefer a git worktree from the start — a running devPNT server locks `.devpnt/*.db` and blocks in-place branch switches/merges in the primary worktree.
195
197
  - Modify surgically, consistently with the plan.
196
198
  - Implementation work follows the TDD discipline in `tdd.md` (RED/GREEN/REFACTOR — the L2/L3 default; record the reason when it does not apply).
199
+ - Before implementing (L2/L3; L1 exempt), **consult the guide router** for a guide covering the task and read it first — the consult trigger (`guides.md` §0, summarized under `## Operative Guides`). A targeted description match, not a blanket read.
197
200
  - If the environment does not allow automated tests, declare the alternative verification and the reason.
198
201
  - For bugs (L2/L3), follow the systematic debugging method in `debugging.md`.
199
202
  - 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.
200
203
  - Update the ANALYSIS Diary or the Action Plan when you complete milestones, hit blockers or change decisions.
204
+ - **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.
201
205
 
202
206
  ### 5. Closure
203
207
 
204
208
  - Run the relevant tests/lint/smoke checks.
205
209
  - 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).
206
210
  - Verify alignment with the local Vision or the devPNT M-VISION.
211
+ - If the work was governed by user-provided indications and is reusable, **PROPOSE distilling a guide** (proactive trigger, `guides.md` §1) — a proposal for the user, never a silent write, never from model knowledge.
207
212
  - Update only the documents actually impacted.
208
213
  - **Aligned indexes (Poka-Yoke)**: if you created, moved or removed canonical documents (`vision/`, `reference/`, `architecture/`, `functional/`, `strategic/`):
209
214
  - regenerate the manifest with `sdlc_check.py index` (writes `ai_docs/INDEX.md`) — never write it by hand;
@@ -215,6 +220,7 @@ Hybrid L3:
215
220
  - In Hybrid propose ADR/KL updates when there were architectural decisions.
216
221
  - In Standalone, if the project adopts `sdlc_check.py`, run `python <skill_dir>/scripts/sdlc_check.py check --root <project_root>` or the equivalent local copy.
217
222
  - Updated documents must travel in the same commit/PR as the code they describe.
223
+ - **Branch/worktree hygiene**: an L3 ran on its own branch (Phase 4) — close it with an explicit merge decision (merge, keep open, or discard) and clean up the branch/worktree; never leave orphan branches. In Hybrid, the running devPNT server locks `.devpnt/*.db`, so the merge is done from a separate git worktree or via a ref-only push, never an in-place branch switch in the primary worktree.
218
224
 
219
225
  ## ai_docs documents: two indexes + lifecycle
220
226
 
@@ -243,6 +249,11 @@ Legacy note: the validator also accepts the deprecated Italian frontmatter keys
243
249
 
244
250
  ## Operative Guides
245
251
 
252
+ Guides are **consulted, created, and proposed** — three moments; the mechanics live once in `guides.md`:
253
+ - **Consult (before acting):** before operative L2/L3 work (L1 exempt), check the guide router for a guide covering the task and read the match first — a targeted description match, never a blanket read. → `guides.md` §0.
254
+ - **Create (from user indications):** the origin+purpose test below.
255
+ - **Propose proactively (after success):** after reusable, user-indication-governed work, PROPOSE distilling a guide — a proposal, never a silent write, never from model knowledge. → `guides.md` §1.
256
+
246
257
  Trigger test: the user hands over indications to follow (origin = user, not model
247
258
  knowledge) meant to govern how the agent operates (purpose = operative), not just
248
259
  inform an answer. Both hold → distill into `ai_docs/reference/GUIDE_[topic].md`.
@@ -264,6 +275,7 @@ regenerate with `sdlc_check.py index`.
264
275
  The prompt is not enforcement. When the project needs repeatable guarantees:
265
276
  - read `ENFORCEMENT.md`;
266
277
  - use `scripts/sdlc_check.py validate --strict` in CI;
267
- - use `scripts/sdlc_check.py gate` only for security-critical directories, not for the whole repository.
278
+ - use `scripts/sdlc_check.py gate` only for security-critical directories, not for the whole repository;
279
+ - (skill development) the self-eval battery `python -m unittest discover -s scripts -p "test_*.py"` guards the skill's own doctrine invariants and is the deterministic release gate — see `ENFORCEMENT.md` §5.
268
280
 
269
281
  The validator is a support, not a universal prerequisite: the skill must stay usable in environments without Python or hooks, declaring what it cannot verify automatically.
@@ -0,0 +1,85 @@
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
+ **Guide consumption under dispatch.** Selecting each task's `guides` field IS the
37
+ consult trigger (`guides.md` §0) applied at plan-authoring time: the orchestrator
38
+ runs the router lookup (project router `ai_docs/reference/INDEX.md` + the agent-KB
39
+ router) when populating `guides`. A dispatched context-free subagent does **NOT**
40
+ run its own router consult — it reads the guide pointers handed to it in the
41
+ brief. (Proactive guide-creation stays at closure — the same broad final pass
42
+ below — so it needs no separate dispatch hook.)
43
+
44
+ ## Model tiers (client-relative, no provider names)
45
+
46
+ - Default dispatch: **economy** implementer tier.
47
+ - After **two** consecutive `verify_result: fail` on the same task: escalate
48
+ to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
49
+ first failure — a single fail is often a brief or environment issue, not a
50
+ capability gap.
51
+
52
+ ## Review slots — one-shot, not iterative
53
+
54
+ Exactly three review touches per task, never a loop:
55
+
56
+ 1. Inline self-review by the implementer subagent before it reports done
57
+ (the standard critical-review pass, not a separate call).
58
+ 2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
59
+ Standalone: the `review.md` discipline).
60
+ 3. One broad final pass over the whole plan at closure, after all tasks are
61
+ DONE — catches cross-task drift a per-task review cannot see.
62
+
63
+ If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
64
+ via the ledger's fail path, not an extra review slot.
65
+
66
+ ## Ledger protocol summary
67
+
68
+ Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
69
+ memory the loop needs across sessions or context compaction: a resumed
70
+ orchestrator re-reads it and picks up exactly where it left off, never
71
+ re-running a DONE task.
72
+
73
+ ## Degradation
74
+
75
+ No subagent-spawning tool available → the orchestrator runs each task in the
76
+ same session, against the same plan and ledger, with the same one-shot review
77
+ slots. No capability is lost, only the parallelism/isolation subagents would
78
+ have added.
79
+
80
+ ## Hybrid note
81
+
82
+ The plan's `derived-from` points at the accepted E-TDD document key. Per-task
83
+ review reuses the devPNT independent reviewers (§4.6 code review gate) rather
84
+ than restating review doctrine — see `review.md` for the single definition
85
+ both modes share.
@@ -17,6 +17,26 @@ when a task needs detail. Two levels, both produced by this pipeline:
17
17
  A guide that restates the source at length is as wrong as a fragmented one:
18
18
  completeness is guaranteed by the book level, economy by the synthesis level.
19
19
 
20
+ ## 0. Consuming a guide (consult before acting)
21
+
22
+ Guides only pay off if they reach the work they govern. **Before operative work,
23
+ proportional to triage** — L1 is exempt; engaged for L2/L3 — ask: *does a guide
24
+ already cover how to do this well?*
25
+
26
+ - **Scan the router, not the guides.** Read the when-to-consult descriptions in
27
+ the guide router `ai_docs/reference/INDEX.md` **and** the agent-KB router
28
+ `~/.agentic-sdlc/ai_docs/reference/INDEX.md` (if present). Match by topic.
29
+ - **On a match, read that guide's synthesis whole** before acting (the snapshot
30
+ stays on demand via the section markers). No match → proceed normally.
31
+ - **Targeted match, never blanket.** Open only the guide whose description fits
32
+ the task — never load every guide, which would reintroduce the exact token
33
+ cost the "point to them" model exists to avoid. The two-level model (compact
34
+ synthesis / verbatim snapshot) already bounds a single guide's cost.
35
+ - **Under subagent dispatch** the consult happens at plan-authoring time (the
36
+ orchestrator populates each task's `guides` field); a context-free subagent
37
+ reads the pointers it was handed and does not run its own router lookup. See
38
+ `dispatch.md`.
39
+
20
40
  ## 1. When to trigger
21
41
 
22
42
  Trigger test is origin + purpose, not content taxonomy (no "is this technical
@@ -34,6 +54,20 @@ Never manufacture a guide from model knowledge. If the user asks for "a guide
34
54
  on X" without handing over source material, ask for the source first — a
35
55
  guide with no `distilled_from` is not this pipeline's output.
36
56
 
57
+ ### Proactive trigger (after reusable success)
58
+
59
+ The trigger above is reactive (the user hands material over). It has a proactive
60
+ twin: **after completing work that was governed by user-provided indications and
61
+ is plausibly reusable** — it would prepare a future task — **PROPOSE** distilling
62
+ a guide.
63
+
64
+ - It is a **proposal to the user**, routed into this same pipeline (§2 onward):
65
+ the user confirms topic/scope before any file is written.
66
+ - **Never a silent write, never from model knowledge.** If the work was not
67
+ governed by user-provided material there is nothing to distill — do not invent
68
+ a guide from general knowledge (the `distilled_from` fidelity constraint, §3,
69
+ is absolute). This adds a moment to PROPOSE, not a new writer.
70
+
37
71
  ## 2. Pipeline
38
72
 
39
73
  0. **Search before creating (DRY — one CURRENT guide per topic).** Before
@@ -59,6 +59,9 @@ GUIDE_MARKER_RE = re.compile(r"\[(?:source:[^\]]+|not covered by source)\]")
59
59
  # AGENTIC_SDLC_KB_ROOT env var is a TEST/CI seam only (scenario battery must
60
60
  # not touch the real user KB); the documented product path is fixed.
61
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")
62
65
 
63
66
  # Deprecated Italian frontmatter keys, mapped to the canonical English ones.
64
67
  LEGACY_KEYS = {"stato": "status", "livello": "level",
@@ -82,6 +85,20 @@ except Exception:
82
85
  pass
83
86
 
84
87
 
88
+ # --- orient (SessionStart hook) ---
89
+ # Fixed, hard-coded doc set (label, path-relative-to-root). No content- or
90
+ # user-derived paths -> no traversal input (P-TM T3); confine_under is
91
+ # defense-in-depth. Emitted at session start by the orient subcommand.
92
+ ORIENT_DOCS = [
93
+ ("Reading guide (README)", "ai_docs/README.md"),
94
+ ("Canonical manifest (INDEX)", "ai_docs/INDEX.md"),
95
+ ("Guide router (when-to-consult)", "ai_docs/reference/INDEX.md"),
96
+ ("Last session handoff", "ai_docs/audit/handoff.md"),
97
+ ]
98
+ ORIENT_PER_DOC_CHARS = 6000 # per-doc truncation
99
+ ORIENT_MAX_TOTAL_CHARS = 16000 # total ingestion cap (P-TM T2); tunable
100
+
101
+
85
102
  # ----------------------------------------------------------------- utilities
86
103
 
87
104
  def utc_now_iso():
@@ -106,6 +123,25 @@ def require_ai_docs(root, command):
106
123
  return True
107
124
 
108
125
 
126
+ def confine_under(base, rel):
127
+ """Fail-closed path confinement: resolve `rel` under `base` and require the
128
+ result to stay inside `base`. Returns None (reject) if `rel` is absolute,
129
+ contains a '..' part, or resolves outside `base` (including an OSError
130
+ during resolution, e.g. an unresolvable/reparse-point path on Windows).
131
+ Single source for path confinement (T2/T3): reused by check_kb_collisions'
132
+ `overrides:` check and cmd_validate's `distilled_from` check, and by the
133
+ new `plan` command's paths/consumes/produces/guides confinement."""
134
+ p = Path(rel)
135
+ if p.is_absolute() or ".." in p.parts:
136
+ return None
137
+ try:
138
+ t = (base / rel).resolve()
139
+ t.relative_to(base.resolve())
140
+ return t
141
+ except (ValueError, OSError):
142
+ return None
143
+
144
+
109
145
  def read_text(path):
110
146
  # utf-8-sig: strips a leading BOM (files authored on Windows) so the
111
147
  # frontmatter '---' on line 0 stays recognizable; reads plain utf-8 otherwise.
@@ -407,15 +443,10 @@ def check_kb_collisions(root, project_guides, errors, warnings):
407
443
  ov = (meta.get("overrides") or "").strip()
408
444
  if ov:
409
445
  # T6: untrusted cross-root pointer — distilled_from parity, fail closed
410
- ovp = Path(ov)
411
- if ovp.is_absolute() or ".." in ovp.parts:
412
- errors.append(f"{rel}: overrides '{ov}' is absolute or contains '..' rejected (fail closed)")
413
- continue
414
- try:
415
- target = (kb_ref / ov).resolve()
416
- target.relative_to(kb_ref.resolve())
417
- except (ValueError, OSError):
418
- errors.append(f"{rel}: overrides '{ov}' escapes the KB reference dir — rejected (fail closed)")
446
+ target = confine_under(kb_ref, ov)
447
+ if target is None:
448
+ errors.append(f"{rel}: overrides '{ov}' is absolute, contains '..', or escapes the KB "
449
+ "reference dir — rejected (fail closed)")
419
450
  continue
420
451
  if not target.is_file():
421
452
  warnings.append(f"{rel}: overrides target '{ov}' not found in KB ({kb_ref})")
@@ -572,15 +603,9 @@ def cmd_validate(root, strict=False):
572
603
  + "; ".join(unmarked[:5]))
573
604
  # (c) distilled_from confinement — fail closed (P-TM T6, distilled_from vector)
574
605
  df = meta.get("distilled_from", "")
575
- if df:
576
- if Path(df).is_absolute() or ".." in Path(df).parts:
577
- errors.append(f"{rel}: distilled_from '{df}' is absolute or escapes the project (..): rejected")
578
- else:
579
- target = (root / df).resolve()
580
- try:
581
- target.relative_to(root.resolve())
582
- except ValueError:
583
- errors.append(f"{rel}: distilled_from '{df}' resolves outside the project root: rejected")
606
+ if df and confine_under(root, df) is None:
607
+ errors.append(f"{rel}: distilled_from '{df}' is absolute, contains '..', or resolves "
608
+ "outside the project root: rejected")
584
609
  check_kb_collisions(root, guides, errors, warnings)
585
610
  # guide-router alignment (mirror of the root-manifest check)
586
611
  gidx = root / "ai_docs" / "reference" / "INDEX.md"
@@ -806,6 +831,226 @@ def cmd_gate(args):
806
831
  return 2
807
832
 
808
833
 
834
+ # --------------------------------------------------------------------- plan
835
+ # Subagent Execution (Feature A). Zero-execution surface: this section and
836
+ # everything it calls MUST NOT spawn a process (no subprocess/os.system/eval/
837
+ # exec, no git_* helper). It validates a PLAN_[feature].md and prints a task
838
+ # brief as text; the orchestrator (dispatch.md) is the sole executor.
839
+
840
+ _PLAN_JSON_RE = re.compile(r"```json\s*\n(.*?)```", re.DOTALL)
841
+
842
+
843
+ def extract_plan_json(text):
844
+ """Extract the first fenced ```json block from a PLAN_[feature].md body.
845
+ Returns (data, "") on success, or (None, reason) on any failure. Never
846
+ raises: a malformed or missing block is a validation failure, not a crash."""
847
+ m = _PLAN_JSON_RE.search(text or "")
848
+ if not m:
849
+ return None, "no fenced ```json block found in the plan file"
850
+ try:
851
+ data = json.loads(m.group(1))
852
+ except (ValueError, TypeError) as e:
853
+ return None, f"malformed JSON in the plan block: {e}"
854
+ if not isinstance(data, dict):
855
+ return None, "plan JSON block must be a JSON object"
856
+ return data, ""
857
+
858
+
859
+ def load_ledger(path):
860
+ """Read the sidecar ledger {"<task_id>": {"status", "verify_result",
861
+ "timestamp"}}. Absent file -> ({}, ""). Malformed/unreadable -> ({}, reason).
862
+ Never raises, never hangs: the ledger is untrusted state read on every call."""
863
+ if not path.is_file():
864
+ return {}, ""
865
+ try:
866
+ raw = read_text(path)
867
+ data = json.loads(raw)
868
+ except (ValueError, TypeError, OSError) as e:
869
+ return {}, f"ledger '{path}' unreadable/malformed, treating as empty: {e}"
870
+ if not isinstance(data, dict):
871
+ return {}, f"ledger '{path}' is not a JSON object, treating as empty"
872
+ return data, ""
873
+
874
+
875
+ def _confine_or_reject(base, rel, label, rel_label, errors):
876
+ t = confine_under(base, rel)
877
+ if t is None:
878
+ errors.append(f"{rel_label}: {label} '{rel}' is absolute, contains '..', or escapes "
879
+ f"'{base}' — rejected (fail closed)")
880
+ return t
881
+
882
+
883
+ def _validate_plan_tasks(root, data, rel_label, errors, warnings):
884
+ """Shared core of `plan validate`/`plan brief`: schema + confinement checks.
885
+ Returns the task list (possibly empty) on success; errors/warnings are
886
+ appended in place. Callers decide the exit code."""
887
+ tasks = data.get("tasks")
888
+ if not isinstance(tasks, list) or not tasks:
889
+ errors.append(f"{rel_label}: 'tasks' must be a non-empty JSON array")
890
+ return []
891
+ ref_dir = root / "ai_docs" / "reference"
892
+ kb_ref = DEFAULT_KB_ROOT / "ai_docs" / "reference"
893
+ seen_ids = set()
894
+ for i, task in enumerate(tasks):
895
+ loc = f"{rel_label}: task[{i}]"
896
+ if not isinstance(task, dict):
897
+ errors.append(f"{loc}: not a JSON object")
898
+ continue
899
+ missing = [k for k in PLAN_TASK_REQUIRED if not task.get(k)]
900
+ if missing:
901
+ errors.append(f"{loc}: missing required field(s): {', '.join(missing)}")
902
+ if not task.get("paths") and not task.get("produces"):
903
+ errors.append(f"{loc}: must declare at least one of 'paths'/'produces'")
904
+ tid = task.get("id")
905
+ if tid:
906
+ if tid in seen_ids:
907
+ errors.append(f"{loc}: duplicate task id '{tid}'")
908
+ seen_ids.add(tid)
909
+ for key in ("paths", "consumes", "produces"):
910
+ for p in (task.get(key) or []):
911
+ _confine_or_reject(root, p, key, loc, errors)
912
+ for g in (task.get("guides") or []):
913
+ in_project = confine_under(ref_dir, g)
914
+ in_kb = confine_under(kb_ref, g)
915
+ if in_project is None and in_kb is None:
916
+ errors.append(f"{loc}: guide '{g}' is not confined under the project reference "
917
+ f"dir ({ref_dir}) or the agent KB reference dir ({kb_ref}) — rejected")
918
+ return tasks
919
+
920
+
921
+ def cmd_plan(root, args):
922
+ """Zero-execution: validates/briefs a PLAN_[feature].md. Never spawns a
923
+ process, never calls a git_* helper, never runs the opaque `verify` text —
924
+ it is printed, not executed."""
925
+ plan_path = Path(args.file)
926
+ if not plan_path.is_absolute():
927
+ plan_path = root / plan_path
928
+ if not plan_path.is_file():
929
+ sys.stderr.write(f"[plan] plan file not found: {plan_path}\n")
930
+ return 2
931
+ rel_label = str(plan_path)
932
+ data, reason = extract_plan_json(read_text(plan_path))
933
+ if data is None:
934
+ sys.stderr.write(f"[plan] {rel_label}: {reason}\n")
935
+ return 2
936
+
937
+ errors, warnings = [], []
938
+ tasks = _validate_plan_tasks(root, data, rel_label, errors, warnings)
939
+
940
+ ledger_path = plan_path.with_name(plan_path.stem + ".ledger.json")
941
+ ledger, ledger_reason = load_ledger(ledger_path)
942
+ if ledger_reason:
943
+ warnings.append(ledger_reason)
944
+ if not errors:
945
+ task_ids = {t.get("id") for t in tasks if isinstance(t, dict)}
946
+ for lid in ledger:
947
+ if lid not in task_ids:
948
+ warnings.append(f"ledger id '{lid}' not found in {rel_label}: orphaned entry (not fatal)")
949
+
950
+ for w in warnings:
951
+ sys.stderr.write(f"[warn] {w}\n")
952
+ for e in errors:
953
+ sys.stderr.write(f"[ERROR] {e}\n")
954
+
955
+ if args.plan_cmd == "validate":
956
+ if errors:
957
+ sys.stderr.write(f"\n[plan] validate: {len(errors)} errors, {len(warnings)} warnings.\n")
958
+ return 2
959
+ print(f"[ok] {rel_label}: plan valid ({len(tasks)} task(s), {len(warnings)} warning(s)).")
960
+ return 0
961
+
962
+ # brief
963
+ if errors:
964
+ sys.stderr.write(f"\n[plan] brief: plan is invalid, refusing to brief ({len(errors)} errors).\n")
965
+ return 2
966
+ target = None
967
+ for t in tasks:
968
+ if isinstance(t, dict) and t.get("id") == args.task:
969
+ target = t
970
+ break
971
+ if target is None:
972
+ sys.stderr.write(f"[plan] brief: task id '{args.task}' not found in {rel_label}\n")
973
+ return 2
974
+
975
+ print(f"# Task: {target.get('id')} — {target.get('title', '')}")
976
+ print()
977
+ print("## Task block")
978
+ print(json.dumps(target, indent=2))
979
+ print()
980
+ print("## Produces of prior-order tasks (interfaces)")
981
+ prior_produces = []
982
+ for t in tasks:
983
+ if not isinstance(t, dict):
984
+ continue
985
+ if t.get("id") == target.get("id"):
986
+ break
987
+ prior_produces.extend(t.get("produces") or [])
988
+ if prior_produces:
989
+ for p in prior_produces:
990
+ print(f"- {p}")
991
+ else:
992
+ print("(none)")
993
+ print()
994
+ print("## Guide pointers (paths, not content)")
995
+ guides = target.get("guides") or []
996
+ if guides:
997
+ for g in guides:
998
+ print(f"- {g}")
999
+ else:
1000
+ print("(none)")
1001
+ print()
1002
+ print("## Verify (opaque text — orchestrator runs this out of band, NOT executed here)")
1003
+ print(target.get("verify", ""))
1004
+ return 0
1005
+
1006
+
1007
+ def cmd_orient(args):
1008
+ """SessionStart hook: emit a bounded, repo-sourced ai_docs/ orientation to
1009
+ stdout and ALWAYS return 0 (fail-open, P-TM T8) -- a session hook must never
1010
+ block the session or surface a traceback. Zero-execution (P-TM T1): reads a
1011
+ fixed hard-coded doc set, confine_under each (P-TM T3), size-caps the total
1012
+ (P-TM T2). No subprocess/eval anywhere in this call graph."""
1013
+ try:
1014
+ root = Path(args.root).resolve() if getattr(args, "root", None) else find_project_root()
1015
+ chunks = []
1016
+ total = 0
1017
+ truncated = False
1018
+ for label, rel in ORIENT_DOCS:
1019
+ target = confine_under(root, rel)
1020
+ if target is None or not target.is_file():
1021
+ continue
1022
+ try:
1023
+ text = read_text(target)
1024
+ except OSError:
1025
+ continue
1026
+ remaining = ORIENT_MAX_TOTAL_CHARS - total
1027
+ if remaining <= 0:
1028
+ truncated = True
1029
+ break
1030
+ text = text[:ORIENT_PER_DOC_CHARS]
1031
+ if len(text) > remaining:
1032
+ text = text[:remaining]
1033
+ truncated = True
1034
+ chunks.append((label, text))
1035
+ total += len(text)
1036
+ if not chunks:
1037
+ return 0
1038
+ out = ["=== Agentic SDLC -- session orientation (repo-sourced context, not authored instructions) ==="]
1039
+ for label, text in chunks:
1040
+ out.append(f"\n## {label}\n{text}")
1041
+ if truncated:
1042
+ out.append("\n[orientation truncated to the size cap -- open the files directly for full content]")
1043
+ out.append("\nTriage every request (Rule Zero): L1 trivial - L2 small - L3 significant - Spike. "
1044
+ "When in doubt, pick the higher level.")
1045
+ if getattr(args, "hybrid", False):
1046
+ out.append("\n[devPNT active] Run devpnt_mcp_get_bootstrap for the Master Plan / Knowledge Layer -- "
1047
+ "the orientation above is the filesystem layer, not a bootstrap duplicate.")
1048
+ print("\n".join(out))
1049
+ return 0
1050
+ except Exception:
1051
+ return 0
1052
+
1053
+
809
1054
  # --------------------------------------------------------------------- main
810
1055
 
811
1056
  def main(argv=None):
@@ -836,9 +1081,23 @@ def main(argv=None):
836
1081
  gp.add_argument("--file", help="file path to evaluate (alternative to --hook)")
837
1082
  gp.add_argument("--protected", default="", help="protected prefixes separated by ';' (e.g. \"src/auth;src/crypto\")")
838
1083
 
1084
+ sub.add_parser("orient", parents=[common, hybrid_opt],
1085
+ help="SessionStart hook: emit ai_docs/ orientation to stdout (fail-open, zero-execution)")
1086
+
1087
+ pp = sub.add_parser("plan", parents=[common],
1088
+ help="Subagent Execution: validate/brief a PLAN_[feature].md (zero-execution)")
1089
+ pp_sub = pp.add_subparsers(dest="plan_cmd", required=True)
1090
+ pv = pp_sub.add_parser("validate", help="schema + confinement + ledger cross-check (exit 2 on error)")
1091
+ pv.add_argument("file", help="path to the PLAN_[feature].md file")
1092
+ pb = pp_sub.add_parser("brief", help="print a task's brief to stdout (verify text is NOT executed)")
1093
+ pb.add_argument("file", help="path to the PLAN_[feature].md file")
1094
+ pb.add_argument("--task", required=True, help="task id to brief")
1095
+
839
1096
  args = ap.parse_args(argv)
840
1097
  if args.cmd == "gate":
841
1098
  return cmd_gate(args)
1099
+ if args.cmd == "orient":
1100
+ return cmd_orient(args)
842
1101
 
843
1102
  root = Path(args.root).resolve() if args.root else find_project_root()
844
1103
  if args.cmd == "check":
@@ -851,6 +1110,8 @@ def main(argv=None):
851
1110
  return cmd_stale(root, hybrid=args.hybrid)
852
1111
  if args.cmd == "mark":
853
1112
  return cmd_mark(root, args.paths)
1113
+ if args.cmd == "plan":
1114
+ return cmd_plan(root, args)
854
1115
  return 0
855
1116
 
856
1117
 
@@ -189,6 +189,54 @@ Allowed frontmatter states: `PLANNED` | `IN_PROGRESS` | `COMPLETED` | `CANCELLED
189
189
  <!-- max 1 page. Spike code is NOT mergeable: for production reclassify L2/L3. -->
190
190
  ```
191
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
+
192
240
  ## ai_docs/audit/audit_plan.md (Standalone mode only)
193
241
 
194
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`.