riskrails 4.1.0 → 4.3.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.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +25 -15
  5. package/agents/ai-reviewer.md.tmpl +14 -1
  6. package/ai/agents/implementer.md +6 -0
  7. package/ai/agents/reviewer.md +8 -1
  8. package/bin/ansi.js +12 -0
  9. package/bin/menu.js +71 -12
  10. package/bin/riskrails.js +240 -14
  11. package/bin/uninstall.js +115 -0
  12. package/bin/wizard.js +72 -0
  13. package/hooks/AGENTS.md +0 -6
  14. package/hooks/CLAUDE.md +0 -6
  15. package/hooks/context-guard.py +11 -1
  16. package/hooks/dialog-gate.py +3 -2
  17. package/hooks/guard/decision.py +4 -3
  18. package/hooks/guard/dialog.py +12 -2
  19. package/hooks/guard/ere.py +29 -10
  20. package/hooks/guard/git.py +2 -1
  21. package/hooks/guard/paths.py +48 -1406
  22. package/hooks/guard/paths_base.py +66 -0
  23. package/hooks/guard/paths_bash.py +176 -0
  24. package/hooks/guard/paths_classify.py +205 -0
  25. package/hooks/guard/paths_fs.py +137 -0
  26. package/hooks/guard/paths_human.py +105 -0
  27. package/hooks/guard/paths_interp.py +162 -0
  28. package/hooks/guard/paths_movers.py +393 -0
  29. package/hooks/guard/paths_shell.py +288 -0
  30. package/hooks/runtime-gate.py +87 -0
  31. package/hooks/update_notice.py +23 -8
  32. package/package.json +1 -1
  33. package/skills/AGENTS.md +0 -6
  34. package/skills/CLAUDE.md +0 -6
  35. package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
  36. package/skills/ai-init/templates/gitignore.snippet +2 -0
  37. package/skills/ai-task/memory_cleanup.py +213 -0
  38. package/skills/ai-task/memory_compact.py +25 -0
  39. package/skills/ai-task/memory_config.py +120 -0
  40. package/skills/ai-task/memory_export.py +246 -0
  41. package/skills/ai-task/memory_format.py +7 -3
  42. package/skills/ai-task/memory_hook.py +172 -0
  43. package/skills/ai-task/memory_io.py +122 -0
  44. package/skills/ai-task/memory_migrate.py +187 -0
  45. package/skills/ai-task/memory_packages.py +83 -0
  46. package/skills/ai-task/memory_risks.py +84 -0
  47. package/skills/ai-task/memory_status.py +76 -0
  48. package/skills/ai-task/memory_sync.py +20 -1481
  49. package/skills/ai-task/memory_verify.py +316 -0
  50. package/skills/ai-task/rr_state/cli.py +1 -0
  51. package/skills/ai-task/rr_state/close.py +2 -2
  52. package/skills/ai-task/rr_state/dialog.py +32 -7
  53. package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
  54. package/skills/ai-task/rr_state/gate.py +10 -4
  55. package/skills/ai-task/rr_state/labels.py +33 -4
  56. package/skills/ai-task/rr_state/memory_cmd.py +84 -64
  57. package/skills/ai-task/rr_state/questions_cmd.py +5 -4
  58. package/skills/ai-task/rr_state/report.py +73 -10
  59. package/skills/ai-task/rr_state/report_cmd.py +2 -0
  60. package/skills/ai-task/rr_state/review.py +18 -14
  61. package/skills/ai-task/rr_state/start.py +12 -9
  62. package/skills/ai-task/rr_state/steps.py +16 -7
  63. package/skills/ai-task/rr_state/verify.py +17 -12
  64. package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
  65. package/skills/project-update/history/blobs/fa0cb370b024e1a30e1355912032657b08f609e47773a603c193b1e609b0f997 +8 -0
  66. package/skills/project-update/history/index.json +8 -4
  67. package/skills/project-update/rr_update/__init__.py +1 -0
  68. package/skills/project-update/rr_update/apply.py +188 -0
  69. package/skills/project-update/rr_update/blocks.py +38 -0
  70. package/skills/project-update/rr_update/build.py +96 -0
  71. package/skills/project-update/rr_update/cleanup.py +194 -0
  72. package/skills/project-update/rr_update/cli.py +102 -0
  73. package/skills/project-update/rr_update/files.py +106 -0
  74. package/skills/project-update/rr_update/gate.py +60 -0
  75. package/skills/project-update/rr_update/hints.py +128 -0
  76. package/skills/project-update/rr_update/history.py +117 -0
  77. package/skills/project-update/rr_update/kept.py +120 -0
  78. package/skills/project-update/rr_update/paths.py +108 -0
  79. package/skills/project-update/rr_update/plan.py +123 -0
  80. package/skills/project-update/rr_update/report.py +51 -0
  81. package/skills/project-update/rr_update/rules.py +178 -0
  82. package/skills/project-update/rr_update/shared.py +74 -0
  83. package/skills/project-update/rr_update/siblings.py +37 -0
  84. package/skills/project-update/rr_update/weights.py +171 -0
  85. package/skills/project-update/update.py +8 -1682
  86. package/skills/rr-build/SKILL.md +5 -2
  87. package/skills/rr-setup/SKILL.md +4 -2
  88. package/skills/rr-setup/setup.py +13 -12
  89. package/skills/rr-ship/SKILL.md +2 -2
  90. package/skills/rr-spec/SKILL.md +6 -0
  91. package/skills/rr-status/SKILL.md +2 -0
  92. package/skills/rr-status/status.py +5 -3
