riskrails 4.2.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/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_base.py +7 -0
- package/hooks/guard/paths_bash.py +3 -3
- package/hooks/runtime-gate.py +87 -0
- package/hooks/update_notice.py +23 -8
- package/package.json +1 -1
- package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
- 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/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/index.json +6 -3
- package/skills/rr-build/SKILL.md +5 -2
- 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,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."
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Coding policy
|
|
2
|
+
|
|
3
|
+
<!-- /ai-init fills the project-specific half of this file from the repository.
|
|
4
|
+
The rules above the line are general and apply everywhere. -->
|
|
5
|
+
|
|
6
|
+
## General
|
|
7
|
+
|
|
8
|
+
- Follow the conventions already in the file you are editing, even when they are
|
|
9
|
+
not the conventions you would choose. Consistency inside a file beats
|
|
10
|
+
correctness in the abstract.
|
|
11
|
+
- One logical change per step. If you find yourself writing "and also", stop.
|
|
12
|
+
- No new abstraction until there are three call sites that need it.
|
|
13
|
+
- Extend through the framework's own extension points before writing a new
|
|
14
|
+
mechanism: decorators, event subscribers, form extensions, template overrides,
|
|
15
|
+
compiler passes, configuration.
|
|
16
|
+
- Never modify `vendor/` or `node_modules/`. A needed change to a third-party
|
|
17
|
+
package becomes a decorator, an event listener, or a patch that is applied on
|
|
18
|
+
install and reviewable in git.
|
|
19
|
+
- Dead code is deleted only in a task that is about deleting it, with evidence
|
|
20
|
+
that it is dead.
|
|
21
|
+
- A function does one thing and its name says which. When the name needs
|
|
22
|
+
"and", split it.
|
|
23
|
+
- Inputs come in as parameters and the result goes out as the return value;
|
|
24
|
+
files, the network and the clock stay at the edge, in the caller.
|
|
25
|
+
- An error message says what failed, what was expected, what was found, and
|
|
26
|
+
the way out.
|
|
27
|
+
|
|
28
|
+
In existing code the file's conventions win; the last three lines hold for the
|
|
29
|
+
functions you add.
|
|
30
|
+
|
|
31
|
+
## Comments
|
|
32
|
+
|
|
33
|
+
Write the comment that explains *why*, and only when the reason is not visible in
|
|
34
|
+
the code. Do not narrate what the next line does. Do not leave commented-out
|
|
35
|
+
code; git remembers it.
|
|
36
|
+
|
|
37
|
+
## Project specifics
|
|
38
|
+
|
|
39
|
+
<!-- KNOWN FACT entries only: the linters that actually run, the static-analysis
|
|
40
|
+
level actually configured, the naming and layering the code actually uses.
|
|
41
|
+
Cite the config file for each. -->
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"ai-init/.ai/agents/implementer.md": [
|
|
41
41
|
"7e5c824bb71fe5353ec8a67827145b8fbc22ecd902ade1ad3005fafeed4ffb71",
|
|
42
42
|
"94da08ac6945d5614cc7918d450aab94f3aa8a929a1c8f9fa9a0a9ac51176287",
|
|
43
|
-
"1fe2f3d3bfe2d1b671aa759b89ac1d88ae73eefed2208a485c9e972fa7b079e6"
|
|
43
|
+
"1fe2f3d3bfe2d1b671aa759b89ac1d88ae73eefed2208a485c9e972fa7b079e6",
|
|
44
|
+
"71364f3e392fe460b5b32cfec8de047a0aae8f88f144d110f90a329163677d42"
|
|
44
45
|
],
|
|
45
46
|
"ai-init/.ai/agents/manager.md": [
|
|
46
47
|
"0aeb04b04262683cac6513545683486a73dc473b447bba43b5aaab18403957a6",
|
|
@@ -67,7 +68,8 @@
|
|
|
67
68
|
"e71abd1823fbbe6e635b141f04a301c3d24c2cdd56c7a5c2b25c7ea1898be54f",
|
|
68
69
|
"be34ab013c39ace1e1ec902b9bcfe9653ba6aaeddb9df63fde2d2aa17bfb2263",
|
|
69
70
|
"6956a4c991262b5733645f29d7067c091c1126721623b07275b4f276560ec28e",
|
|
70
|
-
"707e67834ec3295705c4e391b794d8600a3fbc3c02dda16db50035f84e6b47c9"
|
|
71
|
+
"707e67834ec3295705c4e391b794d8600a3fbc3c02dda16db50035f84e6b47c9",
|
|
72
|
+
"499e51e1a393f776aa425aeb2be575f0a58eb483d83bc9f1c7da94ff38c3fb0e"
|
|
71
73
|
],
|
|
72
74
|
"ai-init/.ai/agents/risk.md": [
|
|
73
75
|
"d7146a591898984115124c83174a2c93cc080ad594f132f3d89c8b41b006c805",
|
|
@@ -83,7 +85,8 @@
|
|
|
83
85
|
"7f713c6ecd00bc2379deda511d8bda786341dbe5f5187f3b899302d904fed6bc"
|
|
84
86
|
],
|
|
85
87
|
"ai-init/.ai/policies/coding.md": [
|
|
86
|
-
"3dd55bf5f9d9471c68af3353166da11593a11e639f7557397ce46787291fa629"
|
|
88
|
+
"3dd55bf5f9d9471c68af3353166da11593a11e639f7557397ce46787291fa629",
|
|
89
|
+
"dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a"
|
|
87
90
|
],
|
|
88
91
|
"ai-init/.ai/policies/context-management.md": [
|
|
89
92
|
"230df2220e9c73c563c3c926d26f5c6dbe249df26a56fa43b7b321cdee2d5783",
|
package/skills/rr-build/SKILL.md
CHANGED
|
@@ -157,10 +157,13 @@ The batch is `[{"question": "…", "options": [{"key": "A", "text": "…"}, …]
|
|
|
157
157
|
`"A: text"` becomes `{"key": "A", "text": "text"}`.
|
|
158
158
|
|
|
159
159
|
Option keys are single letters, A first, at least two; `--recommend` marks the
|
|
160
|
-
one you would pick.
|
|
160
|
+
one you would pick. If `ask` printed `DIALOG_PENDING`, send that dialog. With no
|
|
161
|
+
decision pending (a free question-tool call is denied while one is), put the
|
|
162
|
+
question through the question tool and record the pick with
|
|
163
|
+
`$STATE answer Q1=B --via picker`. Otherwise **stop** and show the questions: while one is pending,
|
|
161
164
|
`step-done` and `close` exit 4. The human answers in the conversation — record
|
|
162
165
|
it with `$STATE answer Q1=B Q2=X:"free text"` or `--prose "1B 2A"` — or fills the
|
|
163
|
-
`[Answer]:` line in `.ai/reports/<task-id>/task.md` §Questions
|
|
166
|
+
`[Answer]:` line in `.ai/reports/<task-id>/task.md` §Questions; that file stays the
|
|
164
167
|
record. Never edit that file, and never answer on the human's behalf.
|
|
165
168
|
|
|
166
169
|
## 6. Hand over by mode
|
package/skills/rr-ship/SKILL.md
CHANGED
|
@@ -51,8 +51,8 @@ approve in your own terminal, then run close again:
|
|
|
51
51
|
python3 /home/…/skills/ai-task/state.py --root /path/to/project approve --by "<name>"
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Show the human the summary (§3), the
|
|
55
|
-
command **verbatim** — paths absolute and quoted, because they paste it into
|
|
54
|
+
Show the human the summary (§3), the review's `READ FIRST` lines, the open
|
|
55
|
+
findings, the rollback, and that command **verbatim** — paths absolute and quoted, because they paste it into
|
|
56
56
|
their own shell, where `$AI_HOME` is not set. Then **stop**.
|
|
57
57
|
|
|
58
58
|
- **You approve only through the dialog.** With `.ai/policies/dialog.json`
|
package/skills/rr-spec/SKILL.md
CHANGED
|
@@ -39,6 +39,12 @@ $STATE ask --topic <slug> "<one line>" --option "A: <text>" --option "B: <text>"
|
|
|
39
39
|
$STATE answer --topic <slug> Q1=B # or --prose "1B", or Q1=X:"free text"
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
If `ask` printed `DIALOG_PENDING`, send that dialog. With no decision pending (a
|
|
43
|
+
free question-tool call is denied while one is), put the question through the
|
|
44
|
+
question tool and relay the pick with `$STATE answer --topic <slug> Q1=B --via
|
|
45
|
+
picker`; the questions file stays the record, and the human may still answer in
|
|
46
|
+
the chat or fill `[Answer]:`.
|
|
47
|
+
|
|
42
48
|
If `superpowers:brainstorming` is available, let it drive the conversation; the
|
|
43
49
|
questions and answers still go through `ask` and `answer`. Otherwise:
|
|
44
50
|
|
|
@@ -16,6 +16,8 @@ done
|
|
|
16
16
|
python3 -I "$AI_HOME/skills/rr-status/status.py" --root "$PWD"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
With a task in flight or a decision open, the first line says who it waits on (`waiting on you` or `waiting on the agent`).
|
|
20
|
+
|
|
19
21
|
After the output, at most one line, and only when a line asks for it:
|
|
20
22
|
|
|
21
23
|
- `root:` is an ancestor of this repository, or `$HOME`: that `.ai/` arms the
|
|
@@ -174,9 +174,11 @@ def main(argv=None):
|
|
|
174
174
|
return 1
|
|
175
175
|
lines, task_id = None, None
|
|
176
176
|
state = read_json(state_file(root))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
#
|
|
177
|
+
decisions = read_json(os.path.join(root, ".ai", "state", "decision.json")).get("open")
|
|
178
|
+
if state.get("schema") == 7 or (not state.get("task_id") and decisions):
|
|
179
|
+
# A schema-7 task, or an open decision with no task, is state.py's to describe
|
|
180
|
+
# (its first line says who waits); this file's own read stays for an older
|
|
181
|
+
# task, and for an install whose state.py cannot answer.
|
|
180
182
|
done = subprocess.run([sys.executable, "-I", os.path.join(SKILLS, "ai-task", "state.py"),
|
|
181
183
|
"--root", root, "status"], capture_output=True, text=True, check=False)
|
|
182
184
|
if done.returncode == 0 and done.stdout.strip():
|