@antoneeo/mkt-agentic-sdlc-skill 0.6.0 → 0.8.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.
@@ -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