loki-mode 11.84.0 → 11.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/grill.sh +8 -3
  4. package/autonomy/lib/check_author.py +191 -2
  5. package/autonomy/lib/claude-flags.sh +10 -4
  6. package/autonomy/prd-checklist.sh +162 -0
  7. package/autonomy/run.sh +4 -4
  8. package/autonomy/spec.sh +87 -0
  9. package/dashboard/__init__.py +1 -1
  10. package/docs/CLI-REFERENCE.md +1 -1
  11. package/docs/INSTALLATION.md +1 -1
  12. package/loki-ts/data/child-env.json +1 -0
  13. package/loki-ts/dist/loki.js +1504 -1388
  14. package/mcp/__init__.py +1 -1
  15. package/package.json +2 -2
  16. package/packages/control-plane/dist/ask-tools-server.js +145 -20
  17. package/packages/control-plane/dist/server.js +17153 -15919
  18. package/packages/control-plane/src/server/integrity.ts +22 -4
  19. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  20. package/web-app/dist/assets/{AdminPage-CXLv81So.js → AdminPage-CXRvxMO_.js} +1 -1
  21. package/web-app/dist/assets/{Avatar-Dkquf4s9.js → Avatar-Bk9K6OnI.js} +1 -1
  22. package/web-app/dist/assets/{Badge-BPlO-Wiw.js → Badge-CDePJKKg.js} +1 -1
  23. package/web-app/dist/assets/{Button-BO-JvVyh.js → Button-DMzR-FT4.js} +1 -1
  24. package/web-app/dist/assets/{CockpitPage-DCuXv1OL.js → CockpitPage-Cfh42owC.js} +1 -1
  25. package/web-app/dist/assets/{ComparePage-zVSpiuEp.js → ComparePage-Oft34xnR.js} +1 -1
  26. package/web-app/dist/assets/{ErrorBoundary-Ci4wqP2a.js → ErrorBoundary-DlXBbQDo.js} +1 -1
  27. package/web-app/dist/assets/{EvidenceReceiptPanel-CEBhqO60.js → EvidenceReceiptPanel-C0LKef5y.js} +1 -1
  28. package/web-app/dist/assets/{GitHubIssuesPanel-UR6xkpVn.js → GitHubIssuesPanel-CyqsExTP.js} +1 -1
  29. package/web-app/dist/assets/{GitHubPRsPanel-BspmPZ6K.js → GitHubPRsPanel-BC7ExtnE.js} +1 -1
  30. package/web-app/dist/assets/{HomePage-DchBOs8o.js → HomePage-CmYisyjJ.js} +2 -2
  31. package/web-app/dist/assets/{LoginPage-BldlfUlE.js → LoginPage-Cunt-wYH.js} +1 -1
  32. package/web-app/dist/assets/{MagicPage-CVnhnrh6.js → MagicPage-lvGAbDEv.js} +1 -1
  33. package/web-app/dist/assets/{MetricsPage-I_Ye6IjS.js → MetricsPage-DPvRVQU7.js} +1 -1
  34. package/web-app/dist/assets/{NotFoundPage-CCDI4T7K.js → NotFoundPage-DDGT6teS.js} +1 -1
  35. package/web-app/dist/assets/{ProjectPage-DuyA8Qz7.js → ProjectPage-Bf6fTvjD.js} +3 -3
  36. package/web-app/dist/assets/{ProjectsPage-BF2JSz1r.js → ProjectsPage-DL2gvN_V.js} +1 -1
  37. package/web-app/dist/assets/{SettingsPage-B_5LeF6K.js → SettingsPage-DV-X99VB.js} +1 -1
  38. package/web-app/dist/assets/{ShowcasePage-lE5cIgbU.js → ShowcasePage-B3eyQWQJ.js} +1 -1
  39. package/web-app/dist/assets/{SystemSettingsPage-v2DJyogx.js → SystemSettingsPage-DIUT7aYr.js} +1 -1
  40. package/web-app/dist/assets/{TeamsPage-eXMmORI1.js → TeamsPage-BJpeWszJ.js} +1 -1
  41. package/web-app/dist/assets/{TemplatesPage-CkHulSrT.js → TemplatesPage-Cqo6IMUO.js} +1 -1
  42. package/web-app/dist/assets/{TerminalOutput-DJwjzb2Y.js → TerminalOutput-B7NsTAYn.js} +1 -1
  43. package/web-app/dist/assets/{activity-CAtLpfNe.js → activity-Bpy5MefF.js} +1 -1
  44. package/web-app/dist/assets/{bell-CL-IIbCA.js → bell-BwXuuFea.js} +1 -1
  45. package/web-app/dist/assets/{bot-fT57nwl5.js → bot-Cij-5kEZ.js} +1 -1
  46. package/web-app/dist/assets/{check-hWhzzw0m.js → check-YWtXU6Zo.js} +1 -1
  47. package/web-app/dist/assets/{chevron-left-BLHNp2u-.js → chevron-left-MxOda7Z9.js} +1 -1
  48. package/web-app/dist/assets/{circle-alert-BiPOqU-L.js → circle-alert-w9za8rUy.js} +1 -1
  49. package/web-app/dist/assets/{clock-Cqcvrmkj.js → clock-Bp3AU4Nx.js} +1 -1
  50. package/web-app/dist/assets/{cloud-DUijmoIE.js → cloud-BFuxyAYb.js} +1 -1
  51. package/web-app/dist/assets/{code-xml-CHpO2Oxh.js → code-xml-B3jPToQW.js} +1 -1
  52. package/web-app/dist/assets/{database-DggasNHP.js → database-D85Q7ufb.js} +1 -1
  53. package/web-app/dist/assets/{dollar-sign-CXxG_8K6.js → dollar-sign-C0hEZ3BL.js} +1 -1
  54. package/web-app/dist/assets/{file-code-corner-xfVvREvW.js → file-code-corner-DeMr0sSL.js} +1 -1
  55. package/web-app/dist/assets/{file-plus-CxboFD8a.js → file-plus-BtBan_QU.js} +1 -1
  56. package/web-app/dist/assets/{globe-uQo7wmFo.js → globe-DLHflM3B.js} +1 -1
  57. package/web-app/dist/assets/{hammer-DpmqB3Jc.js → hammer-BLoZn0OU.js} +1 -1
  58. package/web-app/dist/assets/{index-DSecwFL3.js → index-q6GEQ6U8.js} +3 -3
  59. package/web-app/dist/assets/{layers-DAhv2mJy.js → layers-Cac5ZHvD.js} +1 -1
  60. package/web-app/dist/assets/{loader-circle-DlfD5ae4.js → loader-circle-C7UMl5I-.js} +1 -1
  61. package/web-app/dist/assets/{lock-Bp9L8WT9.js → lock-BC3v0P7Z.js} +1 -1
  62. package/web-app/dist/assets/{package-YoE2sWU8.js → package-CFsriyik.js} +1 -1
  63. package/web-app/dist/assets/{plus-DdcvjiWR.js → plus-CSi88k0-.js} +1 -1
  64. package/web-app/dist/assets/{refresh-cw-DlhQH4A4.js → refresh-cw-BBBOc6SY.js} +1 -1
  65. package/web-app/dist/assets/{rotate-ccw-BMskUWd9.js → rotate-ccw-D_dHBG_h.js} +1 -1
  66. package/web-app/dist/assets/{scroll-text-wq9VgYsu.js → scroll-text-z4CcL8tu.js} +1 -1
  67. package/web-app/dist/assets/{server-BvBfqjcL.js → server-Dsl-ujtv.js} +1 -1
  68. package/web-app/dist/assets/{shield-alert-Ctv17L9P.js → shield-alert-CXuzPi9-.js} +1 -1
  69. package/web-app/dist/assets/{thumbs-up-Bppr-IMM.js → thumbs-up-CNkCfkkx.js} +1 -1
  70. package/web-app/dist/assets/{trash-2-C__rfpz0.js → trash-2-rCtL1tv8.js} +1 -1
  71. package/web-app/dist/assets/{trending-down-CA0bokwG.js → trending-down-0wlGANoa.js} +1 -1
  72. package/web-app/dist/assets/{trending-up-BFh3twff.js → trending-up-C9iCihGe.js} +1 -1
  73. package/web-app/dist/assets/{upload-DABSXWW6.js → upload-DdKjc6-R.js} +1 -1
  74. package/web-app/dist/assets/{usePolling-DcTxi6kF.js → usePolling-Dyqa_HZa.js} +1 -1
  75. package/web-app/dist/assets/{user-y8K-uMqG.js → user-D1NeTikp.js} +1 -1
  76. 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.84.0
6
+ # Loki Mode v11.86.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.84.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
478
+ **v11.86.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.84.0
1
+ 11.86.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 grill 2>/dev/null)
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 grill "$out" >/dev/null 2>&1 || true
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
- out="$(printf '%s' "$prompt" | _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" codex exec --sandbox workspace-write - 2>/dev/null)"
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
- Prints ONE JSON object on stdout: the exact and complete input handed to the
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 prints nothing. Main-session
253
- # flags are not touched by this helper.
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" ] && return 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 [ -n "$prd" ] && [ ! -f ".loki/checklist/checklist.json" ]; then
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"
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.84.0"
10
+ __version__ = "11.86.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -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.84.0. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.86.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 |