@antoneeo/agentic-sdlc-skill 1.9.0 → 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 +14 -0
- package/README.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +2 -1
- package/skills/agentic-sdlc-skill/SKILL.md +1 -0
- package/skills/agentic-sdlc-skill/dispatch.md +77 -0
- package/skills/agentic-sdlc-skill/scripts/sdlc_check.py +213 -18
- package/skills/agentic-sdlc-skill/templates.md +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
5
19
|
## [1.9.0] - 2026-07-03 (M2 execution disciplines + Feature B unit 2 agent KB)
|
|
6
20
|
### Added
|
|
7
21
|
- `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
|
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",
|
|
@@ -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",
|
|
@@ -198,6 +198,7 @@ Hybrid L3:
|
|
|
198
198
|
- For bugs (L2/L3), follow the systematic debugging method in `debugging.md`.
|
|
199
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.
|
|
200
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.
|
|
201
202
|
|
|
202
203
|
### 5. Closure
|
|
203
204
|
|
|
@@ -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.
|
|
@@ -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",
|
|
@@ -106,6 +109,25 @@ def require_ai_docs(root, command):
|
|
|
106
109
|
return True
|
|
107
110
|
|
|
108
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
|
+
|
|
109
131
|
def read_text(path):
|
|
110
132
|
# utf-8-sig: strips a leading BOM (files authored on Windows) so the
|
|
111
133
|
# frontmatter '---' on line 0 stays recognizable; reads plain utf-8 otherwise.
|
|
@@ -407,15 +429,10 @@ def check_kb_collisions(root, project_guides, errors, warnings):
|
|
|
407
429
|
ov = (meta.get("overrides") or "").strip()
|
|
408
430
|
if ov:
|
|
409
431
|
# T6: untrusted cross-root pointer — distilled_from parity, fail closed
|
|
410
|
-
|
|
411
|
-
if
|
|
412
|
-
errors.append(f"{rel}: overrides '{ov}' is absolute
|
|
413
|
-
|
|
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)")
|
|
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)")
|
|
419
436
|
continue
|
|
420
437
|
if not target.is_file():
|
|
421
438
|
warnings.append(f"{rel}: overrides target '{ov}' not found in KB ({kb_ref})")
|
|
@@ -572,15 +589,9 @@ def cmd_validate(root, strict=False):
|
|
|
572
589
|
+ "; ".join(unmarked[:5]))
|
|
573
590
|
# (c) distilled_from confinement — fail closed (P-TM T6, distilled_from vector)
|
|
574
591
|
df = meta.get("distilled_from", "")
|
|
575
|
-
if df:
|
|
576
|
-
|
|
577
|
-
|
|
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")
|
|
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")
|
|
584
595
|
check_kb_collisions(root, guides, errors, warnings)
|
|
585
596
|
# guide-router alignment (mirror of the root-manifest check)
|
|
586
597
|
gidx = root / "ai_docs" / "reference" / "INDEX.md"
|
|
@@ -806,6 +817,179 @@ def cmd_gate(args):
|
|
|
806
817
|
return 2
|
|
807
818
|
|
|
808
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
|
+
|
|
809
993
|
# --------------------------------------------------------------------- main
|
|
810
994
|
|
|
811
995
|
def main(argv=None):
|
|
@@ -836,6 +1020,15 @@ def main(argv=None):
|
|
|
836
1020
|
gp.add_argument("--file", help="file path to evaluate (alternative to --hook)")
|
|
837
1021
|
gp.add_argument("--protected", default="", help="protected prefixes separated by ';' (e.g. \"src/auth;src/crypto\")")
|
|
838
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
|
+
|
|
839
1032
|
args = ap.parse_args(argv)
|
|
840
1033
|
if args.cmd == "gate":
|
|
841
1034
|
return cmd_gate(args)
|
|
@@ -851,6 +1044,8 @@ def main(argv=None):
|
|
|
851
1044
|
return cmd_stale(root, hybrid=args.hybrid)
|
|
852
1045
|
if args.cmd == "mark":
|
|
853
1046
|
return cmd_mark(root, args.paths)
|
|
1047
|
+
if args.cmd == "plan":
|
|
1048
|
+
return cmd_plan(root, args)
|
|
854
1049
|
return 0
|
|
855
1050
|
|
|
856
1051
|
|
|
@@ -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`.
|