@antoneeo/agentic-sdlc-skill 1.34.0 → 1.36.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 +44 -0
- package/README.md +204 -186
- package/gemini-extension.json +1 -1
- package/package.json +3 -1
- package/scripts/init.js +6 -17
- package/skills/agentic-sdlc-skill/ENFORCEMENT.md +5 -5
- package/skills/agentic-sdlc-skill/SKILL.md +11 -3
- package/skills/agentic-sdlc-skill/dispatch.md +5 -1
- package/skills/agentic-sdlc-skill/elicitation.md +216 -173
- package/skills/agentic-sdlc-skill/memory.md +88 -0
- package/skills/agentic-sdlc-skill/review.md +8 -1
- package/skills/agentic-sdlc-skill/routing.md +7 -1
- package/skills/agentic-sdlc-skill/scripts/knowledge.py +2089 -0
- package/skills/agentic-sdlc-skill/scripts/sdlc_check.py +71 -80
- package/skills/agentic-sdlc-skill/scripts/sdlc_core.py +84 -5
- package/skills/agentic-sdlc-skill/templates.md +1 -1
|
@@ -1,80 +1,71 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def main(argv=None):
|
|
76
|
-
return sdlc_core.main(argv)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if __name__ == "__main__":
|
|
80
|
-
sys.exit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Domain entry point with shared project memory.
|
|
4
|
+
|
|
5
|
+
Ship this file, sdlc_core.py and knowledge.py together. The core alone omits
|
|
6
|
+
knowledge integrity and this entry point\'s domain-specific behavior.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import sdlc_core
|
|
15
|
+
except ImportError as exc: # pragma: no cover - exercised by TS12, not by unit tests
|
|
16
|
+
sys.stderr.write(
|
|
17
|
+
"[ERROR] sdlc_check.py cannot find sdlc_core.py next to it: " + str(exc) + "\n"
|
|
18
|
+
" The validator needs entry point, sdlc_core.py and knowledge.py.\n"
|
|
19
|
+
" Copy all three together; core alone omits memory and overlay checks.\n")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
# Re-export the core's surface: existing importers (`import sdlc_check as sc`)
|
|
23
|
+
# and the test batteries reach for these names on this module.
|
|
24
|
+
from sdlc_core import * # noqa: F401,F403
|
|
25
|
+
from sdlc_core import _map_refs # noqa: F401 underscore helper used by the batteries
|
|
26
|
+
import knowledge
|
|
27
|
+
|
|
28
|
+
# The domain this distribution implements. It does NOT decide any document's
|
|
29
|
+
# owning domain -- that is resolved per project (`default_domain:` in the docs
|
|
30
|
+
# root's README) and per artifact (`domain:`), so the same tree gets the same
|
|
31
|
+
# verdict from every installed distribution. What it decides is which portable
|
|
32
|
+
# checks a document may import here by name; the rest warn as unavailable.
|
|
33
|
+
DOMAIN = "code"
|
|
34
|
+
|
|
35
|
+
sdlc_core.set_entry_point(DOMAIN, provides=("code", "knowledge"))
|
|
36
|
+
|
|
37
|
+
# What this distribution carries. The shared battery reads it; the spine
|
|
38
|
+
# capabilities are not optional, and a shared test refuses a profile that drops one.
|
|
39
|
+
sdlc_core.set_profile(
|
|
40
|
+
skill_name="agentic-sdlc",
|
|
41
|
+
unit_noun="feature",
|
|
42
|
+
support_files=("templates.md", "architect.md", "guides.md", "vision.md", "tdd.md",
|
|
43
|
+
"debugging.md", "elicitation.md", "review.md", "dispatch.md",
|
|
44
|
+
"routing.md", "hybrid.md", "ENFORCEMENT.md", "memory.md"),
|
|
45
|
+
capabilities=(
|
|
46
|
+
# spine
|
|
47
|
+
"triage", "write_triggers", "workstream_registry", "vision_gate",
|
|
48
|
+
"design_review_gate", "guide_router", "worktree_hygiene",
|
|
49
|
+
# code overlay
|
|
50
|
+
"architect_pass", "interaction_contract", "comprehension_guides", "tdd",
|
|
51
|
+
"subagent_dispatch", "legacy_narrative_handoff", "question_discipline",
|
|
52
|
+
),
|
|
53
|
+
design_gate_between=("### 3. Request Analysis", "### 4. Development and Testing"),
|
|
54
|
+
remind_line=(
|
|
55
|
+
"first decide if this skill governs the work; if yes, LOAD it once -- an "
|
|
56
|
+
"unread protocol cannot be applied. Declare the level (L1/L2/L3/Spike) "
|
|
57
|
+
"and the guide-router verdict before acting; an imperative is a request "
|
|
58
|
+
"to triage. Name the mechanism before fixing the first defect you can "
|
|
59
|
+
"measure -- symptom patches recur and stack. Replay governing text -- "
|
|
60
|
+
"doctrine, a prompt, a schema -- against the case that motivated it: red "
|
|
61
|
+
"under the old text, green under the new."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv=None):
|
|
67
|
+
return knowledge.main(argv)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
sys.exit(main())
|
|
@@ -569,6 +569,36 @@ def git_changed_since(root, ref, rel_path):
|
|
|
569
569
|
return None
|
|
570
570
|
|
|
571
571
|
|
|
572
|
+
def git_ref_state(root, ref):
|
|
573
|
+
"""Where a recorded commit stands against HEAD (F-056).
|
|
574
|
+
|
|
575
|
+
'ancestor' -- in HEAD's history; 'orphan' -- a commit HEAD's history does not
|
|
576
|
+
contain (amended, rebased or squash-merged away, or marked on another branch:
|
|
577
|
+
it stops resolving once pruned, and on any fresh clone); 'missing' -- not a
|
|
578
|
+
commit in this repository; 'unknown' -- git gave no yes/no answer.
|
|
579
|
+
Callers pass a hash-shaped ref only: that check is what keeps document
|
|
580
|
+
content from reaching git's argv as an option."""
|
|
581
|
+
try:
|
|
582
|
+
r = subprocess.run(["git", "cat-file", "-e", ref + "^{commit}"],
|
|
583
|
+
cwd=str(root), capture_output=True, text=True, timeout=30)
|
|
584
|
+
if r.returncode != 0:
|
|
585
|
+
return "missing"
|
|
586
|
+
r = subprocess.run(["git", "merge-base", "--is-ancestor", ref, "HEAD"],
|
|
587
|
+
cwd=str(root), capture_output=True, text=True, timeout=30)
|
|
588
|
+
except Exception:
|
|
589
|
+
return "unknown"
|
|
590
|
+
return {0: "ancestor", 1: "orphan"}.get(r.returncode, "unknown")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def git_is_shallow(root):
|
|
594
|
+
try:
|
|
595
|
+
r = subprocess.run(["git", "rev-parse", "--is-shallow-repository"],
|
|
596
|
+
cwd=str(root), capture_output=True, text=True, timeout=10)
|
|
597
|
+
return r.returncode == 0 and r.stdout.strip() == "true"
|
|
598
|
+
except Exception:
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
|
|
572
602
|
# -------------------------------------------------------------------- index
|
|
573
603
|
|
|
574
604
|
def build_index(root):
|
|
@@ -1543,6 +1573,18 @@ def cmd_stale(root, hybrid=False):
|
|
|
1543
1573
|
"(audit not initialized, or Hybrid mode where mapping is delegated to devPNT).")
|
|
1544
1574
|
return rc # was: return 0 — MUST carry guide rc
|
|
1545
1575
|
use_git = git_available(root)
|
|
1576
|
+
entry = entry_script()
|
|
1577
|
+
# The audit plan records the marks: its own edits are not a change to the area
|
|
1578
|
+
# that contains it. Counting them made a committed hash mark of that area
|
|
1579
|
+
# stale again, and every re-mark repeated the loop.
|
|
1580
|
+
try:
|
|
1581
|
+
plan_rel = f.resolve().relative_to(Path(root).resolve()).as_posix()
|
|
1582
|
+
except ValueError:
|
|
1583
|
+
plan_rel = None
|
|
1584
|
+
# F-056: an ANALYZED row whose reference cannot be evaluated is UNVERIFIED,
|
|
1585
|
+
# never fresh. Each `continue` below that skips such a row records it here;
|
|
1586
|
+
# skipping silently is how a pruned reference printed `[ok]` over three areas.
|
|
1587
|
+
unverifiable = []
|
|
1546
1588
|
stale = []
|
|
1547
1589
|
for row in rows:
|
|
1548
1590
|
if row["status"] != "ANALYZED":
|
|
@@ -1561,16 +1603,42 @@ def cmd_stale(root, hybrid=False):
|
|
|
1561
1603
|
print(f"[warn] {rel}: path does not exist")
|
|
1562
1604
|
continue
|
|
1563
1605
|
changed = []
|
|
1564
|
-
|
|
1606
|
+
remark = f"re-analyze the area, then run: {entry} mark {rel}"
|
|
1607
|
+
if re.fullmatch(r"[0-9a-fA-F]{7,40}", ref or ""):
|
|
1608
|
+
if not use_git:
|
|
1609
|
+
print(f"[stale] {rel}: reference '{ref}' is a git commit, but git is not usable "
|
|
1610
|
+
"here (not a git work tree, git missing, or the repository refused): its "
|
|
1611
|
+
"freshness cannot be evaluated. Run stale inside the git work tree, or "
|
|
1612
|
+
+ remark)
|
|
1613
|
+
unverifiable.append(rel)
|
|
1614
|
+
continue
|
|
1615
|
+
state = git_ref_state(root, ref)
|
|
1616
|
+
if state == "missing":
|
|
1617
|
+
shallow = (" This clone is shallow: full history may hold it "
|
|
1618
|
+
"(git fetch --unshallow)." if git_is_shallow(root) else "")
|
|
1619
|
+
print(f"[stale] {rel}: git ref '{ref}' is not a commit in this repository "
|
|
1620
|
+
"(amended, rebased or squash-merged away and pruned, or never fetched "
|
|
1621
|
+
f"here): its freshness cannot be evaluated.{shallow} To fix, " + remark)
|
|
1622
|
+
unverifiable.append(rel)
|
|
1623
|
+
continue
|
|
1624
|
+
if state == "orphan":
|
|
1625
|
+
print(f"[warn] {rel}: git ref '{ref}' is not an ancestor of HEAD (amended, "
|
|
1626
|
+
"rebased, squash-merged, or marked on another branch): it stops "
|
|
1627
|
+
"resolving once git prunes it, and on any fresh clone. Re-mark now: "
|
|
1628
|
+
f"{entry} mark {rel}")
|
|
1565
1629
|
res = git_changed_since(root, ref, rel.replace("\\", "/"))
|
|
1566
1630
|
if res is None:
|
|
1567
|
-
print(f"[
|
|
1631
|
+
print(f"[stale] {rel}: git could not compare against ref '{ref}': its "
|
|
1632
|
+
"freshness cannot be evaluated. To fix, " + remark)
|
|
1633
|
+
unverifiable.append(rel)
|
|
1568
1634
|
continue
|
|
1569
1635
|
changed = res
|
|
1570
1636
|
else:
|
|
1571
1637
|
ts = parse_iso(ref)
|
|
1572
1638
|
if ts is None:
|
|
1573
|
-
print(f"[
|
|
1639
|
+
print(f"[stale] {rel}: reference '{ref}' is neither a git commit nor an ISO "
|
|
1640
|
+
"UTC timestamp: its freshness cannot be evaluated. To fix, " + remark)
|
|
1641
|
+
unverifiable.append(rel)
|
|
1574
1642
|
continue
|
|
1575
1643
|
for fp in iter_files(target):
|
|
1576
1644
|
mtime = datetime.fromtimestamp(fp.stat().st_mtime, tz=timezone.utc)
|
|
@@ -1580,11 +1648,15 @@ def cmd_stale(root, hybrid=False):
|
|
|
1580
1648
|
except ValueError: # symlink out of the tree: report absolute, never crash
|
|
1581
1649
|
name = str(fp)
|
|
1582
1650
|
changed.append(name)
|
|
1651
|
+
changed = [c for c in changed if c != plan_rel]
|
|
1583
1652
|
if changed:
|
|
1584
1653
|
stale.append((rel, changed))
|
|
1585
1654
|
|
|
1655
|
+
if unverifiable:
|
|
1656
|
+
rc = 1 # unverified is not fresh (F-056)
|
|
1586
1657
|
if not stale:
|
|
1587
|
-
|
|
1658
|
+
if not unverifiable:
|
|
1659
|
+
print("[ok] no analyzed area was modified after its last recorded analysis.")
|
|
1588
1660
|
return rc # was: return 0 — MUST carry guide rc
|
|
1589
1661
|
print("Areas modified after the last recorded analysis:")
|
|
1590
1662
|
for rel, changed in stale:
|
|
@@ -1593,7 +1665,7 @@ def cmd_stale(root, hybrid=False):
|
|
|
1593
1665
|
print(f" - {c}")
|
|
1594
1666
|
if len(changed) > 10:
|
|
1595
1667
|
print(f" ... and {len(changed) - 10} more")
|
|
1596
|
-
print("\nAfter re-analyzing, record it with:
|
|
1668
|
+
print(f"\nAfter re-analyzing, record it with: {entry} mark <path>")
|
|
1597
1669
|
return 1 # stale areas dominate: rc already implied
|
|
1598
1670
|
|
|
1599
1671
|
|
|
@@ -1645,6 +1717,13 @@ def cmd_mark(root, paths):
|
|
|
1645
1717
|
|
|
1646
1718
|
f.parent.mkdir(parents=True, exist_ok=True)
|
|
1647
1719
|
f.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
1720
|
+
if use_git_ref:
|
|
1721
|
+
# F-056: the reference is born here, so this is where the gestures that
|
|
1722
|
+
# orphan it are named. An amend was the observed one.
|
|
1723
|
+
print(f"[info] {ref} is the current HEAD. Commit this audit plan change as a new "
|
|
1724
|
+
"commit: amending, rebasing or squash-merging that commit orphans the "
|
|
1725
|
+
f"reference, and `{entry_script()} stale` fails once it no longer resolves. "
|
|
1726
|
+
"After a squash or rebase merge, re-mark on the integration branch.")
|
|
1648
1727
|
return 0
|
|
1649
1728
|
|
|
1650
1729
|
|
|
@@ -436,7 +436,7 @@ as pending. A ledger id absent from the plan is a non-fatal orphan warning.
|
|
|
436
436
|
|
|
437
437
|
## ai_docs/audit/audit_plan.md (Standalone mode only)
|
|
438
438
|
|
|
439
|
-
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`.
|
|
439
|
+
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`. Commit the `mark` result as a new commit: amending, rebasing or squash-merging the commit it recorded orphans the reference, and `stale` fails once it no longer resolves — after a squash or rebase merge, re-mark on the integration branch.
|
|
440
440
|
|
|
441
441
|
```markdown
|
|
442
442
|
# Audit Plan
|