@antoneeo/agentic-sdlc-skill 1.34.0 → 1.35.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.
@@ -1,80 +1,71 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """Agentic SDLC — the CODE domain entry point.
4
-
5
- Thin by design. Every behaviour lives in `sdlc_core.py`, the spine shipped
6
- verbatim in every distribution of the family; this file only says which domain
7
- this distribution implements and which portable checks it exposes. Command
8
- names, flags, output and exit codes are unchanged — an existing project sees the
9
- same tool it always had.
10
-
11
- Both files must sit in the same directory. If you copy the validator into a CI
12
- image, copy BOTH (`ENFORCEMENT.md` §2 has the recipe); copying this one alone
13
- fails at import, loudly and immediately, which is the intended failure.
14
-
15
- Usage is `sdlc_core.py`'s: check / validate / index / stale / mark / gate /
16
- orient / benefit / plan.
17
- """
18
- import sys
19
- from pathlib import Path
20
-
21
- sys.path.insert(0, str(Path(__file__).resolve().parent))
22
-
23
- try:
24
- import sdlc_core
25
- except ImportError as exc: # pragma: no cover - exercised by TS12, not by unit tests
26
- sys.stderr.write(
27
- "[ERROR] sdlc_check.py cannot find sdlc_core.py next to it: " + str(exc) + "\n"
28
- " The validator ships as TWO files since the multi-domain core.\n"
29
- " Copy both, or run sdlc_core.py directly.\n")
30
- sys.exit(1)
31
-
32
- # Re-export the core's surface: existing importers (`import sdlc_check as sc`)
33
- # and the test batteries reach for these names on this module.
34
- from sdlc_core import * # noqa: F401,F403
35
- from sdlc_core import _map_refs # noqa: F401 underscore helper used by the batteries
36
-
37
- # The domain this distribution implements. It does NOT decide any document's
38
- # owning domain -- that is resolved per project (`default_domain:` in the docs
39
- # root's README) and per artifact (`domain:`), so the same tree gets the same
40
- # verdict from every installed distribution. What it decides is which portable
41
- # checks a document may import here by name; the rest warn as unavailable.
42
- DOMAIN = "code"
43
-
44
- sdlc_core.set_entry_point(DOMAIN, provides=("code", "knowledge"))
45
-
46
- # What this distribution carries. The shared battery reads it; the spine
47
- # capabilities are not optional, and a shared test refuses a profile that drops one.
48
- sdlc_core.set_profile(
49
- skill_name="agentic-sdlc",
50
- unit_noun="feature",
51
- support_files=("templates.md", "architect.md", "guides.md", "vision.md", "tdd.md",
52
- "debugging.md", "elicitation.md", "review.md", "dispatch.md",
53
- "routing.md", "hybrid.md", "ENFORCEMENT.md"),
54
- capabilities=(
55
- # spine
56
- "triage", "write_triggers", "workstream_registry", "vision_gate",
57
- "design_review_gate", "guide_router", "worktree_hygiene",
58
- # code overlay
59
- "architect_pass", "interaction_contract", "comprehension_guides", "tdd",
60
- "subagent_dispatch", "legacy_narrative_handoff", "question_discipline",
61
- ),
62
- design_gate_between=("### 3. Request Analysis", "### 4. Development and Testing"),
63
- remind_line=(
64
- "first decide if this skill governs the work; if yes, LOAD it once -- an "
65
- "unread protocol cannot be applied. Declare the level (L1/L2/L3/Spike) "
66
- "and the guide-router verdict before acting; an imperative is a request "
67
- "to triage. Name the mechanism before fixing the first defect you can "
68
- "measure -- symptom patches recur and stack. Replay governing text -- "
69
- "doctrine, a prompt, a schema -- against the case that motivated it: red "
70
- "under the old text, green under the new."
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,11 @@ 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
+ # F-056: an ANALYZED row whose reference cannot be evaluated is UNVERIFIED,
1578
+ # never fresh. Each `continue` below that skips such a row records it here;
1579
+ # skipping silently is how a pruned reference printed `[ok]` over three areas.
1580
+ unverifiable = []
1546
1581
  stale = []
1547
1582
  for row in rows:
1548
1583
  if row["status"] != "ANALYZED":
@@ -1561,16 +1596,42 @@ def cmd_stale(root, hybrid=False):
1561
1596
  print(f"[warn] {rel}: path does not exist")
1562
1597
  continue
1563
1598
  changed = []
1564
- if use_git and re.fullmatch(r"[0-9a-fA-F]{7,40}", ref or ""):
1599
+ remark = f"re-analyze the area, then run: {entry} mark {rel}"
1600
+ if re.fullmatch(r"[0-9a-fA-F]{7,40}", ref or ""):
1601
+ if not use_git:
1602
+ print(f"[stale] {rel}: reference '{ref}' is a git commit, but git is not usable "
1603
+ "here (not a git work tree, git missing, or the repository refused): its "
1604
+ "freshness cannot be evaluated. Run stale inside the git work tree, or "
1605
+ + remark)
1606
+ unverifiable.append(rel)
1607
+ continue
1608
+ state = git_ref_state(root, ref)
1609
+ if state == "missing":
1610
+ shallow = (" This clone is shallow: full history may hold it "
1611
+ "(git fetch --unshallow)." if git_is_shallow(root) else "")
1612
+ print(f"[stale] {rel}: git ref '{ref}' is not a commit in this repository "
1613
+ "(amended, rebased or squash-merged away and pruned, or never fetched "
1614
+ f"here): its freshness cannot be evaluated.{shallow} To fix, " + remark)
1615
+ unverifiable.append(rel)
1616
+ continue
1617
+ if state == "orphan":
1618
+ print(f"[warn] {rel}: git ref '{ref}' is not an ancestor of HEAD (amended, "
1619
+ "rebased, squash-merged, or marked on another branch): it stops "
1620
+ "resolving once git prunes it, and on any fresh clone. Re-mark now: "
1621
+ f"{entry} mark {rel}")
1565
1622
  res = git_changed_since(root, ref, rel.replace("\\", "/"))
1566
1623
  if res is None:
1567
- print(f"[warn] {rel}: git ref '{ref}' unresolvable, cannot evaluate")
1624
+ print(f"[stale] {rel}: git could not compare against ref '{ref}': its "
1625
+ "freshness cannot be evaluated. To fix, " + remark)
1626
+ unverifiable.append(rel)
1568
1627
  continue
1569
1628
  changed = res
1570
1629
  else:
1571
1630
  ts = parse_iso(ref)
1572
1631
  if ts is None:
1573
- print(f"[warn] {rel}: reference '{ref}' not parseable (neither git hash nor ISO UTC)")
1632
+ print(f"[stale] {rel}: reference '{ref}' is neither a git commit nor an ISO "
1633
+ "UTC timestamp: its freshness cannot be evaluated. To fix, " + remark)
1634
+ unverifiable.append(rel)
1574
1635
  continue
1575
1636
  for fp in iter_files(target):
1576
1637
  mtime = datetime.fromtimestamp(fp.stat().st_mtime, tz=timezone.utc)
@@ -1583,8 +1644,11 @@ def cmd_stale(root, hybrid=False):
1583
1644
  if changed:
1584
1645
  stale.append((rel, changed))
1585
1646
 
1647
+ if unverifiable:
1648
+ rc = 1 # unverified is not fresh (F-056)
1586
1649
  if not stale:
1587
- print("[ok] no analyzed area was modified after its last recorded analysis.")
1650
+ if not unverifiable:
1651
+ print("[ok] no analyzed area was modified after its last recorded analysis.")
1588
1652
  return rc # was: return 0 — MUST carry guide rc
1589
1653
  print("Areas modified after the last recorded analysis:")
1590
1654
  for rel, changed in stale:
@@ -1593,7 +1657,7 @@ def cmd_stale(root, hybrid=False):
1593
1657
  print(f" - {c}")
1594
1658
  if len(changed) > 10:
1595
1659
  print(f" ... and {len(changed) - 10} more")
1596
- print("\nAfter re-analyzing, record it with: sdlc_check.py mark <path>")
1660
+ print(f"\nAfter re-analyzing, record it with: {entry} mark <path>")
1597
1661
  return 1 # stale areas dominate: rc already implied
1598
1662
 
1599
1663
 
@@ -1645,6 +1709,13 @@ def cmd_mark(root, paths):
1645
1709
 
1646
1710
  f.parent.mkdir(parents=True, exist_ok=True)
1647
1711
  f.write_text("\n".join(lines) + "\n", encoding="utf-8")
1712
+ if use_git_ref:
1713
+ # F-056: the reference is born here, so this is where the gestures that
1714
+ # orphan it are named. An amend was the observed one.
1715
+ print(f"[info] {ref} is the current HEAD. Commit this audit plan change as a new "
1716
+ "commit: amending, rebasing or squash-merging that commit orphans the "
1717
+ f"reference, and `{entry_script()} stale` fails once it no longer resolves. "
1718
+ "After a squash or rebase merge, re-mark on the integration branch.")
1648
1719
  return 0
1649
1720
 
1650
1721
 
@@ -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