riskrails 3.1.0 → 3.2.2
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 +12 -11
- package/bin/riskrails.js +109 -18
- package/hooks/ai-path-guard-defaults.json +3 -2
- package/hooks/context-guard.py +14 -11
- package/hooks/dialog-gate.py +1 -1
- package/hooks/guard/dialog.py +107 -27
- package/hooks/guard/git.py +12 -3
- package/hooks/guard/paths.py +109 -7
- package/hooks/project-scaffold.py +2 -1
- package/hooks/runtime-gate.py +128 -27
- package/hooks/update_notice.py +3 -3
- package/package.json +1 -1
- package/skills/ai-init/templates/.ai/state/README.md +4 -4
- package/skills/ai-task/rr_state/__init__.py +1 -0
- package/skills/ai-task/rr_state/cli.py +210 -0
- package/skills/ai-task/rr_state/close.py +171 -0
- package/skills/ai-task/rr_state/dialog.py +541 -0
- package/skills/ai-task/rr_state/dialog_cmd.py +164 -0
- package/skills/ai-task/rr_state/fs.py +220 -0
- package/skills/ai-task/rr_state/gate.py +125 -0
- package/skills/ai-task/rr_state/gate_cmd.py +128 -0
- package/skills/ai-task/rr_state/handoff.py +226 -0
- package/skills/ai-task/rr_state/journal.py +126 -0
- package/skills/ai-task/rr_state/labels.py +99 -0
- package/skills/ai-task/rr_state/ledger.py +145 -0
- package/skills/ai-task/rr_state/locks.py +84 -0
- package/skills/ai-task/rr_state/memory_cmd.py +310 -0
- package/skills/ai-task/rr_state/policy.py +378 -0
- package/skills/ai-task/rr_state/profile.py +373 -0
- package/skills/ai-task/rr_state/questions.py +336 -0
- package/skills/ai-task/rr_state/questions_cmd.py +321 -0
- package/skills/ai-task/rr_state/report.py +271 -0
- package/skills/ai-task/rr_state/report_cmd.py +230 -0
- package/skills/ai-task/rr_state/review.py +165 -0
- package/skills/ai-task/rr_state/runtime.py +80 -0
- package/skills/ai-task/rr_state/start.py +257 -0
- package/skills/ai-task/rr_state/steps.py +131 -0
- package/skills/ai-task/rr_state/task.py +396 -0
- package/skills/ai-task/rr_state/verify.py +231 -0
- package/skills/ai-task/sensors.py +16 -3
- package/skills/ai-task/state.py +24 -5161
- package/skills/project-update/adopt.py +38 -24
- package/skills/project-update/history/blobs/a51517e13e57420d2d88a6bcfb4055d762d4a0d10a8f88450929c5c4572213a9 +67 -0
- package/skills/project-update/history/index.json +2 -1
- package/skills/project-update/migrations/0003_context_diet.py +1 -1
- package/skills/project-update/migrations/0007_weight_model.py +130 -23
- package/skills/project-update/migrations/README.md +16 -1
- package/skills/project-update/update.py +202 -26
- package/skills/rr-setup/SKILL.md +9 -2
- package/skills/rr-status/status.py +36 -9
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""locks: the two locks that serialise writers of the questions file and the consent ledger.
|
|
2
|
+
|
|
3
|
+
LOCK_DEPTH: shell
|
|
4
|
+
question_lock: shell
|
|
5
|
+
LOCK_STALE: core
|
|
6
|
+
ledger_lock: shell
|
|
7
|
+
"""
|
|
8
|
+
import contextlib
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import fcntl
|
|
14
|
+
except ImportError: # not POSIX: the journal falls back to O_APPEND alone
|
|
15
|
+
fcntl = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# One counter per process, held in a list changed in place: `global` is refused under
|
|
19
|
+
# rr_state/ (R4), and a per-object counter would deadlock on the second descriptor
|
|
20
|
+
# (the gate locks the questions file and write_task_md locks it again inside).
|
|
21
|
+
LOCK_DEPTH = [0]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@contextlib.contextmanager
|
|
25
|
+
def question_lock(path):
|
|
26
|
+
"""The questions are read, modified and written back. Without a lock across
|
|
27
|
+
that window two `ask` calls lose a block — and the journal would then name a
|
|
28
|
+
question the file does not hold, which nothing would ever block on.
|
|
29
|
+
|
|
30
|
+
Re-entrant within the process: flock is per open file description, so a
|
|
31
|
+
nested acquisition on a second fd would wait for the first for ever. That is
|
|
32
|
+
what lets the gate decide and apply inside one critical section."""
|
|
33
|
+
directory = os.path.dirname(path)
|
|
34
|
+
os.makedirs(directory, exist_ok=True)
|
|
35
|
+
if fcntl is None or LOCK_DEPTH[0]:
|
|
36
|
+
LOCK_DEPTH[0] += 1
|
|
37
|
+
try:
|
|
38
|
+
yield
|
|
39
|
+
finally:
|
|
40
|
+
LOCK_DEPTH[0] -= 1
|
|
41
|
+
return
|
|
42
|
+
# The lock is taken on the directory itself, so the audit trail gains no
|
|
43
|
+
# lock file and the lock survives the os.replace that swaps the inode.
|
|
44
|
+
try:
|
|
45
|
+
fd = os.open(directory, os.O_RDONLY)
|
|
46
|
+
except OSError:
|
|
47
|
+
yield
|
|
48
|
+
return
|
|
49
|
+
LOCK_DEPTH[0] += 1
|
|
50
|
+
try:
|
|
51
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
LOCK_DEPTH[0] -= 1
|
|
55
|
+
os.close(fd)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
LOCK_STALE = 2.0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@contextlib.contextmanager
|
|
62
|
+
def ledger_lock(root):
|
|
63
|
+
"""consent.lock by O_EXCL, portable where flock is not; a lock older than
|
|
64
|
+
LOCK_STALE belongs to a writer that died and is taken over (R36.6)."""
|
|
65
|
+
path = os.path.join(root, ".ai", "state", "consent.lock")
|
|
66
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
67
|
+
deadline = time.time() + 3 * LOCK_STALE
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
os.close(os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600))
|
|
71
|
+
break
|
|
72
|
+
except FileExistsError:
|
|
73
|
+
with contextlib.suppress(OSError):
|
|
74
|
+
if time.time() - os.stat(path).st_mtime > LOCK_STALE:
|
|
75
|
+
os.remove(path)
|
|
76
|
+
continue
|
|
77
|
+
if time.time() > deadline:
|
|
78
|
+
raise OSError("consent.lock is held and cannot be taken over") from None
|
|
79
|
+
time.sleep(0.02)
|
|
80
|
+
try:
|
|
81
|
+
yield
|
|
82
|
+
finally:
|
|
83
|
+
with contextlib.suppress(OSError):
|
|
84
|
+
os.remove(path)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""memory_cmd: the `memory` and `recall` commands over the memory_store and memory_sync modules -- the actions, the human's route, the risk record.
|
|
2
|
+
|
|
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
|
+
core: MEMORY_ACTIONS
|
|
5
|
+
"""
|
|
6
|
+
# pylint: disable=protected-access # cmd_recall, memory_search and memory_risk call fs._split_files, which partition.json assigns to fs
|
|
7
|
+
# pylint: disable=unused-argument # every action in MEMORY_ACTIONS is called with the same arguments, rt included; most never read it
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shlex
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from rr_state import fs, journal, task
|
|
14
|
+
from rr_state import dialog as rr_dialog
|
|
15
|
+
from rr_state import runtime as rr_runtime
|
|
16
|
+
|
|
17
|
+
# sensors.py sits beside state.py; the entry puts that directory on sys.path.
|
|
18
|
+
try:
|
|
19
|
+
import sensors # measurement only; it never writes this file
|
|
20
|
+
except ImportError: # an old install that shipped state.py alone
|
|
21
|
+
sensors = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def memory_modules():
|
|
25
|
+
"""memory_sync and memory_store, imported only by the commands that use them."""
|
|
26
|
+
try:
|
|
27
|
+
import memory_store # pylint: disable=import-outside-toplevel
|
|
28
|
+
import memory_sync # pylint: disable=import-outside-toplevel
|
|
29
|
+
except ImportError as exc:
|
|
30
|
+
fs.die("the memory modules are not installed beside state.py (%s)" % exc, 2)
|
|
31
|
+
return memory_sync, memory_store
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def memory_config(sync, root):
|
|
35
|
+
try:
|
|
36
|
+
return sync.load_config(root)
|
|
37
|
+
except sync.MemoryError_ as exc:
|
|
38
|
+
fs.die(str(exc), exc.code)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def memory_human(root, what, rt=rr_runtime.Runtime()):
|
|
43
|
+
"""R16: enable, disable, cleanup, pack, redact and move-logs take the human's route, as approve does."""
|
|
44
|
+
via, why = rr_dialog.human_route(root, rt=rt)
|
|
45
|
+
if via is None:
|
|
46
|
+
fs.die("APPROVAL_REFUSED — 'memory %s' is the human's, from their own terminal:\n python3 %s --root %s "
|
|
47
|
+
"%s\nRefused because %s." % (what, shlex.quote(fs.ENTRY), shlex.quote(root),
|
|
48
|
+
" ".join(shlex.quote(a) for a in sys.argv[sys.argv.index("memory"):]
|
|
49
|
+
if "memory" in sys.argv), why), 5)
|
|
50
|
+
return via
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def memory_confirm(cfg, name, what):
|
|
54
|
+
if not name:
|
|
55
|
+
fs.die("'memory %s' deletes or moves files: add --confirm-delete %s" % (what, cfg.author), 2)
|
|
56
|
+
if name != cfg.author:
|
|
57
|
+
fs.die("--confirm-delete names %r; this clone's author is %r (.ai/local/memory/config.json)"
|
|
58
|
+
% (name, cfg.author), 2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_recall(args, root, rt=rr_runtime.Runtime()):
|
|
62
|
+
"""R10: the open risks, residual findings, decisions, scope changes and task
|
|
63
|
+
one-liners that touch these files, within the budget. Read-only."""
|
|
64
|
+
sync, store = memory_modules()
|
|
65
|
+
cfg = memory_config(sync, root)
|
|
66
|
+
files = fs._split_files(args.files)
|
|
67
|
+
if not files:
|
|
68
|
+
fs.die("recall needs --files \"a,b,dir/**\"", 2)
|
|
69
|
+
kinds = fs._split_files(args.kinds) or None
|
|
70
|
+
history = store.open_history(root, cfg)
|
|
71
|
+
budget = args.budget if args.budget is not None else cfg.recall_budget
|
|
72
|
+
text, result = history.recall(files, budget, cfg.recall_max_items, kinds, args.all_risks, args.offset)
|
|
73
|
+
if args.json:
|
|
74
|
+
print(json.dumps({"schema": 1, "files": files, "budget": budget, "used": result["used"],
|
|
75
|
+
"items": [{k: i[k] for k in ("rid", "kind", "task", "ts", "line", "src")}
|
|
76
|
+
for i in result["items"]],
|
|
77
|
+
"more": result["more"], "next": result["next"], "source": history.source()},
|
|
78
|
+
ensure_ascii=False, sort_keys=True))
|
|
79
|
+
return
|
|
80
|
+
sys.stdout.write(text)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_memory(args, root, rt=rr_runtime.Runtime()):
|
|
84
|
+
sync, store = memory_modules()
|
|
85
|
+
action = args.memory_action
|
|
86
|
+
try:
|
|
87
|
+
MEMORY_ACTIONS[action](args, root, sync, store, rt=rt)
|
|
88
|
+
except sync.MemoryError_ as exc:
|
|
89
|
+
fs.die(str(exc), exc.code)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def memory_status(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
93
|
+
lines = sync.status_lines(root)
|
|
94
|
+
if args.json:
|
|
95
|
+
print(json.dumps(dict(line.split(": ", 1) for line in lines), ensure_ascii=False, sort_keys=True))
|
|
96
|
+
return
|
|
97
|
+
print("\n".join(lines))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def memory_enable(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
101
|
+
memory_human(root, "enable", rt=rt)
|
|
102
|
+
hook = sync.enable(root, args.author, args.period, args.index)
|
|
103
|
+
print("memory: packages mode on (%s); every teammate's next close exports" % sync.POLICY_REL)
|
|
104
|
+
print("pre-push: %s" % hook)
|
|
105
|
+
if hook in ("foreign", "hookspath"):
|
|
106
|
+
print("not written — %s. Add these lines to your own pre-push hook:\n%s" % (
|
|
107
|
+
"core.hooksPath is set" if hook == "hookspath" else "a pre-push hook exists", sync.snippet_text()))
|
|
108
|
+
print("\n".join(sync.status_lines(root)))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def memory_disable(_args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
112
|
+
memory_human(root, "disable", rt=rt)
|
|
113
|
+
print("memory: files mode; packages and local data kept; pre-push: %s" % sync.disable(root))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def memory_export(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
117
|
+
cfg = memory_config(sync, root)
|
|
118
|
+
ids = "pending" if args.pending or not args.ids else [task.valid_task_id(t, "ID") for t in args.ids]
|
|
119
|
+
result = sync.export(root, ids, cfg)
|
|
120
|
+
for line in sync.record_lines(result):
|
|
121
|
+
print(line)
|
|
122
|
+
print("exported: +%d entries%s" % (result.added, "" if result.added else " — nothing pending"))
|
|
123
|
+
for task_id, why in result.failures:
|
|
124
|
+
print("memory: export of %s failed (%s)" % (task_id, why))
|
|
125
|
+
if result.failures:
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def memory_verify(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
130
|
+
stats = None
|
|
131
|
+
if args.range:
|
|
132
|
+
if ".." not in args.range:
|
|
133
|
+
fs.die("--range takes OLD..NEW", 2)
|
|
134
|
+
old, new = args.range.split("..", 1)
|
|
135
|
+
findings, stats = sync.validate_range(root, sync.rev(root, old), sync.rev(root, new) or new, local=False)
|
|
136
|
+
findings = findings or []
|
|
137
|
+
elif args.fresh_clone:
|
|
138
|
+
findings = sync.verify_fresh_clone(root)
|
|
139
|
+
else:
|
|
140
|
+
findings = sync.verify(root, "all", [task.valid_task_id(t, "ID") for t in args.ids] or None)
|
|
141
|
+
failing = [f for f in findings if not f.check.endswith("-warn")]
|
|
142
|
+
if args.json:
|
|
143
|
+
print(json.dumps({"findings": [f._asdict() for f in findings], "ok": not failing, "stats": stats},
|
|
144
|
+
ensure_ascii=False, sort_keys=True))
|
|
145
|
+
else:
|
|
146
|
+
for f in findings:
|
|
147
|
+
print(sync.format_finding(f))
|
|
148
|
+
print("memory verify: %s" % ("ok" if not failing else "%d finding(s)" % len(failing)))
|
|
149
|
+
if failing:
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def memory_show(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
154
|
+
print("\n".join(sync.show(root, task.valid_task_id(args.id, "ID"), "json" if args.json else "md", args.entry)))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def memory_search(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
158
|
+
cfg = memory_config(sync, root)
|
|
159
|
+
filters = {"kind": args.kind, "by": args.by, "task": args.task, "since": args.since, "until": args.until,
|
|
160
|
+
"files": fs._split_files(args.file) or None}
|
|
161
|
+
history = store.open_history(root, cfg)
|
|
162
|
+
hits, total = history.search(args.query or "", filters, args.limit, args.offset)
|
|
163
|
+
if args.json:
|
|
164
|
+
print(json.dumps({"schema": 1, "query": args.query or "", "total": total, "offset": args.offset,
|
|
165
|
+
"items": [{"rid": e["rid"], "kind": e["kind"], "task": e.get("task"), "ts": e.get("ts"),
|
|
166
|
+
"line": store.mf.search_line(e)} for e in hits],
|
|
167
|
+
"source": history.source()}, ensure_ascii=False, sort_keys=True))
|
|
168
|
+
return
|
|
169
|
+
print("# Search — \"%s\" · %d of %d matches · offset %d · index %s" % (
|
|
170
|
+
args.query or "", len(hits), total, args.offset, history.source()["index"]))
|
|
171
|
+
for e in hits:
|
|
172
|
+
print("- %s" % store.mf.search_line(e))
|
|
173
|
+
if args.offset + len(hits) < total:
|
|
174
|
+
print("… %d more: --offset %d" % (total - args.offset - len(hits), args.offset + len(hits)))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def risk_scope(state):
|
|
178
|
+
"""C4 (§D9): with a task in flight, files mode edits known-risks.md only
|
|
179
|
+
when the task names it."""
|
|
180
|
+
rel = ".ai/project/known-risks.md"
|
|
181
|
+
if state and not sensors.scope_any(rel, task.task_files(state)):
|
|
182
|
+
fs.die("SCOPE_CHANGE_REQUIRED — 'memory risk' writes %s, which task %s does not name:\n"
|
|
183
|
+
" state.py start --amend --files \"+%s\" --why \"<the risk you record>\"" % (rel, state["task_id"], rel),
|
|
184
|
+
6)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def memory_risk(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
188
|
+
cfg = memory_config(sync, root)
|
|
189
|
+
state = task.in_flight(root, rt=rt)
|
|
190
|
+
if args.risk_action == "add":
|
|
191
|
+
where = fs._split_files(args.where)
|
|
192
|
+
if args.label not in store.mf.LABELS:
|
|
193
|
+
fs.die("--label takes one of: %s" % ", ".join(store.mf.LABELS), 2)
|
|
194
|
+
if args.status not in store.mf.RISK_STATUS:
|
|
195
|
+
fs.die("--status takes one of: %s" % ", ".join(store.mf.RISK_STATUS), 2)
|
|
196
|
+
if not cfg.packages:
|
|
197
|
+
risk_scope(state)
|
|
198
|
+
found = "%s, by %s" % (fs.now()[:10], state["task_id"] if state else "no task")
|
|
199
|
+
rkey = sync.risk_add_files(root, args.title, where, args.label, args.what, args.likely, args.fix,
|
|
200
|
+
args.status, found)
|
|
201
|
+
print("risk: %s added to .ai/project/known-risks.md" % rkey)
|
|
202
|
+
return
|
|
203
|
+
data = sync.risk_data(args.title, where, args.label, args.what, args.likely, args.fix, args.status)
|
|
204
|
+
else:
|
|
205
|
+
if args.status not in ("fixed", "accepted", "ticketed"):
|
|
206
|
+
fs.die("--status takes fixed, accepted or ticketed", 2)
|
|
207
|
+
if not cfg.packages:
|
|
208
|
+
risk_scope(state)
|
|
209
|
+
sync.risk_close_files(root, args.rkey, args.status, args.why)
|
|
210
|
+
print("risk: %s is %s" % (args.rkey, args.status))
|
|
211
|
+
return
|
|
212
|
+
history = store.FileHistory(root, cfg)
|
|
213
|
+
found = [e for e in history.risks("all") if e.get("rkey") == args.rkey]
|
|
214
|
+
if not found:
|
|
215
|
+
fs.die("no risk %s ('state.py memory risks --status all')" % args.rkey, 1)
|
|
216
|
+
last = found[0]
|
|
217
|
+
data = sync.risk_data(last["title"], last.get("where"), last.get("label"), last.get("what"),
|
|
218
|
+
last.get("likelihood"), last.get("fix"), args.status, args.why, last["rid"])
|
|
219
|
+
if state:
|
|
220
|
+
state = task.load(root, rt=rt)
|
|
221
|
+
journal.emit(root, state, "risk_recorded", "%s: %s" % (data["rkey"], fs.one_line(data["title"], 200)), data, rt=rt)
|
|
222
|
+
task.save(root, state)
|
|
223
|
+
print("risk: %s recorded in %s's journal (exported at close)" % (data["rkey"], state["task_id"]))
|
|
224
|
+
return
|
|
225
|
+
entry = sync.standalone_risk(root, cfg, data, rt.name)
|
|
226
|
+
print("risk: %s recorded (%s)" % (data["rkey"], entry["rid"]))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def memory_risks(args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
230
|
+
cfg = memory_config(sync, root)
|
|
231
|
+
risks = store.open_history(root, cfg).risks(args.status)
|
|
232
|
+
if args.json:
|
|
233
|
+
for e in risks:
|
|
234
|
+
print(store.mf.canonical(e).decode("utf-8"))
|
|
235
|
+
return
|
|
236
|
+
if args.md:
|
|
237
|
+
out = ["# Known risks", ""]
|
|
238
|
+
for e in risks:
|
|
239
|
+
out += (e["text"].split("\n") if e.get("text") else store.mc.risk_section(
|
|
240
|
+
e.get("title"), store.mf.where_list(e.get("where")), e.get("label"), e.get("what"),
|
|
241
|
+
e.get("likelihood"), e.get("fix"), "%s%s" % (e.get("status"), " — %s" % e["status_why"]
|
|
242
|
+
if e.get("status_why") else ""),
|
|
243
|
+
e.get("found_by") or "-")) + [""]
|
|
244
|
+
print("\n".join(out).rstrip("\n"))
|
|
245
|
+
return
|
|
246
|
+
print("# Risks — %s · %d" % (args.status, len(risks)))
|
|
247
|
+
for e in risks:
|
|
248
|
+
where = store.mf.where_list(e.get("where"))
|
|
249
|
+
print("- [%s %s] %s — %s (%s)" % (e.get("status"), e.get("label"), where[0] if where else "-",
|
|
250
|
+
e.get("title"), e.get("rkey")))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def memory_migrate(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
254
|
+
cfg = memory_config(sync, root)
|
|
255
|
+
if args.move_logs:
|
|
256
|
+
memory_human(root, "migrate --move-logs", rt=rt)
|
|
257
|
+
memory_confirm(cfg, args.confirm_delete, "migrate --move-logs")
|
|
258
|
+
moved, command = sync.move_logs(root)
|
|
259
|
+
print("moved %d committed log(s) into .ai/state/logs/<id>/legacy/; now, as its own commit (C3):\n %s"
|
|
260
|
+
% (len(moved), command))
|
|
261
|
+
return
|
|
262
|
+
if args.apply:
|
|
263
|
+
print("\n".join(sync.migrate_apply(root, risks=args.risks)))
|
|
264
|
+
return
|
|
265
|
+
report = sync.migrate_report(root)
|
|
266
|
+
print("report: %s (%d lines) — read it, then 'state.py memory migrate --apply'"
|
|
267
|
+
% (os.path.join(sync.LOCAL_REL, "migration-report.md"), report.count("\n")))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def memory_rebuild(_args, root, sync, store, rt=rr_runtime.Runtime()):
|
|
271
|
+
cfg = memory_config(sync, root)
|
|
272
|
+
print("index: rebuilt from %d sources (%s)" % (store.SqliteHistory(root, cfg).rebuild(), store.INDEX_REL))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def memory_cleanup(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
276
|
+
cfg = memory_config(sync, root)
|
|
277
|
+
memory_human(root, "cleanup", rt=rt)
|
|
278
|
+
memory_confirm(cfg, args.confirm_delete, "cleanup")
|
|
279
|
+
if not args.all and not args.ids:
|
|
280
|
+
fs.die("memory cleanup takes IDs or --all", 2)
|
|
281
|
+
print("\n".join(sync.cleanup(root, "all" if args.all else [task.valid_task_id(t, "ID") for t in args.ids])))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def memory_restore(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
285
|
+
if not args.all and not args.ids:
|
|
286
|
+
fs.die("memory restore takes IDs or --all", 2)
|
|
287
|
+
print("\n".join(sync.restore(root, "all" if args.all else [task.valid_task_id(t, "ID") for t in args.ids])))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def memory_pack(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
291
|
+
cfg = memory_config(sync, root)
|
|
292
|
+
memory_human(root, "pack", rt=rt)
|
|
293
|
+
memory_confirm(cfg, args.confirm_delete, "pack")
|
|
294
|
+
target, removed, count = sync.pack(root, args.period)
|
|
295
|
+
print("pack: %s (%d records; %d batch(es) removed)" % (os.path.join(sync.MEMORY_REL, target), count,
|
|
296
|
+
len(removed)))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def memory_redact(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
300
|
+
via = memory_human(root, "redact", rt=rt)
|
|
301
|
+
rid, name = sync.redact(root, args.rid, args.field, args.why, by="%s via %s" % (os.environ.get("USER") or
|
|
302
|
+
"unknown", via))
|
|
303
|
+
print("redacted: %s in %s (new rid %s)" % (args.field, os.path.join(sync.MEMORY_REL, name), rid))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
MEMORY_ACTIONS = {"status": memory_status, "enable": memory_enable, "disable": memory_disable,
|
|
307
|
+
"export": memory_export, "verify": memory_verify, "show": memory_show, "search": memory_search,
|
|
308
|
+
"risk": memory_risk, "risks": memory_risks, "migrate": memory_migrate, "rebuild": memory_rebuild,
|
|
309
|
+
"cleanup": memory_cleanup, "restore": memory_restore, "pack": memory_pack,
|
|
310
|
+
"redact": memory_redact}
|