@@ -45,6 +45,7 @@ INTERPRETER_RE = re.compile(r"^python3?$")
45
45
  INTERPRETER_FLAG_RE = re.compile(r"^-[IS]+$")
46
46
  PUSH_REF_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
47
47
  NPX_PACKAGE_RE = re.compile(r"^riskrails@[0-9A-Za-z.+-]+$")
48
+ BATCH_ON = {"--by": True, "--why": False} # `batch on` options, each at most once; True = required. rr_state/labels.py keeps the same table
48
49
  TS = "%Y-%m-%dT%H:%M:%SZ"
49
50
 
50
51
  # hooks/guard/dialog.py -> the plugin root; the Stop reminder names its
@@ -217,12 +218,12 @@ def validate_label(label):
217
218
  if state_label_ok(tokens):
218
219
  return None
219
220
  return ("a label starts with git commit, git push, a human-only state.py command (approve, reject, "
220
- "profile set, verify/review/start --by, memory enable) or npx riskrails@<v>")
221
+ "profile set, verify/review/start --by, batch on --by, memory enable) or npx riskrails@<v>")
221
222
 
222
223
 
223
224
  def state_label_ok(tokens):
224
225
  """python3 [-I|-S…] <…/state.py> approve|reject|profile set|verify --by|
225
- review --by|start --mode … --by|memory enable [--author S] [--period month|quarter|year]
226
+ review --by|start --mode … --by|batch on --by S [--why S]|memory enable [--author S] [--period month|quarter|year]
226
227
  [--index none|sqlite]: each option at most once, as two words, in any order, and no other
227
228
  memory verb. rr_state/labels.py keeps the same rule; tests/test_dialog_contract.py pins the
228
229
  two equal."""
@@ -236,6 +237,11 @@ def state_label_ok(tokens):
236
237
  rest = tokens[i + 1:]
237
238
  while rest[:1] in (["--root"], ["--runtime"]) and len(rest) >= 2:
238
239
  rest = rest[2:]
240
+ if rest[:2] == ["batch", "on"]:
241
+ names, values = rest[2::2], rest[3::2]
242
+ return (len(names) == len(values) == len(set(names)) and set(names) <= set(BATCH_ON)
243
+ and all(n in names for n, need in BATCH_ON.items() if need)
244
+ and all(v[:1] not in ("", "-") for v in values))
239
245
  if rest[:1] in (["approve"], ["reject"]) or rest[:2] == ["profile", "set"]:
