loki-mode 11.84.0 → 11.85.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/grill.sh +8 -3
- package/autonomy/lib/check_author.py +191 -2
- package/autonomy/lib/claude-flags.sh +10 -4
- package/autonomy/prd-checklist.sh +162 -0
- package/autonomy/run.sh +4 -4
- package/autonomy/spec.sh +87 -0
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/data/child-env.json +1 -0
- package/loki-ts/dist/loki.js +1412 -1311
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/ask-tools-server.js +144 -19
- package/packages/control-plane/dist/server.js +12364 -11697
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-CXLv81So.js → AdminPage-CIcHJlAf.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dkquf4s9.js → Avatar-fSbN52Rn.js} +1 -1
- package/web-app/dist/assets/{Badge-BPlO-Wiw.js → Badge-DV7TUsqR.js} +1 -1
- package/web-app/dist/assets/{Button-BO-JvVyh.js → Button-D16_Kixn.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DCuXv1OL.js → CockpitPage-DqH6iBNN.js} +1 -1
- package/web-app/dist/assets/{ComparePage-zVSpiuEp.js → ComparePage-D85PNjoa.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-Ci4wqP2a.js → ErrorBoundary-BeT6cXcK.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CEBhqO60.js → EvidenceReceiptPanel-CUH-iYKA.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-UR6xkpVn.js → GitHubIssuesPanel-DWdiqzHn.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BspmPZ6K.js → GitHubPRsPanel-DsH26a3x.js} +1 -1
- package/web-app/dist/assets/{HomePage-DchBOs8o.js → HomePage-z85a_1rL.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BldlfUlE.js → LoginPage-BVQ8VH-D.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CVnhnrh6.js → MagicPage-Dn5JMfWl.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-I_Ye6IjS.js → MetricsPage-CbCGiyKZ.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CCDI4T7K.js → NotFoundPage-j4jjSqMS.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DuyA8Qz7.js → ProjectPage-aOWsJjD9.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BF2JSz1r.js → ProjectsPage-B84wO9Cr.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-B_5LeF6K.js → SettingsPage-B2Ug7j2E.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-lE5cIgbU.js → ShowcasePage-c-8uJKMj.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-v2DJyogx.js → SystemSettingsPage-tTg7Sn1W.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-eXMmORI1.js → TeamsPage-CU9pwIFY.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CkHulSrT.js → TemplatesPage-DSILwTWB.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DJwjzb2Y.js → TerminalOutput-BVBNxLfo.js} +1 -1
- package/web-app/dist/assets/{activity-CAtLpfNe.js → activity-D3dTgTol.js} +1 -1
- package/web-app/dist/assets/{bell-CL-IIbCA.js → bell-nKHHBPy4.js} +1 -1
- package/web-app/dist/assets/{bot-fT57nwl5.js → bot-Cs1wMX37.js} +1 -1
- package/web-app/dist/assets/{check-hWhzzw0m.js → check-DvE_DNJj.js} +1 -1
- package/web-app/dist/assets/{chevron-left-BLHNp2u-.js → chevron-left-BafCbx7C.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BiPOqU-L.js → circle-alert-CoVthuJC.js} +1 -1
- package/web-app/dist/assets/{clock-Cqcvrmkj.js → clock-D8xfN8aa.js} +1 -1
- package/web-app/dist/assets/{cloud-DUijmoIE.js → cloud-BL8Y_LIc.js} +1 -1
- package/web-app/dist/assets/{code-xml-CHpO2Oxh.js → code-xml-BJC8Ey7k.js} +1 -1
- package/web-app/dist/assets/{database-DggasNHP.js → database-CGA6URCb.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CXxG_8K6.js → dollar-sign-CHRvwgyL.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-xfVvREvW.js → file-code-corner-CQ3uLV2P.js} +1 -1
- package/web-app/dist/assets/{file-plus-CxboFD8a.js → file-plus-Sqdt7xLt.js} +1 -1
- package/web-app/dist/assets/{globe-uQo7wmFo.js → globe-BiXQ5FOy.js} +1 -1
- package/web-app/dist/assets/{hammer-DpmqB3Jc.js → hammer-CUwt2o7I.js} +1 -1
- package/web-app/dist/assets/{index-DSecwFL3.js → index-BYowAgK4.js} +3 -3
- package/web-app/dist/assets/{layers-DAhv2mJy.js → layers-DXq9VSwW.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DlfD5ae4.js → loader-circle-Ctn_xD3_.js} +1 -1
- package/web-app/dist/assets/{lock-Bp9L8WT9.js → lock-nl9XJEyi.js} +1 -1
- package/web-app/dist/assets/{package-YoE2sWU8.js → package-98dbH_LD.js} +1 -1
- package/web-app/dist/assets/{plus-DdcvjiWR.js → plus-D7hmCkNi.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DlhQH4A4.js → refresh-cw-DX3wUkhV.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-BMskUWd9.js → rotate-ccw-XMHg_YgZ.js} +1 -1
- package/web-app/dist/assets/{scroll-text-wq9VgYsu.js → scroll-text-CRfkubNP.js} +1 -1
- package/web-app/dist/assets/{server-BvBfqjcL.js → server-WcGRWROZ.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Ctv17L9P.js → shield-alert-Der-fmpH.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Bppr-IMM.js → thumbs-up-CrBhUBj9.js} +1 -1
- package/web-app/dist/assets/{trash-2-C__rfpz0.js → trash-2-Pi47j24F.js} +1 -1
- package/web-app/dist/assets/{trending-down-CA0bokwG.js → trending-down-B6sr1-IB.js} +1 -1
- package/web-app/dist/assets/{trending-up-BFh3twff.js → trending-up-99SzxDxr.js} +1 -1
- package/web-app/dist/assets/{upload-DABSXWW6.js → upload-BREQadWi.js} +1 -1
- package/web-app/dist/assets/{usePolling-DcTxi6kF.js → usePolling-CGDeEkDK.js} +1 -1
- package/web-app/dist/assets/{user-y8K-uMqG.js → user-xN4ww_AA.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Runs an autonomous software factory that knows what it is supposed to deliver and proves it did, with a signed receipt. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.85.0
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -475,4 +475,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
|
|
|
475
475
|
|
|
476
476
|
---
|
|
477
477
|
|
|
478
|
-
**v11.
|
|
478
|
+
**v11.85.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.85.0
|
package/autonomy/grill.sh
CHANGED
|
@@ -191,6 +191,9 @@ _grill_with_timeout() {
|
|
|
191
191
|
|
|
192
192
|
grill_invoke_provider() {
|
|
193
193
|
local prompt="$1"
|
|
194
|
+
# Optional role: selects the lean-child flag set and ledger phase. The
|
|
195
|
+
# check author passes check_author (text-only, no tools).
|
|
196
|
+
local role="${2:-grill}"
|
|
194
197
|
local provider="${LOKI_PROVIDER:-claude}"
|
|
195
198
|
|
|
196
199
|
case "$provider" in
|
|
@@ -254,7 +257,7 @@ grill_invoke_provider() {
|
|
|
254
257
|
if type loki_lean_child_flags >/dev/null 2>&1; then
|
|
255
258
|
while IFS= read -r _gr_lf; do
|
|
256
259
|
[ -n "$_gr_lf" ] && _gr_argv+=("$_gr_lf")
|
|
257
|
-
done < <(loki_lean_child_flags
|
|
260
|
+
done < <(loki_lean_child_flags "$role" 2>/dev/null)
|
|
258
261
|
fi
|
|
259
262
|
if type loki_child_ledger_append >/dev/null 2>&1; then
|
|
260
263
|
_gr_ledger=true
|
|
@@ -274,7 +277,7 @@ grill_invoke_provider() {
|
|
|
274
277
|
out="$(printf '%s' "$prompt" \
|
|
275
278
|
| _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" env CAVEMAN_DEFAULT_MODE=off claude "${_gr_argv[@]}" -p - 2>/dev/null)"
|
|
276
279
|
if [ "$_gr_ledger" = true ] && [ -n "$out" ]; then
|
|
277
|
-
loki_child_ledger_append
|
|
280
|
+
loki_child_ledger_append "$role" "$out" >/dev/null 2>&1 || true
|
|
278
281
|
out="$(printf '%s' "$out" | python3 -I -c 'import json,sys
|
|
279
282
|
raw = sys.stdin.read()
|
|
280
283
|
try:
|
|
@@ -299,7 +302,9 @@ sys.stdout.write(str(d.get("result", "")) if isinstance(d, dict) else raw)' 2>/d
|
|
|
299
302
|
return $GRILL_EXIT_ERROR
|
|
300
303
|
fi
|
|
301
304
|
local out
|
|
302
|
-
|
|
305
|
+
local _cx_extra=()
|
|
306
|
+
[ "$role" = "check_author" ] && _cx_extra=(--skip-git-repo-check)
|
|
307
|
+
out="$(printf '%s' "$prompt" | _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" codex exec --sandbox workspace-write ${_cx_extra[@]+"${_cx_extra[@]}"} - 2>/dev/null)"
|
|
303
308
|
if [ -z "$out" ]; then
|
|
304
309
|
_grill_err "provider returned no output"
|
|
305
310
|
return $GRILL_EXIT_ERROR
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
"""Check-author context (moat P3, interface A in tests/moat/p3-the-wall.sh).
|
|
3
3
|
|
|
4
4
|
check_author.py context --spec <file> --repo <dir>
|
|
5
|
+
check_author.py accept --spec <file> --repo <dir> --out <dir> [--context <file>] [--attempts N]
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
context: prints ONE JSON object on stdout: the exact and complete input handed to the
|
|
7
8
|
check-authoring model. No model call, no network, deterministic.
|
|
8
9
|
|
|
9
10
|
{"spec_sha256": "<hex>", "inputs": [{"path", "sha256"}...], "prompt": "<text>"}
|
|
@@ -19,6 +20,16 @@ Same MAX_BLOB size cap and control-character path rule as the Wall. The Wall's
|
|
|
19
20
|
other two classes are implementation-bearing and are excluded here: TESTISH test
|
|
20
21
|
sources (style examples) and "named" source files (signatures of files the task
|
|
21
22
|
names). Anything outside the spec and CONFIG files never reaches the author.
|
|
23
|
+
|
|
24
|
+
accept: reads the author model's stdout from stdin, extracts exactly one JSON
|
|
25
|
+
object (prose or a fenced block around it is tolerated; zero or several objects
|
|
26
|
+
fail), validates it against the schema the verifier runs, and writes
|
|
27
|
+
<out>/checklist.json plus <out>/author.json {status: "authored"} atomically. Any
|
|
28
|
+
failure writes <out>/author.json {status: "unknown", reason} and the critical
|
|
29
|
+
failing verification-results.json sentinel (same shape as council_reverify_checklist
|
|
30
|
+
in autonomy/completion-council.sh), never a checklist.json, and exits non-zero.
|
|
31
|
+
accept reads only the spec file (for its sha256) and the optional --context file;
|
|
32
|
+
--repo is accepted for symmetry and is never opened.
|
|
22
33
|
"""
|
|
23
34
|
import argparse
|
|
24
35
|
import hashlib
|
|
@@ -39,10 +50,36 @@ MAX_FILES = 300
|
|
|
39
50
|
TESTISH = re.compile(r"(^|/)(tests?|__tests__|spec)/|\.(test|spec)\.[jt]sx?$|(^|/)test_[^/]*\.py$|_test\.(py|go)$")
|
|
40
51
|
CTRL = re.compile(r"[\x00-\x1f\x7f]")
|
|
41
52
|
|
|
53
|
+
# Verification types the verifier dispatches in autonomy/checklist-verify.py
|
|
54
|
+
# run_check(): file_exists :57, file_contains :69, tests_pass :89, command :218,
|
|
55
|
+
# grep_codebase :249, http_check :350. Each maps to the fields that make the check
|
|
56
|
+
# runnable (a check missing them can only ever stay pending).
|
|
57
|
+
VERIFY_TYPES = {
|
|
58
|
+
"file_exists": ("path|target|file",),
|
|
59
|
+
"file_contains": ("path|target|file", "pattern"),
|
|
60
|
+
"tests_pass": ("pattern",),
|
|
61
|
+
"command": ("command",),
|
|
62
|
+
"grep_codebase": ("pattern",),
|
|
63
|
+
"http_check": (),
|
|
64
|
+
}
|
|
65
|
+
PRIORITIES = ("critical", "major", "minor")
|
|
66
|
+
MAX_ITEMS = 60
|
|
67
|
+
MAX_INPUT_BYTES = 2_000_000
|
|
68
|
+
RESULT_FIELDS = ("passed", "output", "status", "verified_at")
|
|
69
|
+
SENTINEL = ('{"categories":[{"items":[{"id":"check_authoring_unknown","title":'
|
|
70
|
+
'"acceptance checks could not be authored","priority":"critical","status":"failing"}]}]}')
|
|
71
|
+
|
|
42
72
|
PROMPT_HEAD = (
|
|
43
73
|
"You are the acceptance check author. You see only the specification and the "
|
|
44
74
|
"project's runner configuration below. You cannot see the implementation. "
|
|
45
|
-
"Write acceptance checks that prove the specification is met
|
|
75
|
+
"Write acceptance checks that prove the specification is met.\n\n"
|
|
76
|
+
"Reply with exactly ONE JSON object and nothing else of substance, in this format:\n"
|
|
77
|
+
'{"categories":[{"name":"<group>","items":[{"id":"<unique id>","title":"<requirement>",'
|
|
78
|
+
'"priority":"critical|major|minor","verification":[{"type":"<type>", ...}]}]}]}\n'
|
|
79
|
+
"Every item needs id, title, priority and at least one verification. Allowed "
|
|
80
|
+
"verification types and their fields: file_exists {path}; file_contains {path, pattern}; "
|
|
81
|
+
"tests_pass {pattern}; command {command}; grep_codebase {pattern}; http_check {path}. "
|
|
82
|
+
"Use at most %d items. Do not include pass or result fields." % MAX_ITEMS
|
|
46
83
|
)
|
|
47
84
|
|
|
48
85
|
|
|
@@ -124,12 +161,162 @@ def build_context(spec: str, repo: str) -> dict:
|
|
|
124
161
|
return {"spec_sha256": inputs[0]["sha256"], "inputs": inputs, "prompt": "\n\n".join(parts)}
|
|
125
162
|
|
|
126
163
|
|
|
164
|
+
class Reject(Exception):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def extract_one_object(text: str) -> dict:
|
|
169
|
+
"""The single top-level JSON object in text; zero or several is a Reject."""
|
|
170
|
+
dec = json.JSONDecoder()
|
|
171
|
+
found = []
|
|
172
|
+
i = 0
|
|
173
|
+
while True:
|
|
174
|
+
i = text.find("{", i)
|
|
175
|
+
if i < 0:
|
|
176
|
+
break
|
|
177
|
+
try:
|
|
178
|
+
obj, end = dec.raw_decode(text, i)
|
|
179
|
+
except ValueError:
|
|
180
|
+
i += 1
|
|
181
|
+
continue
|
|
182
|
+
found.append(obj)
|
|
183
|
+
i = end
|
|
184
|
+
if not found:
|
|
185
|
+
raise Reject("no JSON object in author output")
|
|
186
|
+
if len(found) > 1:
|
|
187
|
+
raise Reject("%d JSON objects in author output, expected exactly one" % len(found))
|
|
188
|
+
return found[0]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def validate_checklist(doc: dict) -> dict:
|
|
192
|
+
cats = doc.get("categories")
|
|
193
|
+
if not isinstance(cats, list) or not cats:
|
|
194
|
+
raise Reject("categories must be a non-empty list")
|
|
195
|
+
out_cats, seen, count = [], set(), 0
|
|
196
|
+
for ci, cat in enumerate(cats):
|
|
197
|
+
items = cat.get("items") if isinstance(cat, dict) else None
|
|
198
|
+
if not isinstance(items, list):
|
|
199
|
+
raise Reject("category %d has no items list" % ci)
|
|
200
|
+
out_items = []
|
|
201
|
+
for it in items:
|
|
202
|
+
count += 1
|
|
203
|
+
if count > MAX_ITEMS:
|
|
204
|
+
raise Reject("more than %d items" % MAX_ITEMS)
|
|
205
|
+
if not isinstance(it, dict):
|
|
206
|
+
raise Reject("item is not an object")
|
|
207
|
+
iid, title = it.get("id"), it.get("title")
|
|
208
|
+
if not isinstance(iid, str) or not iid.strip() or not isinstance(title, str) or not title.strip():
|
|
209
|
+
raise Reject("item needs a non-empty string id and title")
|
|
210
|
+
if iid in seen:
|
|
211
|
+
raise Reject("duplicate item id: %s" % iid)
|
|
212
|
+
seen.add(iid)
|
|
213
|
+
if it.get("priority") not in PRIORITIES:
|
|
214
|
+
raise Reject("item %s: priority must be one of %s" % (iid, "/".join(PRIORITIES)))
|
|
215
|
+
ver = it.get("verification")
|
|
216
|
+
if not isinstance(ver, list) or not ver:
|
|
217
|
+
raise Reject("item %s: needs at least one verification" % iid)
|
|
218
|
+
out_ver = []
|
|
219
|
+
for v in ver:
|
|
220
|
+
if not isinstance(v, dict) or v.get("type") not in VERIFY_TYPES:
|
|
221
|
+
raise Reject("item %s: verification type must be one of %s"
|
|
222
|
+
% (iid, ", ".join(sorted(VERIFY_TYPES))))
|
|
223
|
+
for need in VERIFY_TYPES[v["type"]]:
|
|
224
|
+
if not any(isinstance(v.get(k), str) and v.get(k).strip() for k in need.split("|")):
|
|
225
|
+
raise Reject("item %s: %s check needs %s" % (iid, v["type"], need))
|
|
226
|
+
out_ver.append({k: x for k, x in v.items() if k not in RESULT_FIELDS})
|
|
227
|
+
out_items.append({"id": iid, "title": title, "priority": it["priority"], "verification": out_ver})
|
|
228
|
+
name = cat.get("name")
|
|
229
|
+
out_cats.append({"name": name if isinstance(name, str) else "", "items": out_items})
|
|
230
|
+
if count == 0:
|
|
231
|
+
raise Reject("checklist has no items")
|
|
232
|
+
return {"categories": out_cats}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def atomic_write(path: str, text: str) -> None:
|
|
236
|
+
tmp = "%s.tmp.%d" % (path, os.getpid())
|
|
237
|
+
try:
|
|
238
|
+
with open(tmp, "w") as fh:
|
|
239
|
+
fh.write(text)
|
|
240
|
+
os.replace(tmp, path)
|
|
241
|
+
except BaseException:
|
|
242
|
+
try:
|
|
243
|
+
os.unlink(tmp)
|
|
244
|
+
except OSError:
|
|
245
|
+
pass
|
|
246
|
+
raise
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def record_unknown(out: str, reason: str, spec_sha, attempts) -> None:
|
|
250
|
+
"""Failure record: drop every artifact of an earlier run, write the critical
|
|
251
|
+
failing sentinel first (the gate reads it), then status unknown."""
|
|
252
|
+
for name in ("checklist.json", "author.json", "verification-results.json"):
|
|
253
|
+
try:
|
|
254
|
+
os.unlink(os.path.join(out, name))
|
|
255
|
+
except OSError:
|
|
256
|
+
pass
|
|
257
|
+
atomic_write(os.path.join(out, "verification-results.json"), SENTINEL + "\n")
|
|
258
|
+
atomic_write(os.path.join(out, "author.json"), json.dumps(
|
|
259
|
+
{"status": "unknown", "reason": reason, "spec_sha256": spec_sha, "attempts": attempts},
|
|
260
|
+
sort_keys=True) + "\n")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_accept(args) -> int:
|
|
264
|
+
out = args.out
|
|
265
|
+
try:
|
|
266
|
+
os.makedirs(out, exist_ok=True)
|
|
267
|
+
except OSError as e:
|
|
268
|
+
print("check_author: cannot create %s: %s" % (out, e), file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
spec_sha = None
|
|
271
|
+
try:
|
|
272
|
+
spec_sha = sha256(read_bytes(args.spec))
|
|
273
|
+
inputs, prompt_sha = [{"path": os.path.realpath(args.spec), "sha256": spec_sha}], None
|
|
274
|
+
if args.context:
|
|
275
|
+
try:
|
|
276
|
+
with open(args.context) as fh:
|
|
277
|
+
ctx = json.load(fh)
|
|
278
|
+
inputs, prompt_sha = ctx["inputs"], sha256(ctx["prompt"].encode("utf-8"))
|
|
279
|
+
ctx_spec = ctx.get("spec_sha256")
|
|
280
|
+
except (OSError, ValueError, KeyError, TypeError, AttributeError, RecursionError):
|
|
281
|
+
raise Reject("context file unreadable or malformed")
|
|
282
|
+
if ctx_spec != spec_sha:
|
|
283
|
+
raise Reject("context spec_sha256 does not match the spec")
|
|
284
|
+
raw = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
|
|
285
|
+
if len(raw) > MAX_INPUT_BYTES:
|
|
286
|
+
raise Reject("author output exceeds %d bytes" % MAX_INPUT_BYTES)
|
|
287
|
+
text = raw.decode("utf-8", "replace")
|
|
288
|
+
if not text.strip():
|
|
289
|
+
raise Reject("empty author output")
|
|
290
|
+
checklist = validate_checklist(extract_one_object(text))
|
|
291
|
+
atomic_write(os.path.join(out, "checklist.json"), json.dumps(checklist, sort_keys=True, indent=2) + "\n")
|
|
292
|
+
atomic_write(os.path.join(out, "author.json"), json.dumps(
|
|
293
|
+
{"status": "authored", "spec_sha256": spec_sha, "inputs": inputs,
|
|
294
|
+
"prompt_sha256": prompt_sha, "attempts": args.attempts}, sort_keys=True) + "\n")
|
|
295
|
+
return 0
|
|
296
|
+
except Reject as e:
|
|
297
|
+
reason = str(e)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
reason = "accept failed: %s" % type(e).__name__
|
|
300
|
+
try:
|
|
301
|
+
record_unknown(out, reason, spec_sha, args.attempts)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print("check_author: could not record failure: %s" % e, file=sys.stderr)
|
|
304
|
+
print("check_author: unknown: %s" % reason, file=sys.stderr)
|
|
305
|
+
return 1
|
|
306
|
+
|
|
307
|
+
|
|
127
308
|
def main(argv=None) -> int:
|
|
128
309
|
ap = argparse.ArgumentParser(prog="check_author.py")
|
|
129
310
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
130
311
|
c = sub.add_parser("context", help="print the exact check-author input as JSON")
|
|
131
312
|
c.add_argument("--spec", required=True)
|
|
132
313
|
c.add_argument("--repo", required=True)
|
|
314
|
+
a = sub.add_parser("accept", help="validate the author model's stdout and write the checklist")
|
|
315
|
+
a.add_argument("--spec", required=True)
|
|
316
|
+
a.add_argument("--repo", required=True)
|
|
317
|
+
a.add_argument("--out", required=True)
|
|
318
|
+
a.add_argument("--context", help="saved context output, for inputs and prompt_sha256")
|
|
319
|
+
a.add_argument("--attempts", type=int, default=1)
|
|
133
320
|
args = ap.parse_args(argv)
|
|
134
321
|
if not os.path.isfile(args.spec):
|
|
135
322
|
print("check_author: spec not found: %s" % args.spec, file=sys.stderr)
|
|
@@ -137,6 +324,8 @@ def main(argv=None) -> int:
|
|
|
137
324
|
if not os.path.isdir(args.repo):
|
|
138
325
|
print("check_author: repo not a directory: %s" % args.repo, file=sys.stderr)
|
|
139
326
|
return 2
|
|
327
|
+
if args.cmd == "accept":
|
|
328
|
+
return run_accept(args)
|
|
140
329
|
json.dump(build_context(args.spec, args.repo), sys.stdout, sort_keys=True)
|
|
141
330
|
sys.stdout.write("\n")
|
|
142
331
|
return 0
|
|
@@ -249,11 +249,17 @@ _loki_apikey_helper_configured() {
|
|
|
249
249
|
# - Text-only roles additionally get the single argument "--tools=" (no tools). --tools is
|
|
250
250
|
# variadic: "--tools" plus an empty argument swallows a trailing prompt.
|
|
251
251
|
# Every flag is probed with loki_claude_flag_supported; an unsupported flag and
|
|
252
|
-
# its value are dropped. LOKI_LEAN_CHILDREN=0
|
|
253
|
-
#
|
|
252
|
+
# its value are dropped. LOKI_LEAN_CHILDREN=0 drops the lean flags but the walled
|
|
253
|
+
# check_author still gets "--tools=" (its isolation does not depend on the toggle).
|
|
254
|
+
# Main-session flags are not touched by this helper.
|
|
254
255
|
loki_lean_child_flags() {
|
|
255
256
|
local role="${1:-}"
|
|
256
|
-
[ "${LOKI_LEAN_CHILDREN:-1}" = "0" ]
|
|
257
|
+
if [ "${LOKI_LEAN_CHILDREN:-1}" = "0" ]; then
|
|
258
|
+
if [ "$role" = "check_author" ] && loki_claude_flag_supported "--tools"; then
|
|
259
|
+
printf '%s\n' "--tools="
|
|
260
|
+
fi
|
|
261
|
+
return 0
|
|
262
|
+
fi
|
|
257
263
|
if loki_subcall_bare_enabled; then
|
|
258
264
|
printf '%s\n' "--bare"
|
|
259
265
|
else
|
|
@@ -271,7 +277,7 @@ loki_lean_child_flags() {
|
|
|
271
277
|
fi
|
|
272
278
|
fi
|
|
273
279
|
case "$role" in
|
|
274
|
-
grill|council|vote|review|reviewer|enrich|wiki|docs|docgen|summary)
|
|
280
|
+
grill|check_author|council|vote|review|reviewer|enrich|wiki|docs|docgen|summary)
|
|
275
281
|
if loki_claude_flag_supported "--tools"; then
|
|
276
282
|
printf '%s\n' "--tools="
|
|
277
283
|
fi
|
|
@@ -147,6 +147,143 @@ checklist_init() {
|
|
|
147
147
|
return 0
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
#===============================================================================
|
|
151
|
+
# Walled check author
|
|
152
|
+
#===============================================================================
|
|
153
|
+
# The agent that writes the code never writes the acceptance checks. After
|
|
154
|
+
# checklist_init this authors them in a separate provider call that runs in an
|
|
155
|
+
# EMPTY run-owned directory (the repo is never the cwd and never named in the
|
|
156
|
+
# prompt), validates the reply with lib/check_author.py accept, and freezes the
|
|
157
|
+
# result. Once per spec sha256, at most 2 attempts. A failed author leaves the
|
|
158
|
+
# critical failing sentinel (council_checklist_gate then BLOCKs) and freeze
|
|
159
|
+
# refuses any checklist.json the implementer writes afterwards.
|
|
160
|
+
# Only claude gets hard no-tools; codex and seam providers are cwd-only and are
|
|
161
|
+
# reported as such, never as tool-denied.
|
|
162
|
+
CHECKLIST_AUTHOR_REQUIRED=${CHECKLIST_AUTHOR_REQUIRED:-false}
|
|
163
|
+
CHECKLIST_AUTHOR_MAX_ATTEMPTS=2
|
|
164
|
+
# sha256 of the checklist.json the author's accepted reply produced, held in
|
|
165
|
+
# this shell's memory only (never exported, never written to a file). author.json
|
|
166
|
+
# is an unsigned file the implementer can forge, so freeze trusts only this.
|
|
167
|
+
_CHECKLIST_AUTHORED_DIGEST=""
|
|
168
|
+
|
|
169
|
+
# _checklist_file_sha <path>: sha256 hex of the file, empty if unreadable.
|
|
170
|
+
_checklist_file_sha() {
|
|
171
|
+
local py
|
|
172
|
+
py="$(_loki_snapshot_py_tool)" || return 0
|
|
173
|
+
"$py" -I -S -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$1" 2>/dev/null || true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# _checklist_author_field <field>: field of the author record, empty if absent.
|
|
177
|
+
_checklist_author_field() {
|
|
178
|
+
local py rec
|
|
179
|
+
py="$(_loki_snapshot_py_tool)" || return 0
|
|
180
|
+
rec="$(dirname "${CHECKLIST_FILE:-.loki/checklist/checklist.json}")/author.json"
|
|
181
|
+
[ -f "$rec" ] && [ ! -L "$rec" ] || return 0
|
|
182
|
+
"$py" -I -S -c 'import json,sys
|
|
183
|
+
try:
|
|
184
|
+
v=json.load(open(sys.argv[1])).get(sys.argv[2])
|
|
185
|
+
print("" if v is None else v)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass' "$rec" "$1" 2>/dev/null || true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
checklist_author_isolation() {
|
|
191
|
+
# no-tools only when the argv the check_author role sends carries --tools=.
|
|
192
|
+
if [ "${LOKI_PROVIDER:-claude}" = "claude" ] && type loki_lean_child_flags >/dev/null 2>&1 \
|
|
193
|
+
&& loki_lean_child_flags check_author 2>/dev/null | grep -qx -- '--tools='; then
|
|
194
|
+
echo "check author isolation: no-tools"
|
|
195
|
+
else
|
|
196
|
+
echo "check author isolation: cwd-only"
|
|
197
|
+
fi
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
checklist_author() {
|
|
201
|
+
local prd_path="${1:-${CHECKLIST_PRD_PATH:-}}"
|
|
202
|
+
[ "$CHECKLIST_ENABLED" = "true" ] || return 0
|
|
203
|
+
[ "${LOKI_CHECKLIST_AUTHOR:-1}" != "0" ] || return 0
|
|
204
|
+
[ -n "$prd_path" ] && [ -f "$prd_path" ] || return 0
|
|
205
|
+
CHECKLIST_AUTHOR_REQUIRED=true
|
|
206
|
+
|
|
207
|
+
local script_dir ca py spec_sha prior_status prior_sha attempts out_dir
|
|
208
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
209
|
+
ca="${script_dir}/lib/check_author.py"
|
|
210
|
+
out_dir="${CHECKLIST_DIR:-.loki/checklist}"
|
|
211
|
+
CHECKLIST_FILE="${CHECKLIST_FILE:-${out_dir}/checklist.json}"
|
|
212
|
+
mkdir -p "$out_dir"
|
|
213
|
+
if [ ! -f "$ca" ] || ! py="$(_loki_snapshot_py_tool)"; then
|
|
214
|
+
log_warn "check author unavailable (fail closed): freeze refuses until the checks are authored"
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
spec_sha="$("$py" -I -S -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$prd_path" 2>/dev/null)" || spec_sha=""
|
|
218
|
+
|
|
219
|
+
prior_status="$(_checklist_author_field status)"
|
|
220
|
+
prior_sha="$(_checklist_author_field spec_sha256)"
|
|
221
|
+
attempts=0
|
|
222
|
+
if [ -n "$spec_sha" ] && [ "$prior_sha" = "$spec_sha" ]; then
|
|
223
|
+
# An authored record is only trusted by the process that wrote it: the
|
|
224
|
+
# accepted digest lives in memory, so a restarted process re-authors
|
|
225
|
+
# rather than believe an unsigned author.json.
|
|
226
|
+
if [ "$prior_status" = "authored" ] && [ -n "$_CHECKLIST_AUTHORED_DIGEST" ]; then
|
|
227
|
+
checklist_freeze_once || true
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
if [ "$prior_status" = "authored" ]; then
|
|
231
|
+
prior_status=""
|
|
232
|
+
fi
|
|
233
|
+
attempts="$(_checklist_author_field attempts)"
|
|
234
|
+
case "$attempts" in ''|*[!0-9]*) attempts=0 ;; esac
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
local work cwd ctx prompt rc out
|
|
238
|
+
work="$(mktemp -d "${TMPDIR:-/tmp}/loki-author.XXXXXXXX")" || return 1
|
|
239
|
+
cwd="${work}/cwd"
|
|
240
|
+
mkdir -p "$cwd"
|
|
241
|
+
ctx="${work}/context.json"
|
|
242
|
+
if ! "$py" -I -S "$ca" context --spec "$prd_path" --repo "$PWD" > "$ctx" 2>/dev/null; then
|
|
243
|
+
printf '' | "$py" -I -S "$ca" accept --spec "$prd_path" --repo "$PWD" --out "$out_dir" --attempts "$CHECKLIST_AUTHOR_MAX_ATTEMPTS" >/dev/null 2>&1 || true
|
|
244
|
+
rm -rf -- "$work"
|
|
245
|
+
log_warn "check author: could not build the author context (fail closed)"
|
|
246
|
+
return 1
|
|
247
|
+
fi
|
|
248
|
+
prompt="$("$py" -I -S -c 'import json,sys;sys.stdout.write(json.load(open(sys.argv[1]))["prompt"])' "$ctx" 2>/dev/null)" || prompt=""
|
|
249
|
+
|
|
250
|
+
if ! type grill_invoke_provider >/dev/null 2>&1; then
|
|
251
|
+
local _o
|
|
252
|
+
_o="$(set +o)"
|
|
253
|
+
# shellcheck disable=SC1091
|
|
254
|
+
. "${script_dir}/grill.sh" 2>/dev/null || true
|
|
255
|
+
eval "$_o"
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# The child ledger is written relative to LOKI_DIR; keep it pointing at the
|
|
259
|
+
# real state dir while the cwd is the empty directory.
|
|
260
|
+
local _ca_loki_dir="${LOKI_DIR:-$PWD/.loki}"
|
|
261
|
+
case "$_ca_loki_dir" in /*) ;; *) _ca_loki_dir="$PWD/$_ca_loki_dir" ;; esac
|
|
262
|
+
rc=1
|
|
263
|
+
while [ "$attempts" -lt "$CHECKLIST_AUTHOR_MAX_ATTEMPTS" ]; do
|
|
264
|
+
attempts=$((attempts + 1))
|
|
265
|
+
out=""
|
|
266
|
+
if [ -n "$prompt" ] && type grill_invoke_provider >/dev/null 2>&1; then
|
|
267
|
+
out="$(cd "$cwd" && LOKI_DIR="$_ca_loki_dir" grill_invoke_provider "$prompt" check_author 2>/dev/null)" || out=""
|
|
268
|
+
fi
|
|
269
|
+
if printf '%s' "$out" | "$py" -I -S "$ca" accept --spec "$prd_path" --repo "$PWD" \
|
|
270
|
+
--out "$out_dir" --context "$ctx" --attempts "$attempts" >/dev/null 2>&1; then
|
|
271
|
+
rc=0
|
|
272
|
+
break
|
|
273
|
+
fi
|
|
274
|
+
done
|
|
275
|
+
rm -rf -- "$work"
|
|
276
|
+
checklist_author_isolation > "${out_dir}/author-isolation.txt" 2>/dev/null || true
|
|
277
|
+
log_info "$(checklist_author_isolation)"
|
|
278
|
+
if [ "$rc" -ne 0 ]; then
|
|
279
|
+
log_warn "check author: no valid checks after ${attempts} attempt(s); completion stays blocked"
|
|
280
|
+
return 1
|
|
281
|
+
fi
|
|
282
|
+
_CHECKLIST_AUTHORED_DIGEST="$(_checklist_file_sha "$CHECKLIST_FILE")"
|
|
283
|
+
checklist_freeze_once || true
|
|
284
|
+
return 0
|
|
285
|
+
}
|
|
286
|
+
|
|
150
287
|
#===============================================================================
|
|
151
288
|
# Acceptance-Oracle Triangulation (P2-3)
|
|
152
289
|
#===============================================================================
|
|
@@ -1020,6 +1157,27 @@ checklist_freeze_once() {
|
|
|
1020
1157
|
[ -z "$CHECKLIST_FROZEN_DIGEST" ] || return 0
|
|
1021
1158
|
[ -n "$CHECKLIST_FILE" ] && [ -f "$CHECKLIST_FILE" ] || return 0
|
|
1022
1159
|
|
|
1160
|
+
# Walled author: refuse unless the checks were authored by the check author
|
|
1161
|
+
# for this spec. An implementer-written checklist.json has no authored record.
|
|
1162
|
+
if [ "$CHECKLIST_AUTHOR_REQUIRED" = "true" ]; then
|
|
1163
|
+
local _fa_status _fa_sha _fa_want="" _fa_py
|
|
1164
|
+
_fa_status="$(_checklist_author_field status)"
|
|
1165
|
+
_fa_sha="$(_checklist_author_field spec_sha256)"
|
|
1166
|
+
if [ -n "${CHECKLIST_PRD_PATH:-}" ] && [ -f "$CHECKLIST_PRD_PATH" ] && _fa_py="$(_loki_snapshot_py_tool)"; then
|
|
1167
|
+
_fa_want="$("$_fa_py" -I -S -c 'import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$CHECKLIST_PRD_PATH" 2>/dev/null)" || _fa_want=""
|
|
1168
|
+
fi
|
|
1169
|
+
if [ "$_fa_status" != "authored" ] || [ -z "$_fa_sha" ] || [ "$_fa_sha" != "$_fa_want" ]; then
|
|
1170
|
+
log_warn "checklist freeze refused: the checks were not authored by the check author (status: ${_fa_status:-none})"
|
|
1171
|
+
return 1
|
|
1172
|
+
fi
|
|
1173
|
+
# author.json is forgeable; bind the freeze to the digest the harness
|
|
1174
|
+
# recorded in memory when it accepted the author's checklist.
|
|
1175
|
+
if [ -z "$_CHECKLIST_AUTHORED_DIGEST" ] || [ "$(_checklist_file_sha "$CHECKLIST_FILE")" != "$_CHECKLIST_AUTHORED_DIGEST" ]; then
|
|
1176
|
+
log_warn "checklist freeze refused: checklist.json does not match the checklist the check author produced"
|
|
1177
|
+
return 1
|
|
1178
|
+
fi
|
|
1179
|
+
fi
|
|
1180
|
+
|
|
1023
1181
|
local script_dir verify_script _cl_py digest
|
|
1024
1182
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1025
1183
|
verify_script="${script_dir}/checklist-verify.py"
|
|
@@ -1222,6 +1380,10 @@ checklist_as_evidence() {
|
|
|
1222
1380
|
echo ""
|
|
1223
1381
|
echo "## PRD Checklist Verification"
|
|
1224
1382
|
echo ""
|
|
1383
|
+
if [ -f "$(dirname "$CHECKLIST_RESULTS_FILE")/author-isolation.txt" ]; then
|
|
1384
|
+
cat "$(dirname "$CHECKLIST_RESULTS_FILE")/author-isolation.txt"
|
|
1385
|
+
echo ""
|
|
1386
|
+
fi
|
|
1225
1387
|
|
|
1226
1388
|
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" "$_cl_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1227
1389
|
import json, os
|
package/autonomy/run.sh
CHANGED
|
@@ -23461,10 +23461,7 @@ if lines:
|
|
|
23461
23461
|
|
|
23462
23462
|
# PRD Checklist status injection (v5.44.0)
|
|
23463
23463
|
local checklist_status=""
|
|
23464
|
-
if
|
|
23465
|
-
# First iteration with PRD but no checklist yet: instruct AI to create it
|
|
23466
|
-
checklist_status="PRD_CHECKLIST_INIT: Create .loki/checklist/checklist.json from the PRD. Extract requirements into categories with items. Each item needs: id, title, description, priority (critical|major|minor), and verification checks (file_exists, file_contains, tests_pass, grep_codebase, command). This checklist will be auto-verified every ${CHECKLIST_INTERVAL:-5} iterations."
|
|
23467
|
-
elif type checklist_summary &>/dev/null && [ -f ".loki/checklist/verification-results.json" ]; then
|
|
23464
|
+
if type checklist_summary &>/dev/null && [ -f ".loki/checklist/verification-results.json" ]; then
|
|
23468
23465
|
checklist_status=$(checklist_summary 2>/dev/null || true)
|
|
23469
23466
|
if [ -n "$checklist_status" ]; then
|
|
23470
23467
|
checklist_status="PRD_CHECKLIST_STATUS: ${checklist_status}. Review failing items and prioritize fixing them in this iteration."
|
|
@@ -25347,6 +25344,9 @@ except Exception:
|
|
|
25347
25344
|
fi
|
|
25348
25345
|
if type checklist_init &>/dev/null; then
|
|
25349
25346
|
checklist_init "$prd_path"
|
|
25347
|
+
if type checklist_author &>/dev/null; then
|
|
25348
|
+
checklist_author "$prd_path" || true
|
|
25349
|
+
fi
|
|
25350
25350
|
fi
|
|
25351
25351
|
fi
|
|
25352
25352
|
|
package/autonomy/spec.sh
CHANGED
|
@@ -662,6 +662,7 @@ loki spec - the living spec: the spec is the contract; we keep it true.
|
|
|
662
662
|
|
|
663
663
|
USAGE:
|
|
664
664
|
loki spec <lock|status|sync> [<spec-path>] [options]
|
|
665
|
+
loki spec checklist-reset <spec-sha256>
|
|
665
666
|
|
|
666
667
|
DESCRIPTION:
|
|
667
668
|
Binds a spec (PRD) to content hashes so drift between the spec and the code
|
|
@@ -680,6 +681,15 @@ SUBCOMMANDS:
|
|
|
680
681
|
sync Refresh the lock after a human review (explicit action). This MVP
|
|
681
682
|
NEVER auto-rewrites the spec itself.
|
|
682
683
|
|
|
684
|
+
checklist-reset <spec-sha256>
|
|
685
|
+
Operator reset for a spec whose acceptance-check authoring failed
|
|
686
|
+
twice. That failure leaves a check_authoring_unknown sentinel in
|
|
687
|
+
.loki/checklist that fails the completion gate closed. This
|
|
688
|
+
command, and nothing else, clears it for exactly the named spec
|
|
689
|
+
sha256 (the value in .loki/checklist/author.json), so the next run
|
|
690
|
+
authors the checks again. Refuses while a run is live. Exit 0
|
|
691
|
+
cleared, 1 refused or no such sentinel, 2 usage error.
|
|
692
|
+
|
|
683
693
|
SPEC RESOLUTION (when <spec-path> is omitted, first match wins):
|
|
684
694
|
.loki/generated-prd.md -> prd.md -> PRD.md -> docs/prd.md
|
|
685
695
|
|
|
@@ -704,6 +714,73 @@ VERIFY INTEGRATION:
|
|
|
704
714
|
EOF
|
|
705
715
|
}
|
|
706
716
|
|
|
717
|
+
# Operator reset of the check_authoring_unknown sentinel for exactly one spec
|
|
718
|
+
# sha256. The sentinel (verification-results.json) and its record (author.json,
|
|
719
|
+
# status unknown, spec_sha256) are written together by lib/check_author.py
|
|
720
|
+
# record_unknown; this is the only path that clears them. is_session_running is
|
|
721
|
+
# the live-run detection `loki` already uses; without it the live state is
|
|
722
|
+
# unknown, so the reset refuses.
|
|
723
|
+
spec_checklist_reset() {
|
|
724
|
+
local sha="${1:-}"
|
|
725
|
+
case "$sha" in
|
|
726
|
+
*[!0-9a-f]*|"") _spec_err "usage: loki spec checklist-reset <spec-sha256> (64 lowercase hex characters)"; return $SPEC_EXIT_USAGE ;;
|
|
727
|
+
esac
|
|
728
|
+
if [ "${#sha}" -ne 64 ]; then
|
|
729
|
+
_spec_err "usage: loki spec checklist-reset <spec-sha256> (64 lowercase hex characters)"
|
|
730
|
+
return $SPEC_EXIT_USAGE
|
|
731
|
+
fi
|
|
732
|
+
if ! type is_session_running >/dev/null 2>&1; then
|
|
733
|
+
_spec_err "cannot tell whether a run is live; run this through 'loki spec checklist-reset'"
|
|
734
|
+
return $SPEC_EXIT_DRIFT
|
|
735
|
+
fi
|
|
736
|
+
if is_session_running; then
|
|
737
|
+
_spec_err "a run is live; stop it ('loki stop') before resetting the checklist sentinel"
|
|
738
|
+
return $SPEC_EXIT_DRIFT
|
|
739
|
+
fi
|
|
740
|
+
local dir="${LOKI_DIR:-.loki}/checklist"
|
|
741
|
+
local rec="$dir/author.json" res="$dir/verification-results.json"
|
|
742
|
+
local state
|
|
743
|
+
if [ -L "$dir" ]; then
|
|
744
|
+
_spec_err "$dir is a symlink; refusing to clear anything through it"
|
|
745
|
+
return $SPEC_EXIT_DRIFT
|
|
746
|
+
fi
|
|
747
|
+
state="$(python3 -I -S -c 'import json,sys
|
|
748
|
+
try:
|
|
749
|
+
d=json.load(open(sys.argv[1]))
|
|
750
|
+
print("%s %s" % (d.get("status",""), d.get("spec_sha256","")))
|
|
751
|
+
except Exception:
|
|
752
|
+
print("unreadable")' "$rec" 2>/dev/null)" || state="unreadable"
|
|
753
|
+
if [ ! -f "$rec" ] || [ -L "$rec" ] || [ "$state" = "unreadable" ]; then
|
|
754
|
+
_spec_err "no check_authoring_unknown sentinel recorded in $dir (nothing cleared)"
|
|
755
|
+
return $SPEC_EXIT_DRIFT
|
|
756
|
+
fi
|
|
757
|
+
if [ "${state%% *}" != "unknown" ]; then
|
|
758
|
+
_spec_err "the author record in $dir is not a failed authoring (status ${state%% *}); nothing cleared"
|
|
759
|
+
return $SPEC_EXIT_DRIFT
|
|
760
|
+
fi
|
|
761
|
+
if [ "${state#* }" != "$sha" ]; then
|
|
762
|
+
_spec_err "no sentinel for spec sha256 $sha (the recorded failure is for ${state#* }); nothing cleared"
|
|
763
|
+
return $SPEC_EXIT_DRIFT
|
|
764
|
+
fi
|
|
765
|
+
local removed="$rec"
|
|
766
|
+
if [ -e "$res" ] || [ -L "$res" ]; then
|
|
767
|
+
if [ -f "$res" ] && [ ! -L "$res" ] && grep -q 'check_authoring_unknown' "$res" 2>/dev/null; then
|
|
768
|
+
removed="$rec, $res"
|
|
769
|
+
else
|
|
770
|
+
_spec_err "$res is not the check_authoring_unknown sentinel; refusing a partial clear (nothing cleared)"
|
|
771
|
+
return $SPEC_EXIT_DRIFT
|
|
772
|
+
fi
|
|
773
|
+
fi
|
|
774
|
+
if [ "$removed" != "$rec" ]; then
|
|
775
|
+
rm -f -- "${res:?}"
|
|
776
|
+
fi
|
|
777
|
+
rm -f -- "${rec:?}"
|
|
778
|
+
printf 'Cleared the check_authoring_unknown sentinel for spec sha256 %s\n' "$sha"
|
|
779
|
+
printf 'Removed: %s\n' "$removed"
|
|
780
|
+
printf 'The next run authors the acceptance checks again (attempt count reset).\n'
|
|
781
|
+
return $SPEC_EXIT_OK
|
|
782
|
+
}
|
|
783
|
+
|
|
707
784
|
# ---------------------------------------------------------------------------
|
|
708
785
|
# Entry point
|
|
709
786
|
# ---------------------------------------------------------------------------
|
|
@@ -715,6 +792,16 @@ spec_main() {
|
|
|
715
792
|
-h|--help|help|"") spec_help; return $SPEC_EXIT_OK ;;
|
|
716
793
|
esac
|
|
717
794
|
|
|
795
|
+
if [ "$sub" = "checklist-reset" ]; then
|
|
796
|
+
case "${1:-}" in -h|--help) spec_help; return $SPEC_EXIT_OK ;; esac
|
|
797
|
+
if [ $# -ne 1 ]; then
|
|
798
|
+
_spec_err "usage: loki spec checklist-reset <spec-sha256>"
|
|
799
|
+
return $SPEC_EXIT_USAGE
|
|
800
|
+
fi
|
|
801
|
+
spec_checklist_reset "$1"
|
|
802
|
+
return $?
|
|
803
|
+
fi
|
|
804
|
+
|
|
718
805
|
local spec_arg=""
|
|
719
806
|
local out_dir="$SPEC_DIR_DEFAULT"
|
|
720
807
|
local as_json="false"
|
package/dashboard/__init__.py
CHANGED
package/docs/CLI-REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CLI reference
|
|
2
2
|
|
|
3
|
-
Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.
|
|
3
|
+
Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.85.0. Do not edit by hand.
|
|
4
4
|
Hidden and legacy-only commands are omitted; see `docs/CLI-INVENTORY.md` for the full inventory.
|
|
5
5
|
|
|
6
6
|
| Command | Description |
|