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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +25 -15
- package/agents/ai-reviewer.md.tmpl +14 -1
- package/ai/agents/implementer.md +6 -0
- package/ai/agents/reviewer.md +8 -1
- package/bin/ansi.js +12 -0
- package/bin/menu.js +71 -12
- package/bin/riskrails.js +240 -14
- package/bin/uninstall.js +115 -0
- package/bin/wizard.js +72 -0
- package/hooks/AGENTS.md +0 -6
- package/hooks/CLAUDE.md +0 -6
- package/hooks/context-guard.py +11 -1
- package/hooks/dialog-gate.py +3 -2
- package/hooks/guard/decision.py +4 -3
- package/hooks/guard/dialog.py +12 -2
- package/hooks/guard/ere.py +29 -10
- package/hooks/guard/git.py +2 -1
- package/hooks/guard/paths.py +48 -1406
- package/hooks/guard/paths_base.py +66 -0
- package/hooks/guard/paths_bash.py +176 -0
- package/hooks/guard/paths_classify.py +205 -0
- package/hooks/guard/paths_fs.py +137 -0
- package/hooks/guard/paths_human.py +105 -0
- package/hooks/guard/paths_interp.py +162 -0
- package/hooks/guard/paths_movers.py +393 -0
- package/hooks/guard/paths_shell.py +288 -0
- package/hooks/runtime-gate.py +87 -0
- package/hooks/update_notice.py +23 -8
- package/package.json +1 -1
- package/skills/AGENTS.md +0 -6
- package/skills/CLAUDE.md +0 -6
- package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
- package/skills/ai-init/templates/gitignore.snippet +2 -0
- package/skills/ai-task/memory_cleanup.py +213 -0
- package/skills/ai-task/memory_compact.py +25 -0
- package/skills/ai-task/memory_config.py +120 -0
- package/skills/ai-task/memory_export.py +246 -0
- package/skills/ai-task/memory_format.py +7 -3
- package/skills/ai-task/memory_hook.py +172 -0
- package/skills/ai-task/memory_io.py +122 -0
- package/skills/ai-task/memory_migrate.py +187 -0
- package/skills/ai-task/memory_packages.py +83 -0
- package/skills/ai-task/memory_risks.py +84 -0
- package/skills/ai-task/memory_status.py +76 -0
- package/skills/ai-task/memory_sync.py +20 -1481
- package/skills/ai-task/memory_verify.py +316 -0
- package/skills/ai-task/rr_state/cli.py +1 -0
- package/skills/ai-task/rr_state/close.py +2 -2
- package/skills/ai-task/rr_state/dialog.py +32 -7
- package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
- package/skills/ai-task/rr_state/gate.py +10 -4
- package/skills/ai-task/rr_state/labels.py +33 -4
- package/skills/ai-task/rr_state/memory_cmd.py +84 -64
- package/skills/ai-task/rr_state/questions_cmd.py +5 -4
- package/skills/ai-task/rr_state/report.py +73 -10
- package/skills/ai-task/rr_state/report_cmd.py +2 -0
- package/skills/ai-task/rr_state/review.py +18 -14
- package/skills/ai-task/rr_state/start.py +12 -9
- package/skills/ai-task/rr_state/steps.py +16 -7
- package/skills/ai-task/rr_state/verify.py +17 -12
- package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
- package/skills/project-update/history/blobs/fa0cb370b024e1a30e1355912032657b08f609e47773a603c193b1e609b0f997 +8 -0
- package/skills/project-update/history/index.json +8 -4
- package/skills/project-update/rr_update/__init__.py +1 -0
- package/skills/project-update/rr_update/apply.py +188 -0
- package/skills/project-update/rr_update/blocks.py +38 -0
- package/skills/project-update/rr_update/build.py +96 -0
- package/skills/project-update/rr_update/cleanup.py +194 -0
- package/skills/project-update/rr_update/cli.py +102 -0
- package/skills/project-update/rr_update/files.py +106 -0
- package/skills/project-update/rr_update/gate.py +60 -0
- package/skills/project-update/rr_update/hints.py +128 -0
- package/skills/project-update/rr_update/history.py +117 -0
- package/skills/project-update/rr_update/kept.py +120 -0
- package/skills/project-update/rr_update/paths.py +108 -0
- package/skills/project-update/rr_update/plan.py +123 -0
- package/skills/project-update/rr_update/report.py +51 -0
- package/skills/project-update/rr_update/rules.py +178 -0
- package/skills/project-update/rr_update/shared.py +74 -0
- package/skills/project-update/rr_update/siblings.py +37 -0
- package/skills/project-update/rr_update/weights.py +171 -0
- package/skills/project-update/update.py +8 -1682
- package/skills/rr-build/SKILL.md +5 -2
- package/skills/rr-setup/SKILL.md +4 -2
- package/skills/rr-setup/setup.py +13 -12
- package/skills/rr-ship/SKILL.md +2 -2
- package/skills/rr-spec/SKILL.md +6 -0
- package/skills/rr-status/SKILL.md +2 -0
- package/skills/rr-status/status.py +5 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""memory_cmd: the `memory` and `recall` commands over the memory_store and
|
|
1
|
+
"""memory_cmd: the `memory` and `recall` commands over the memory_store module and the owner modules memory_io, memory_config, memory_verify, memory_hook, memory_risks, memory_cleanup, memory_migrate, memory_export and memory_status -- the actions, the human's route, the risk record.
|
|
2
2
|
|
|
3
3
|
shell: memory_modules, memory_config, memory_human, memory_confirm, cmd_recall, cmd_memory, memory_status, memory_enable, memory_disable, memory_export, memory_verify, memory_show, memory_search, risk_scope, memory_risk, memory_risks, memory_migrate, memory_rebuild, memory_cleanup, memory_restore, memory_pack, memory_redact
|
|
4
4
|
core: MEMORY_ACTIONS
|
|
@@ -9,6 +9,7 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
import shlex
|
|
11
11
|
import sys
|
|
12
|
+
import types
|
|
12
13
|
|
|
13
14
|
from rr_state import fs, journal, task
|
|
14
15
|
from rr_state import dialog as rr_dialog
|
|
@@ -22,19 +23,31 @@ except ImportError: # an old install that shipped state.py
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def memory_modules():
|
|
25
|
-
"""
|
|
26
|
+
"""The owner modules of the memory commands as one namespace (mem.io, mem.config, mem.verify, mem.hook,
|
|
27
|
+
mem.risks, mem.cleanup, mem.migrate, mem.export, mem.status -- each name is read on its owner at call time)
|
|
28
|
+
and memory_store, imported only by the commands that use them."""
|
|
26
29
|
try:
|
|
27
30
|
import memory_store # pylint: disable=import-outside-toplevel
|
|
28
|
-
import
|
|
31
|
+
import memory_cleanup as cleanup # pylint: disable=import-outside-toplevel
|
|
32
|
+
import memory_config as config # pylint: disable=import-outside-toplevel
|
|
33
|
+
import memory_export as export # pylint: disable=import-outside-toplevel
|
|
34
|
+
import memory_hook as hook # pylint: disable=import-outside-toplevel
|
|
35
|
+
import memory_io as io # pylint: disable=import-outside-toplevel
|
|
36
|
+
import memory_migrate as migrate # pylint: disable=import-outside-toplevel
|
|
37
|
+
import memory_risks as risks # pylint: disable=import-outside-toplevel
|
|
38
|
+
import memory_status as status # pylint: disable=import-outside-toplevel
|
|
39
|
+
import memory_verify as verify # pylint: disable=import-outside-toplevel
|
|
29
40
|
except ImportError as exc:
|
|
30
41
|
fs.die("the memory modules are not installed beside state.py (%s)" % exc, 2)
|
|
31
|
-
|
|
42
|
+
mem = types.SimpleNamespace(cleanup=cleanup, config=config, export=export, hook=hook, io=io, migrate=migrate,
|
|
43
|
+
risks=risks, status=status, verify=verify)
|
|
44
|
+
return mem, memory_store
|
|
32
45
|
|
|
33
46
|
|
|
34
|
-
def memory_config(
|
|
47
|
+
def memory_config(mem, root):
|
|
35
48
|
try:
|
|
36
|
-
return
|
|
37
|
-
except
|
|
49
|
+
return mem.config.load_config(root)
|
|
50
|
+
except mem.io.MemoryError_ as exc:
|
|
38
51
|
fs.die(str(exc), exc.code)
|
|
39
52
|
return None
|
|
40
53
|
|
|
@@ -62,8 +75,8 @@ def memory_confirm(cfg, name, what):
|
|
|
62
75
|
def cmd_recall(args, root, rt=rr_runtime.Runtime()):
|
|
63
76
|
"""R10: the open risks, residual findings, decisions, scope changes and task
|
|
64
77
|
one-liners that touch these files, within the budget. Read-only."""
|
|
65
|
-
|
|
66
|
-
cfg = memory_config(
|
|
78
|
+
mem, store = memory_modules()
|
|
79
|
+
cfg = memory_config(mem, root)
|
|
67
80
|
files = fs._split_files(args.files)
|
|
68
81
|
if not files:
|
|
69
82
|
fs.die("recall needs --files \"a,b,dir/**\"", 2)
|
|
@@ -82,45 +95,49 @@ def cmd_recall(args, root, rt=rr_runtime.Runtime()):
|
|
|
82
95
|
|
|
83
96
|
|
|
84
97
|
def cmd_memory(args, root, rt=rr_runtime.Runtime()):
|
|
85
|
-
|
|
98
|
+
mem, store = memory_modules()
|
|
86
99
|
action = args.memory_action
|
|
87
100
|
try:
|
|
88
|
-
MEMORY_ACTIONS[action](args, root,
|
|
89
|
-
except
|
|
101
|
+
MEMORY_ACTIONS[action](args, root, mem, store, rt=rt)
|
|
102
|
+
except mem.io.MemoryError_ as exc:
|
|
90
103
|
fs.die(str(exc), exc.code)
|
|
91
104
|
|
|
92
105
|
|
|
93
|
-
def memory_status(args, root,
|
|
94
|
-
lines =
|
|
106
|
+
def memory_status(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
107
|
+
lines = mem.status.status_lines(root)
|
|
95
108
|
if args.json:
|
|
96
109
|
print(json.dumps(dict(line.split(": ", 1) for line in lines), ensure_ascii=False, sort_keys=True))
|
|
97
110
|
return
|
|
98
111
|
print("\n".join(lines))
|
|
99
112
|
|
|
100
113
|
|
|
101
|
-
def memory_enable(args, root,
|
|
114
|
+
def memory_enable(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
102
115
|
if memory_human(root, "enable", allow_dialog=True, rt=rt) == "dialog":
|
|
103
116
|
pick = rr_dialog.pick_data(rr_dialog.DIALOG_PICK[0])
|
|
104
117
|
print("memory: via dialog (consent %s, decision %s)" % (pick["consent"], pick["decision"]))
|
|
105
|
-
hook =
|
|
106
|
-
print("memory: packages mode on (%s); every teammate's next close exports" %
|
|
107
|
-
|
|
118
|
+
hook = mem.hook.enable(root, args.author, args.period, args.index, args.share)
|
|
119
|
+
print("memory: packages mode on (%s); every teammate's next close exports" % mem.io.POLICY_REL)
|
|
120
|
+
if hook == "local":
|
|
121
|
+
print("record: local — .ai/memory/ and .ai/reports/ are git-ignored; 'state.py memory enable --share git' "
|
|
122
|
+
"in your own terminal shares them through git and checks pushes")
|
|
123
|
+
else:
|
|
124
|
+
print("pre-push: %s" % hook)
|
|
108
125
|
if hook in ("foreign", "hookspath"):
|
|
109
126
|
print("not written — %s. Add these lines to your own pre-push hook:\n%s" % (
|
|
110
|
-
"core.hooksPath is set" if hook == "hookspath" else "a pre-push hook exists",
|
|
111
|
-
print("\n".join(
|
|
127
|
+
"core.hooksPath is set" if hook == "hookspath" else "a pre-push hook exists", mem.hook.snippet_text()))
|
|
128
|
+
print("\n".join(mem.status.status_lines(root)))
|
|
112
129
|
|
|
113
130
|
|
|
114
|
-
def memory_disable(_args, root,
|
|
131
|
+
def memory_disable(_args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
115
132
|
memory_human(root, "disable", rt=rt)
|
|
116
|
-
print("memory: files mode; packages and local data kept; pre-push: %s" %
|
|
133
|
+
print("memory: files mode; packages and local data kept; pre-push: %s" % mem.hook.disable(root))
|
|
117
134
|
|
|
118
135
|
|
|
119
|
-
def memory_export(args, root,
|
|
120
|
-
cfg = memory_config(
|
|
136
|
+
def memory_export(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
137
|
+
cfg = memory_config(mem, root)
|
|
121
138
|
ids = "pending" if args.pending or not args.ids else [task.valid_task_id(t, "ID") for t in args.ids]
|
|
122
|
-
result =
|
|
123
|
-
for line in
|
|
139
|
+
result = mem.export.export(root, ids, cfg)
|
|
140
|
+
for line in mem.export.record_lines(result):
|
|
124
141
|
print(line)
|
|
125
142
|
print("exported: +%d entries%s" % (result.added, "" if result.added else " — nothing pending"))
|
|
126
143
|
for task_id, why in result.failures:
|
|
@@ -129,36 +146,39 @@ def memory_export(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
|
129
146
|
sys.exit(1)
|
|
130
147
|
|
|
131
148
|
|
|
132
|
-
def memory_verify(args, root,
|
|
149
|
+
def memory_verify(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
133
150
|
stats = None
|
|
134
151
|
if args.range:
|
|
135
152
|
if ".." not in args.range:
|
|
136
153
|
fs.die("--range takes OLD..NEW", 2)
|
|
137
154
|
old, new = args.range.split("..", 1)
|
|
138
|
-
findings, stats =
|
|
155
|
+
findings, stats = mem.verify.validate_range(root, mem.verify.rev(root, old), mem.verify.rev(root, new) or new, local=False)
|
|
139
156
|
findings = findings or []
|
|
140
157
|
elif args.fresh_clone:
|
|
141
|
-
|
|
158
|
+
if not mem.io.shared(root):
|
|
159
|
+
fs.die("the record is local (.ai/memory/ is git-ignored): a fresh clone has none — "
|
|
160
|
+
"'state.py memory enable --share git' shares it", 2)
|
|
161
|
+
findings = mem.verify.verify_fresh_clone(root)
|
|
142
162
|
else:
|
|
143
|
-
findings =
|
|
163
|
+
findings = mem.verify.verify(root, "all", [task.valid_task_id(t, "ID") for t in args.ids] or None)
|
|
144
164
|
failing = [f for f in findings if not f.check.endswith("-warn")]
|
|
145
165
|
if args.json:
|
|
146
166
|
print(json.dumps({"findings": [f._asdict() for f in findings], "ok": not failing, "stats": stats},
|
|
147
167
|
ensure_ascii=False, sort_keys=True))
|
|
148
168
|
else:
|
|
149
169
|
for f in findings:
|
|
150
|
-
print(
|
|
170
|
+
print(mem.verify.format_finding(f))
|
|
151
171
|
print("memory verify: %s" % ("ok" if not failing else "%d finding(s)" % len(failing)))
|
|
152
172
|
if failing:
|
|
153
173
|
sys.exit(1)
|
|
154
174
|
|
|
155
175
|
|
|
156
|
-
def memory_show(args, root,
|
|
157
|
-
print("\n".join(
|
|
176
|
+
def memory_show(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
177
|
+
print("\n".join(mem.status.show(root, task.valid_task_id(args.id, "ID"), "json" if args.json else "md", args.entry)))
|
|
158
178
|
|
|
159
179
|
|
|
160
|
-
def memory_search(args, root,
|
|
161
|
-
cfg = memory_config(
|
|
180
|
+
def memory_search(args, root, mem, store, rt=rr_runtime.Runtime()):
|
|
181
|
+
cfg = memory_config(mem, root)
|
|
162
182
|
filters = {"kind": args.kind, "by": args.by, "task": args.task, "since": args.since, "until": args.until,
|
|
163
183
|
"files": fs._split_files(args.file) or None}
|
|
164
184
|
history = store.open_history(root, cfg)
|
|
@@ -187,8 +207,8 @@ def risk_scope(state):
|
|
|
187
207
|
6)
|
|
188
208
|
|
|
189
209
|
|
|
190
|
-
def memory_risk(args, root,
|
|
191
|
-
cfg = memory_config(
|
|
210
|
+
def memory_risk(args, root, mem, store, rt=rr_runtime.Runtime()):
|
|
211
|
+
cfg = memory_config(mem, root)
|
|
192
212
|
state = task.in_flight(root, rt=rt)
|
|
193
213
|
if args.risk_action == "add":
|
|
194
214
|
where = fs._split_files(args.where)
|
|
@@ -199,17 +219,17 @@ def memory_risk(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
|
199
219
|
if not cfg.packages:
|
|
200
220
|
risk_scope(state)
|
|
201
221
|
found = "%s, by %s" % (fs.now()[:10], state["task_id"] if state else "no task")
|
|
202
|
-
rkey =
|
|
222
|
+
rkey = mem.risks.risk_add_files(root, args.title, where, args.label, args.what, args.likely, args.fix,
|
|
203
223
|
args.status, found)
|
|
204
224
|
print("risk: %s added to .ai/project/known-risks.md" % rkey)
|
|
205
225
|
return
|
|
206
|
-
data =
|
|
226
|
+
data = mem.risks.risk_data(args.title, where, args.label, args.what, args.likely, args.fix, args.status)
|
|
207
227
|
else:
|
|
208
228
|
if args.status not in ("fixed", "accepted", "ticketed"):
|
|
209
229
|
fs.die("--status takes fixed, accepted or ticketed", 2)
|
|
210
230
|
if not cfg.packages:
|
|
211
231
|
risk_scope(state)
|
|
212
|
-
|
|
232
|
+
mem.risks.risk_close_files(root, args.rkey, args.status, args.why)
|
|
213
233
|
print("risk: %s is %s" % (args.rkey, args.status))
|
|
214
234
|
return
|
|
215
235
|
history = store.FileHistory(root, cfg)
|
|
@@ -217,7 +237,7 @@ def memory_risk(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
|
217
237
|
if not found:
|
|
218
238
|
fs.die("no risk %s ('state.py memory risks --status all')" % args.rkey, 1)
|
|
219
239
|
last = found[0]
|
|
220
|
-
data =
|
|
240
|
+
data = mem.risks.risk_data(last["title"], last.get("where"), last.get("label"), last.get("what"),
|
|
221
241
|
last.get("likelihood"), last.get("fix"), args.status, args.why, last["rid"])
|
|
222
242
|
if state:
|
|
223
243
|
state = task.load(root, rt=rt)
|
|
@@ -225,12 +245,12 @@ def memory_risk(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
|
225
245
|
task.save(root, state)
|
|
226
246
|
print("risk: %s recorded in %s's journal (exported at close)" % (data["rkey"], state["task_id"]))
|
|
227
247
|
return
|
|
228
|
-
entry =
|
|
248
|
+
entry = mem.risks.standalone_risk(root, cfg, data, rt.name)
|
|
229
249
|
print("risk: %s recorded (%s)" % (data["rkey"], entry["rid"]))
|
|
230
250
|
|
|
231
251
|
|
|
232
|
-
def memory_risks(args, root,
|
|
233
|
-
cfg = memory_config(
|
|
252
|
+
def memory_risks(args, root, mem, store, rt=rr_runtime.Runtime()):
|
|
253
|
+
cfg = memory_config(mem, root)
|
|
234
254
|
risks = store.open_history(root, cfg).risks(args.status)
|
|
235
255
|
if args.json:
|
|
236
256
|
for e in risks:
|
|
@@ -253,57 +273,57 @@ def memory_risks(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
|
253
273
|
e.get("title"), e.get("rkey")))
|
|
254
274
|
|
|
255
275
|
|
|
256
|
-
def memory_migrate(args, root,
|
|
257
|
-
cfg = memory_config(
|
|
276
|
+
def memory_migrate(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
277
|
+
cfg = memory_config(mem, root)
|
|
258
278
|
if args.move_logs:
|
|
259
279
|
memory_human(root, "migrate --move-logs", rt=rt)
|
|
260
280
|
memory_confirm(cfg, args.confirm_delete, "migrate --move-logs")
|
|
261
|
-
moved, command =
|
|
281
|
+
moved, command = mem.migrate.move_logs(root)
|
|
262
282
|
print("moved %d committed log(s) into .ai/state/logs/<id>/legacy/; now, as its own commit (C3):\n %s"
|
|
263
283
|
% (len(moved), command))
|
|
264
284
|
return
|
|
265
285
|
if args.apply:
|
|
266
|
-
print("\n".join(
|
|
286
|
+
print("\n".join(mem.migrate.migrate_apply(root, risks=args.risks)))
|
|
267
287
|
return
|
|
268
|
-
report =
|
|
288
|
+
report = mem.migrate.migrate_report(root)
|
|
269
289
|
print("report: %s (%d lines) — read it, then 'state.py memory migrate --apply'"
|
|
270
|
-
% (os.path.join(
|
|
290
|
+
% (os.path.join(mem.io.LOCAL_REL, "migration-report.md"), report.count("\n")))
|
|
271
291
|
|
|
272
292
|
|
|
273
|
-
def memory_rebuild(_args, root,
|
|
274
|
-
cfg = memory_config(
|
|
293
|
+
def memory_rebuild(_args, root, mem, store, rt=rr_runtime.Runtime()):
|
|
294
|
+
cfg = memory_config(mem, root)
|
|
275
295
|
print("index: rebuilt from %d sources (%s)" % (store.SqliteHistory(root, cfg).rebuild(), store.INDEX_REL))
|
|
276
296
|
|
|
277
297
|
|
|
278
|
-
def memory_cleanup(args, root,
|
|
279
|
-
cfg = memory_config(
|
|
298
|
+
def memory_cleanup(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
299
|
+
cfg = memory_config(mem, root)
|
|
280
300
|
memory_human(root, "cleanup", rt=rt)
|
|
281
301
|
memory_confirm(cfg, args.confirm_delete, "cleanup")
|
|
282
302
|
if not args.all and not args.ids:
|
|
283
303
|
fs.die("memory cleanup takes IDs or --all", 2)
|
|
284
|
-
print("\n".join(
|
|
304
|
+
print("\n".join(mem.cleanup.cleanup(root, "all" if args.all else [task.valid_task_id(t, "ID") for t in args.ids])))
|
|
285
305
|
|
|
286
306
|
|
|
287
|
-
def memory_restore(args, root,
|
|
307
|
+
def memory_restore(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
288
308
|
if not args.all and not args.ids:
|
|
289
309
|
fs.die("memory restore takes IDs or --all", 2)
|
|
290
|
-
print("\n".join(
|
|
310
|
+
print("\n".join(mem.cleanup.restore(root, "all" if args.all else [task.valid_task_id(t, "ID") for t in args.ids])))
|
|
291
311
|
|
|
292
312
|
|
|
293
|
-
def memory_pack(args, root,
|
|
294
|
-
cfg = memory_config(
|
|
313
|
+
def memory_pack(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
314
|
+
cfg = memory_config(mem, root)
|
|
295
315
|
memory_human(root, "pack", rt=rt)
|
|
296
316
|
memory_confirm(cfg, args.confirm_delete, "pack")
|
|
297
|
-
target, removed, count =
|
|
298
|
-
print("pack: %s (%d records; %d batch(es) removed)" % (os.path.join(
|
|
317
|
+
target, removed, count = mem.cleanup.pack(root, args.period)
|
|
318
|
+
print("pack: %s (%d records; %d batch(es) removed)" % (os.path.join(mem.io.MEMORY_REL, target), count,
|
|
299
319
|
len(removed)))
|
|
300
320
|
|
|
301
321
|
|
|
302
|
-
def memory_redact(args, root,
|
|
322
|
+
def memory_redact(args, root, mem, _store, rt=rr_runtime.Runtime()):
|
|
303
323
|
via = memory_human(root, "redact", rt=rt)
|
|
304
|
-
rid, name =
|
|
324
|
+
rid, name = mem.cleanup.redact(root, args.rid, args.field, args.why, by="%s via %s" % (os.environ.get("USER") or
|
|
305
325
|
"unknown", via))
|
|
306
|
-
print("redacted: %s in %s (new rid %s)" % (args.field, os.path.join(
|
|
326
|
+
print("redacted: %s in %s (new rid %s)" % (args.field, os.path.join(mem.io.MEMORY_REL, name), rid))
|
|
307
327
|
|
|
308
328
|
|
|
309
329
|
MEMORY_ACTIONS = {"status": memory_status, "enable": memory_enable, "disable": memory_disable,
|
|
@@ -280,10 +280,11 @@ def close_gate_from_file(root, state, question, by, rt=rr_runtime.Runtime()):
|
|
|
280
280
|
state.update(fresh)
|
|
281
281
|
seen, why = (True, "") if unattended else dialog.human_turn(root, state)
|
|
282
282
|
if not seen:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
cmd = "python3 %s --root %s approve --by %s" % (shlex.quote(fs.ENTRY), shlex.quote(root), shlex.quote(by))
|
|
284
|
+
asked = dialog.human_refusal("'state.py approve'", cmd, why, dialog.open_approval_id(root, rt=rt))
|
|
285
|
+
fs.die(asked + " Or fill the [Answer]: line, tell the session, and sync again." if asked else
|
|
286
|
+
"APPROVAL_REFUSED — %s. Fill the answer, tell the session, and sync again, or run "
|
|
287
|
+
"in your own terminal:\n %s" % (why, cmd), 5)
|
|
287
288
|
approved = question["choice"] == "A"
|
|
288
289
|
session = fs.read_session(root)
|
|
289
290
|
task.claim_runtime(root, state, rt=rt)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"""report: the text the state is rendered to — the two close facts, the status lines, the schema line, task.md and its writer.
|
|
2
2
|
|
|
3
|
-
core: md_cell
|
|
4
|
-
shell: two_facts, status_lines, schema_line, task_md_lines, write_task_md
|
|
3
|
+
core: md_cell, waiting_line
|
|
4
|
+
shell: two_facts, pending_questions, waiting_decisions, waiting, status_lines, schema_line, task_md_lines, write_task_md
|
|
5
5
|
"""
|
|
6
6
|
import contextlib
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import sys
|
|
10
11
|
|
|
11
12
|
from rr_state import fs, journal, locks, questions, task
|
|
13
|
+
from rr_state import ledger as rr_ledger
|
|
12
14
|
from rr_state import policy as rr_policy # status_lines names a local `policy`, as state.py does
|
|
13
15
|
|
|
14
16
|
# weight.py and sensors.py sit beside state.py; the entry puts that directory on sys.path.
|
|
@@ -73,20 +75,81 @@ def two_facts(root, state, tree):
|
|
|
73
75
|
b = (True, "%d changed files, all in the task's files" % len(diff))
|
|
74
76
|
return {"a": a, "b": b, "diff": diff or [], "outside": outside, "documents": documents}
|
|
75
77
|
|
|
78
|
+
def pending_questions(root, state):
|
|
79
|
+
"""The ids the status shows as pending: the cache, and for schema 7 `## Questions`
|
|
80
|
+
as the file has it (the cache lags a hand-filled [Answer]:, as guard_pending reads it)
|
|
81
|
+
plus an open gate's id."""
|
|
82
|
+
pending = ((state or {}).get("questions") or {}).get("pending") or []
|
|
83
|
+
if task.is_v7(state):
|
|
84
|
+
with contextlib.suppress(OSError, ValueError, KeyError):
|
|
85
|
+
_, asked = questions.parse_questions(fs.questions_path(root, state, None))
|
|
86
|
+
gate_id = task.open_gate(state)
|
|
87
|
+
pending = [q["id"] for q in asked if (q["kind"] != "G" and questions.is_pending(q)) or q["id"] == gate_id]
|
|
88
|
+
return pending
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def waiting_decisions(root):
|
|
92
|
+
"""The open decisions that still wait — dialog.prune_decisions' rule without its
|
|
93
|
+
writes: not settled, and not every command pick of the latest dialog call used.
|
|
94
|
+
An unreadable decision.json waits for nobody here; the decision commands say why."""
|
|
95
|
+
try:
|
|
96
|
+
with open(fs.decision_path(root), encoding="utf-8") as fh:
|
|
97
|
+
opened = json.load(fh)["open"]
|
|
98
|
+
except (OSError, ValueError, KeyError, TypeError, RecursionError):
|
|
99
|
+
return []
|
|
100
|
+
ledger = rr_ledger.read_ledger(root)
|
|
101
|
+
settled = {e.get("decision") for e in ledger if e["type"] == "settle"}
|
|
102
|
+
used = {e.get("id") for e in ledger if e["type"] == "use"}
|
|
103
|
+
rounds = rr_ledger.latest_rounds(ledger)
|
|
104
|
+
out = []
|
|
105
|
+
for decision in opened:
|
|
106
|
+
commands = {o.get("n") for o in decision.get("options") or [] if o.get("kind") == "command"}
|
|
107
|
+
picks = [e for e in rounds.get(decision.get("id"), []) if e.get("option") in commands]
|
|
108
|
+
if decision.get("id") not in settled and not (picks and all(p.get("id") in used for p in picks)):
|
|
109
|
+
out.append(decision)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def waiting(root, state, pending):
|
|
114
|
+
"""R12: who the task waits on and for what — decisions, then the gate, then questions."""
|
|
115
|
+
decisions = waiting_decisions(root)
|
|
116
|
+
gate = task.open_gate(state) if task.is_v7(state) else None
|
|
117
|
+
asked = [q for q in pending if q != gate]
|
|
118
|
+
on = "you" if decisions or gate or asked else "agent" if (state or {}).get("task_id") else "nobody"
|
|
119
|
+
return {"on": on, "decisions": decisions, "gate": gate, "questions": asked}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def waiting_line(root, state, wait):
|
|
123
|
+
"""Line 1 of the status, or None with no task in flight and nothing open. When
|
|
124
|
+
several wait, one line joins them in this order: decisions, the gate, questions."""
|
|
125
|
+
if wait["on"] == "nobody":
|
|
126
|
+
return None
|
|
127
|
+
if wait["on"] == "agent":
|
|
128
|
+
return "waiting on the agent: %s — %s" % (state.get("stage") or state.get("current_stage") or "?",
|
|
129
|
+
fs.one_line(state.get("next_action"), 140) or "?")
|
|
130
|
+
parts = []
|
|
131
|
+
if wait["decisions"]:
|
|
132
|
+
parts.append("%s — the dialog ('state.py decision show' sends it again)" % ", ".join(
|
|
133
|
+
"%s %s" % (d.get("id"), fs.one_line(d.get("header"), 40)) for d in wait["decisions"][:4]))
|
|
134
|
+
if wait["gate"]:
|
|
135
|
+
parts.append("gate %s — approve in the dialog ('state.py close' offers it) or in your own terminal"
|
|
136
|
+
% wait["gate"])
|
|
137
|
+
if wait["questions"]:
|
|
138
|
+
parts.append("%s — answer in the dialog or the chat, or fill [Answer]: in %s" % (
|
|
139
|
+
", ".join(map(str, wait["questions"][:4])),
|
|
140
|
+
os.path.relpath(fs.questions_path(root, state, None), root)))
|
|
141
|
+
return "waiting on you: " + "; ".join(parts)
|
|
142
|
+
|
|
143
|
+
|
|
76
144
|
def status_lines(root, state):
|
|
77
145
|
"""≤ 20 lines; read-only (the facts are measured, nothing is written)."""
|
|
78
|
-
|
|
146
|
+
pending = pending_questions(root, state)
|
|
147
|
+
head = waiting_line(root, state, waiting(root, state, pending))
|
|
148
|
+
lines = ([head] if head else []) + ["root: %s" % root]
|
|
79
149
|
if not state or not state.get("task_id"):
|
|
80
150
|
return lines + ["task: none — no task in flight", schema_line(root)]
|
|
81
151
|
events, _ = journal.read_journal(root, state["task_id"])
|
|
82
152
|
last = events[-1] if events else {}
|
|
83
|
-
pending = (state.get("questions") or {}).get("pending") or []
|
|
84
|
-
if task.is_v7(state):
|
|
85
|
-
# The cache lags a hand-filled [Answer]:; the file is the truth, as guard_pending reads it.
|
|
86
|
-
with contextlib.suppress(OSError, ValueError, KeyError):
|
|
87
|
-
_, asked = questions.parse_questions(fs.questions_path(root, state, None))
|
|
88
|
-
gate_id = task.open_gate(state)
|
|
89
|
-
pending = [q["id"] for q in asked if (q["kind"] != "G" and questions.is_pending(q)) or q["id"] == gate_id]
|
|
90
153
|
lines.append("task: %s — %s" % (state["task_id"], fs.one_line(state.get("goal"), 140)))
|
|
91
154
|
if not task.is_v7(state):
|
|
92
155
|
lines += ["tier: %s · workflow %s · stage %s" % (state.get("risk_tier") or "?", state.get("workflow"),
|
|
@@ -225,6 +225,8 @@ def cmd_status(args, root, rt=rr_runtime.Runtime()):
|
|
|
225
225
|
"operations")}
|
|
226
226
|
out["facts"] = {"a": list(facts["a"]), "b": list(facts["b"]), "tree": tree}
|
|
227
227
|
out["questions_pending"] = len((state.get("questions") or {}).get("pending") or [])
|
|
228
|
+
wait = report.waiting(root, state, report.pending_questions(root, state))
|
|
229
|
+
out["waiting"] = dict(wait, decisions=[d.get("id") for d in wait["decisions"]])
|
|
228
230
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
229
231
|
return
|
|
230
232
|
print("\n".join(report.status_lines(root, state)))
|
|
@@ -84,20 +84,24 @@ def cmd_review(args, root, rt=rr_runtime.Runtime()):
|
|
|
84
84
|
if review["passes"] >= rounds:
|
|
85
85
|
if not args.by:
|
|
86
86
|
report = ["--report", os.path.abspath(args.report)] if args.report and os.path.isfile(args.report) else []
|
|
87
|
-
dialog.offer_decision(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
decision = dialog.offer_decision(
|
|
88
|
+
root, state, "human_set", "Review",
|
|
89
|
+
"%s\n%d review passes so far; this task allows %d%s. The next verdict is yours."
|
|
90
|
+
% (dialog.task_facts(root, state), review["passes"], rounds,
|
|
91
|
+
" (%s)" % budget_name if rr_policy.batch_on(state) else ""),
|
|
92
|
+
[{"kind": "choice", "effect": "keep", "label": "Keep it open — no verdict now"}] +
|
|
93
|
+
[{"kind": "command",
|
|
94
|
+
"label": rr_labels.state_label(root, ["review", "--verdict", verdict, "--tier", args.tier] +
|
|
95
|
+
report + ["--by", rr_labels.human_name(root)])}
|
|
96
|
+
for verdict in ("pass", "pass_with_findings", "blockers_open")], "review", rt=rt)
|
|
97
|
+
cmd = "python3 %s --root %s review --verdict <verdict> --tier %s --by \"<name>\"" % (
|
|
98
|
+
shlex.quote(fs.ENTRY), shlex.quote(root), args.tier)
|
|
99
|
+
asked = dialog.human_refusal("the next review verdict", cmd, "the review budget is spent: %d passes, %d allowed"
|
|
100
|
+
% (review["passes"], rounds), decision and decision["id"])
|
|
101
|
+
fs.die(("REVIEW_BUDGET — %d review passes; weight.json allows %d (%s).\n%s"
|
|
102
|
+
% (review["passes"], rounds, budget_name, asked)) if asked else
|
|
103
|
+
"REVIEW_BUDGET — %d review passes; weight.json allows %d (%s). The next verdict is the human's, "
|
|
104
|
+
"in their own terminal:\n %s" % (review["passes"], rounds, budget_name, cmd), 6)
|
|
101
105
|
via, why = dialog.human_route(root, allow_dialog=True, rt=rt)
|
|
102
106
|
if via is None:
|
|
103
107
|
fs.die("APPROVAL_REFUSED — a verdict past the review budget is the human's, from their own "
|
|
@@ -159,15 +159,18 @@ def cmd_start(args, root, rt=rr_runtime.Runtime()):
|
|
|
159
159
|
if rr_policy.MODES.index(args.mode) < rr_policy.MODES.index(mode):
|
|
160
160
|
via, why = dialog.human_route(root, allow_dialog=True, rt=rt)
|
|
161
161
|
if via is None:
|
|
162
|
-
dialog.offer_decision(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
decision = dialog.offer_decision(
|
|
163
|
+
root, None, "human_set", "Mode",
|
|
164
|
+
"%s\nThe task's weight says mode %s. Lower it to %s (by %s)? A lower mode means lighter checks "
|
|
165
|
+
"and a lighter review." % (fs.one_line(args.goal, 300), mode, args.mode, args.by),
|
|
166
|
+
[{"kind": "choice", "effect": "keep", "label": "Keep mode %s" % mode},
|
|
167
|
+
{"kind": "command", "label": labels.state_label(root, sys.argv[1:])}], "start --mode", rt=rt)
|
|
168
|
+
cmd = "python3 %s --root %s start … --mode %s --by %s" % (
|
|
169
|
+
shlex.quote(fs.ENTRY), shlex.quote(root), args.mode, shlex.quote(args.by))
|
|
170
|
+
what = "lowering the mode from %s to %s" % (mode, args.mode)
|
|
171
|
+
fs.die(dialog.human_refusal(what, cmd, why, decision and decision["id"])
|
|
172
|
+
or "APPROVAL_REFUSED — %s is a human's decision. Run it in your own terminal:\n %s\n"
|
|
173
|
+
"Refused because %s." % (what, cmd, why), 5)
|
|
171
174
|
override = {"from": mode, "to": args.mode, "by": args.by, "why": args.why, "ts": fs.now()}
|
|
172
175
|
mode = args.mode
|
|
173
176
|
if args.slug is not None:
|
|
@@ -6,8 +6,9 @@ shell: cmd_step_done_v7, cmd_steps, cmd_batch
|
|
|
6
6
|
# (uniform cli dispatch: every cmd_* takes rt last, whether it uses it or not)
|
|
7
7
|
import json
|
|
8
8
|
import shlex
|
|
9
|
+
import sys
|
|
9
10
|
|
|
10
|
-
from rr_state import dialog, fs, handoff, journal, report, task
|
|
11
|
+
from rr_state import dialog, fs, handoff, journal, labels, report, task
|
|
11
12
|
from rr_state import policy as rr_policy # cmd_batch binds a local `policy`
|
|
12
13
|
from rr_state import runtime as rr_runtime
|
|
13
14
|
|
|
@@ -103,11 +104,18 @@ def cmd_batch(args, root, rt=rr_runtime.Runtime()):
|
|
|
103
104
|
return
|
|
104
105
|
via = None
|
|
105
106
|
if want:
|
|
106
|
-
via, why = dialog.human_route(root, rt=rt)
|
|
107
|
+
via, why = dialog.human_route(root, allow_dialog=True, rt=rt)
|
|
107
108
|
if via is None:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"
|
|
109
|
+
decision = dialog.offer_decision(
|
|
110
|
+
root, state, "human_set", "Batch",
|
|
111
|
+
"%s\nTurn the batch loop on for this task? It lowers the rigor for the rest of the task: the batch "
|
|
112
|
+
"budgets replace the standard ones." % dialog.task_facts(root, state),
|
|
113
|
+
[{"kind": "choice", "effect": "keep", "label": "Keep the standard loop"},
|
|
114
|
+
{"kind": "command", "label": labels.state_label(root, sys.argv[1:])}], "batch on", rt=rt)
|
|
115
|
+
cmd = "python3 %s --root %s batch on --by \"<name>\"" % (shlex.quote(fs.ENTRY), shlex.quote(root))
|
|
116
|
+
fs.die(dialog.human_refusal("turning the batch loop on during a task", cmd, why, decision and decision["id"])
|
|
117
|
+
or "APPROVAL_REFUSED — turning the batch loop on during a task lowers the rigor, so it is the "
|
|
118
|
+
"human's. In their own terminal:\n %s\nRefused because %s." % (cmd, why), 5)
|
|
111
119
|
elif not (args.why or "").strip():
|
|
112
120
|
fs.die("batch off needs --why \"<why the standard loop from here>\"")
|
|
113
121
|
policy = rr_policy.weight_policy(root)
|
|
@@ -118,8 +126,9 @@ def cmd_batch(args, root, rt=rr_runtime.Runtime()):
|
|
|
118
126
|
# At the gate (stage ship) the next action is the human's and stays so.
|
|
119
127
|
state["next_action"] = rr_policy.review_next_action(state)
|
|
120
128
|
journal.emit(root, state, "batch_set", "%s by %s%s" % (args.action, args.by, " via %s" % via if via else ""),
|
|
121
|
-
{"on": want, "by": args.by, "why": args.why or None, "via": via,
|
|
122
|
-
|
|
129
|
+
dict({"on": want, "by": args.by, "why": args.why or None, "via": via,
|
|
130
|
+
"review_passes": state["review"]["passes"], "suite_runs": state["verify"]["suite_runs"]},
|
|
131
|
+
**dialog.pick_data(dialog.DIALOG_PICK[0])),
|
|
123
132
|
actor="human" if via == "terminal" else "agent", rt=rt)
|
|
124
133
|
handoff.set_resume_point(state, rt=rt)
|
|
125
134
|
handoff.write_handoff(root, state, "batch", rt=rt)
|
|
@@ -171,18 +171,23 @@ def cmd_verify(args, root, rt=rr_runtime.Runtime()):
|
|
|
171
171
|
max_runs, budget_name = rr_policy.budget(policy, state, "max_suite_runs")
|
|
172
172
|
if scope in ("suite", "all") and state["verify"]["suite_runs"] >= max_runs:
|
|
173
173
|
if not args.by:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
runs = state["verify"]["suite_runs"]
|
|
175
|
+
decision = dialog.offer_decision(
|
|
176
|
+
root, state, "human_set", "Budget",
|
|
177
|
+
"%s\nThe test suite has run %d times; this task allows %d%s. Run it once more?"
|
|
178
|
+
% (dialog.task_facts(root, state), runs, max_runs,
|
|
179
|
+
" (%s)" % budget_name if rr_policy.batch_on(state) else ""),
|
|
180
|
+
[{"kind": "choice", "effect": "keep", "label": "Keep the budget — stop here"},
|
|
181
|
+
{"kind": "command",
|
|
182
|
+
"label": labels.state_label(root, sys.argv[1:] + ["--by", labels.human_name(root)])}], "verify", rt=rt)
|
|
183
|
+
cmd = "python3 %s --root %s verify --scope %s --by \"<name>\"" % (
|
|
184
|
+
shlex.quote(fs.ENTRY), shlex.quote(root), scope)
|
|
185
|
+
asked = dialog.human_refusal("a run past the suite budget", cmd, "the budget is spent: %d runs, %d allowed"
|
|
186
|
+
% (runs, max_runs), decision and decision["id"])
|
|
187
|
+
fs.die(("SUITE_BUDGET — %d suite runs; weight.json allows %d (%s).\n%s" % (runs, max_runs, budget_name, asked))
|
|
188
|
+
if asked else
|
|
189
|
+
"SUITE_BUDGET — %d suite runs; weight.json allows %d (%s). Running it again is not the next "
|
|
190
|
+
"move: the human decides. In their own terminal:\n %s" % (runs, max_runs, budget_name, cmd), 6)
|
|
186
191
|
via, why = dialog.human_route(root, allow_dialog=True, rt=rt)
|
|
187
192
|
if via is None:
|
|
188
193
|
fs.die("APPROVAL_REFUSED — a run past the suite budget is the human's, from their own terminal."
|