@antoneeo/agentic-sdlc-skill 1.28.0 → 1.30.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.
@@ -94,6 +94,23 @@ Hybrid project's devPNT gates write to the same file.
94
94
  The log is how the gate's value is measured over time; skipping it makes the gate
95
95
  unfalsifiable, the same defect as an unnamed EXISTS or a faked router verdict.
96
96
 
97
+ **Revise means converge, not accrete.** Two revision failure modes generate their
98
+ own findings and can make the cap unreachable honestly. (1) *Patch-on-patch*:
99
+ each round's fixes are grafted onto the previous text until stale counts,
100
+ duplicated sections and internal contradictions are findings the revisions
101
+ themselves created. When a round's findings are of that artifact-consistency
102
+ class — and at the latest before a round-3 attempt — rewrite the artifact lean
103
+ and operative against the checklist instead of patching again; a from-scratch
104
+ rewrite is cheaper than a burned round. (2) *Revision narrative in the
105
+ artifact*: the artifact under review carries its own review archaeology
106
+ ("v1.2 fixed…", "as the round-2 review noted…"). An artifact states what IS,
107
+ once; how it got there lives in the REVIEW_LOG row's notes, never in the
108
+ artifact — archaeology bloats the object under review and every stale
109
+ self-reference is a future finding. Field measurement (2026-08): an artifact
110
+ that had grown to ~1,250 lines across eight failing rounds passed on the round
111
+ after a ~500-line operative rewrite moved the history out — the rewrite removed
112
+ an entire finding class.
113
+
97
114
  **The reviewer is read-only and advisory.** It never edits, never commits, never
98
115
  marks anything DONE, and a PASS is not an approval to merge — the human owns that.
99
116
 
@@ -124,6 +141,26 @@ When you hand work to a reviewer (human or agent), give them:
124
141
  should have been. The design reviewer checks that every threat surface the change
125
142
  touches has a matching security requirement.
126
143
 
144
+ - **The severity contract**: state with the request what counts as a blocker
145
+ versus a lesser finding. A blocker is *unfit-to-proceed* evidence only — a
146
+ false claim about existing code, a published falsehood, a missing impacted
147
+ file, an unresolvable internal contradiction, a real regression; an
148
+ improvement idea, a wish, or a style preference is at most a WARN and never
149
+ fails the artifact. An uncalibrated "hunt hard" mandate produces
150
+ blocker-inflated rounds that fail artifacts on wishes — each one burns a
151
+ round from the cap of 3 and patches the artifact for prose, the accretion
152
+ failure the round-cap rule names. Stating the taxonomy calibrates the
153
+ *scale*; it never pre-judges any specific finding (that stays forbidden
154
+ below) — the reviewer still decides where every finding lands.
155
+
156
+ **Pre-audit before you request.** Before handing the artifact over, run a
157
+ scripted author pre-audit: mechanically resolve every `file:line` / symbol
158
+ anchor the artifact cites against the real tree, and recount every count it
159
+ states — with a script, not by eye. Anchors rot between drafts, and a reviewer
160
+ that trips on a broken anchor spends its round proving your citation instead of
161
+ your design. Measured cost is near zero (one grep-loop); measured catch: four
162
+ rotted anchors in a single artifact, pre-review.
163
+
127
164
  **The verdict travels back as the reviewer's own final output** — the text it
128
165
  returns when it finishes, nothing else. A reviewer that tries to message the