240
246
  return True
241
247
  if rest[:1] in (["verify"], ["review"]):
@@ -589,6 +595,10 @@ def is_git_label(label):
589
595
  return (label or "").split()[:1] == ["git"]
590
596
 
591
597
 
598
+ def is_npx_label(label):
599
+ return (label or "").split()[:1] == ["npx"]
600
+
601
+
592
602
  def used_note(pick):
593
603
  """What a deny after the gate spent a git pick adds (review focus 1)."""
594
604
  return ("\n\nThe pick %s was used by this call and is spent; the command did not run. To ask the human "
@@ -528,11 +528,18 @@ class _Found: # pylint: disable=too-few-public-met
528
528
 
529
529
 
530
530
  class Compiled:
531
- """One pattern in one mode. `rx` is None when the pattern is invalid."""
532
- __slots__ = ("rx", "required", "anchored", "dfa")
531
+ """One pattern in one mode. `rx` is None when the pattern is invalid.
532
+
533
+ The pattern is parsed here (parse errors are caught here), but the regex
534
+ and the DFA are built on the first read of `rx`: `search` runs the cheap
535
+ `required` prefilter first, so a subject it rejects never pays for
536
+ `re.compile` (a fresh process holds ~34 patterns, most never get past it).
537
+ """
538
+ __slots__ = ("_rx", "_pending", "required", "anchored", "dfa")
533
539
 
534
540
  def __init__(self, pattern, mode, capture):
535
- self.rx = None
541
+ self._rx = None
542
+ self._pending = None # (tree, mode, capture) until built
536
543
  self.required = None
537
544
  self.anchored = False
538
545
  self.dfa = None
@@ -546,18 +553,30 @@ class Compiled:
546
553
  tree = _strip_leading(tree)
547
554
  elif capture:
548
555
  self.anchored = _starts_with_dotstar(tree)
549
- flags = re.ASCII if mode == "b" else 0
550
- self.rx = re.compile(_emit(tree, mode), flags)
551
- if not capture and _nested(tree) and _dfa_ok(tree):
552
- self.dfa = _Dfa(tree, mode)
556
+ self._pending = (tree, mode, capture)
553
557
  except (EreError, re.error, RecursionError, OverflowError):
554
- self.rx = None
558
+ self._pending = None
559
+
560
+ @property
561
+ def rx(self):
562
+ if self._pending is not None:
563
+ tree, mode, capture = self._pending
564
+ self._pending = None
565
+ try:
566
+ flags = re.ASCII if mode == "b" else 0
567
+ self._rx = re.compile(_emit(tree, mode), flags)
568
+ if not capture and _nested(tree) and _dfa_ok(tree):
569
+ self.dfa = _Dfa(tree, mode)
570
+ except (EreError, re.error, RecursionError, OverflowError):
571
+ self._rx = None
572
+ self.dfa = None
573
+ return self._rx
555
574
 
556
575
  def search(self, subject):
557
- if self.rx is None:
558
- return None
559
576
  if self.required is not None and not any(r in subject for r in self.required):
560
577
  return None
578
+ if self.rx is None:
579
+ return None
561
580
  if self.dfa is not None:
562
581
  return _Found() if self.dfa.search(subject) else None
563
582
  return self.rx.match(subject) if self.anchored else self.rx.search(subject)
@@ -285,7 +285,8 @@ class GitGuard:
285
285
  return
286
286
  except Exception: # pylint: disable=broad-exception-caught
287
287
  pass
288
- raise Deny(text_to_b("CONSENT_REQUIRED \u2014 no live pick of this session is this command.") + WHY_CONSENT)
288
+ raise Deny(text_to_b("CONSENT_REQUIRED \u2014 no live pick of this session is this command. Ask the human in "
289
+ "the dialog; or they run it in their own terminal.") + WHY_CONSENT)
289
290
 
290
291
  def _drop_status(self, path):
291
292
  """`[ "${#path}" -lt 3 ] || path="${path:3}"`, counted in the locale's characters."""