129
166
  requester mid-run depends on a delivery channel it cannot verify (a subagent
@@ -301,6 +338,20 @@ When you are the reviewer:
301
338
  NOT checked here — architecture-awareness is the Capability Ledger / Impact
302
339
  review's job; a clause for it would duplicate that.) A lens whose template
303
340
  defines no such section (knowledge, marketing) never fires this clause.
341
+ - **Behavioural-claim probes (same reviews; fires only in the lens whose
342
+ SKILL.md defines Execute-Before-Specify — the code lens today; that paragraph
343
+ is the owning definition — cite it, never restate it).** On an L3 design
344
+ artifact, a claim of the form "the code today does X" must trace to an
345
+ assertion in the artifact's shipped harness
346
+ (`ai_docs/solutions/harness_[feature]/`): the check is mechanical — re-run
347
+ the harness and compare, instead of re-deriving the claims by reading. Three
348
+ findings live here and nowhere else: a behavioural claim backed by no probe
349
+ (report it as unproven — reading the source yourself is the same
350
+ insufficient evidence the rule bans for the author); a harness absent while
351
+ the prose carries behavioural claims; and a probe that no longer passes
352
+ against the current tree. Structural facts (a file exists, a symbol's
353
+ signature, its callers) are the blast-radius clause's territory, never probe
354
+ findings. A lens whose SKILL.md defines no such duty never fires this clause.
304
355
 
305
356
  ## Anti-patterns
306
357
 
@@ -1646,6 +1646,143 @@ def cmd_mark(root, paths):
1646
1646
  return 0
1647
1647
 
1648
1648
 
1649
+ # ------------------------------------------------- orientation-hook wiring
1650
+ # F-041: read-only mirror of the installer-side detection in lib.js
1651
+ # (findOrientHook + orientHookValidator). The JS owns WRITING hooks, with its
1652
+ # injection guards; this side only DETECTS, at `check`, so a project whose
1653
+ # sessions start unoriented hears about it in-project (ENFORCEMENT par.4: the
1654
+ # hook is wired at init only -- pre-F-036 and agent-bootstrapped projects never
1655
+ # got it, and a wired-but-dead hook is "the worst of the three states").
1656
+ # The file list is a SUPERSET of the JS one: .codex/hooks.json is documented in
1657
+ # ENFORCEMENT par.4 but never written by init. Pinned equal on both sides:
1658
+ # the entry-point names, and (F-042) the user-settings path derivation --
1659
+ # CLAUDE_CONFIG_DIR when set, else <home>/.claude -- empty-string env falsy
1660
+ # on both sides.
1661
+ ORIENT_HOOK_ENTRY_POINTS = ("sdlc_check.py", "mkt_check.py")
1662
+ ORIENT_HOOK_CONFIG_FILES = (".claude/settings.json",
1663
+ ".claude/settings.local.json",
1664
+ ".codex/hooks.json")
1665
+ ORIENT_HOOK_CONFIG_CAP = 1024 * 1024 # settings are tiny; cap pathological files
1666
+ _ORIENT_HOOK_CMD_RE = re.compile(r" orient(\s|$)")
1667
+ _ORIENT_HOOK_TOKEN_RE = re.compile(r'"([^"]*)"|(\S+)')
1668
+
1669
+
1670
+ def _orient_entries_in(p):
1671
+ """Yield the SessionStart commands invoking an orient entry point from ONE
1672
+ client config file. Fail-open: an unreadable, oversized, malformed or
1673
+ directory-shaped file contributes nothing. Reads are utf-8-sig -- a BOM'd
1674
+ settings.json (PowerShell/Notepad) must not read as unwired (declared
1675
+ divergence from Node's JSON.parse)."""
1676
+ try:
1677
+ if not p.is_file() or p.stat().st_size > ORIENT_HOOK_CONFIG_CAP:
1678
+ return
1679
+ data = json.loads(p.read_text(encoding="utf-8-sig"))
1680
+ except Exception:
1681
+ return
1682
+ if not isinstance(data, dict):
1683
+ return
1684
+ hooks = data.get("hooks")
1685
+ groups = hooks.get("SessionStart") if isinstance(hooks, dict) else None
1686
+ if groups is None:
1687
+ groups = data.get("SessionStart") # the .codex/hooks.json shape
1688
+ if not isinstance(groups, list):
1689
+ return
1690
+ for group in groups:
1691
+ if not isinstance(group, dict):
1692
+ continue
1693
+ inner = group.get("hooks")
1694
+ for h in inner if isinstance(inner, list) else [group]:
1695
+ cmd = h.get("command") if isinstance(h, dict) else None
1696
+ if (isinstance(cmd, str)
1697
+ and any(n in cmd for n in ORIENT_HOOK_ENTRY_POINTS)
1698
+ and _ORIENT_HOOK_CMD_RE.search(cmd)):
1699
+ yield cmd
1700
+
1701
+
1702
+ def _orient_user_settings_path():
1703
+ """The machine-global settings file (F-042 wires it at install time).
1704
+
1705
+ Read at CALL time -- a DECLARED divergence from AGENTIC_SDLC_KB_ROOT's
1706
+ import-time read: unittest imports this module once before any test runs,
1707
+ so an import-time seam would be frozen before the matrix could set it.
1708
+ AGENTIC_SDLC_USER_SETTINGS is the test-isolation seam (python-side only);
1709
+ the production derivation mirrors lib.js: CLAUDE_CONFIG_DIR || ~/.claude."""
1710
+ seam = os.environ.get("AGENTIC_SDLC_USER_SETTINGS")
1711
+ if seam:
1712
+ return Path(seam)
1713
+ cfg = os.environ.get("CLAUDE_CONFIG_DIR")
1714
+ return (Path(cfg) if cfg else Path.home() / ".claude") / "settings.json"
1715
+
1716
+
1717
+ def _orient_hook_commands(root):
1718
+ """Yield (command, at_user_level) across every known client config: the
1719
+ three project-relative files, then the machine-global user settings --
1720
+ the file the installer wires (F-042), without which a fixed machine's
1721
+ projects would read as unwired forever."""
1722
+ for rel in ORIENT_HOOK_CONFIG_FILES:
1723
+ for cmd in _orient_entries_in(Path(root) / rel):
1724
+ yield cmd, False
1725
+ for cmd in _orient_entries_in(_orient_user_settings_path()):
1726
+ yield cmd, True
1727
+
1728
+
1729
+ def orient_hook_state(root):
1730
+ """'wired' | 'dead' | 'unwired'.
1731
+
1732
+ A matching entry whose tokens name no validator is "cannot tell" and counts
1733
+ as wired (the lib.js contract: never report broken on cannot-tell). 'dead'
1734
+ requires every matching entry to name a validator that does not resolve:
1735
+ the client runs ALL SessionStart hooks -- project-level AND user-level
1736
+ merge -- so one live entry at either level still orients the session
1737
+ (any-resolves aggregation across levels). Relative tokens resolve against
1738
+ the project root (the vendored-validator convention); a USER-level relative
1739
+ token has no cwd to resolve against and is treated as cannot-tell."""
1740
+ matched = False
1741
+ for cmd, at_user_level in _orient_hook_commands(root):
1742
+ matched = True
1743
+ token = None
1744
+ for m in _ORIENT_HOOK_TOKEN_RE.finditer(cmd):
1745
+ tok = m.group(1) if m.group(1) is not None else m.group(2)
1746
+ if any(tok.endswith(n) for n in ORIENT_HOOK_ENTRY_POINTS):
1747
+ token = tok
1748
+ break
1749
+ if token is None:
1750
+ return "wired"
1751
+ tp = Path(token)
1752
+ if not tp.is_absolute():
1753
+ if at_user_level:
1754
+ return "wired" # cannot tell: no cwd exists at user level
1755
+ tp = Path(root) / tp
1756
+ if tp.exists():
1757
+ return "wired"
1758
+ return "dead" if matched else "unwired"
1759
+
1760
+
1761
+ def print_orient_hook_note(root):
1762
+ """Print the FS-A wiring note (unwired / dead), or nothing when wired.
1763
+
1764
+ Informational only, by contract: never an exit code, never a validate
1765
+ warning (--strict CI must not fail on a hook that legitimately lives in
1766
+ git-ignored settings.local.json). Called by the spine's cmd_check AND by
1767
+ an overlay that replaces check with its own pipeline (the marketing lens
1768
+ does): the note is check-layer behaviour, whoever owns the check."""
1769
+ try:
1770
+ state = orient_hook_state(root)
1771
+ if state == "unwired":
1772
+ print("[note] agent sessions start without automatic orientation on this "
1773
+ "machine (no session-orientation hook found at project or user level). "
1774
+ "Fix once: update or reinstall the skill npm package (on Claude Code "
1775
+ "with Python it wires the hook machine-wide), or wire it per "
1776
+ "ENFORCEMENT.md par.4")
1777
+ elif state == "dead":
1778
+ print("[note] a session-orientation hook is configured but points at a "
1779
+ "validator that no longer exists, so sessions start unoriented. Fix: "
1780
+ "update or reinstall the skill npm package, or correct the command. "
1781
+ "Details: ENFORCEMENT.md par.4")
1782
+ except Exception:
1783
+ pass # detection must never break check
1784
+
1785
+
1649
1786
  def cmd_check(root, strict=False, hybrid=False):
1650
1787
  print("===== validate =====")
1651
1788
  rc_v = cmd_validate(root, strict=strict, hybrid=hybrid)
@@ -1653,6 +1790,7 @@ def cmd_check(root, strict=False, hybrid=False):
1653
1790
  rc_s = cmd_stale(root, hybrid=hybrid)
1654
1791
  print(f"\ncheck: {'CLEAN' if not (rc_v or rc_s) else 'NOT CLEAN'} "
1655
1792
  f"(validate rc={rc_v}, stale rc={rc_s})")
1793
+ print_orient_hook_note(root)
1656
1794
  return 1 if (rc_v or rc_s) else 0
1657
1795
 
1658
1796