sliceagent 0.1.0__py3-none-any.whl

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 (71) hide show
  1. sliceagent/__init__.py +3 -0
  2. sliceagent/__main__.py +6 -0
  3. sliceagent/access.py +93 -0
  4. sliceagent/agents.py +173 -0
  5. sliceagent/background_review.py +146 -0
  6. sliceagent/binsniff.py +89 -0
  7. sliceagent/cli.py +890 -0
  8. sliceagent/clock.py +32 -0
  9. sliceagent/code_grep.py +329 -0
  10. sliceagent/code_index.py +417 -0
  11. sliceagent/config.py +240 -0
  12. sliceagent/context_overflow.py +227 -0
  13. sliceagent/envspec.py +129 -0
  14. sliceagent/errors.py +167 -0
  15. sliceagent/events.py +96 -0
  16. sliceagent/finding_types.py +70 -0
  17. sliceagent/flags.py +63 -0
  18. sliceagent/fuzzy.py +135 -0
  19. sliceagent/guardrails.py +438 -0
  20. sliceagent/guidance.py +69 -0
  21. sliceagent/hippocampus.py +581 -0
  22. sliceagent/hooks.py +334 -0
  23. sliceagent/interfaces.py +144 -0
  24. sliceagent/llm.py +695 -0
  25. sliceagent/loop.py +548 -0
  26. sliceagent/mcp_client.py +255 -0
  27. sliceagent/mcp_security.py +77 -0
  28. sliceagent/memory.py +428 -0
  29. sliceagent/metrics.py +103 -0
  30. sliceagent/model_catalog.py +124 -0
  31. sliceagent/monitor.py +615 -0
  32. sliceagent/neocortex.py +436 -0
  33. sliceagent/onboarding.py +323 -0
  34. sliceagent/oracle.py +36 -0
  35. sliceagent/pagetable.py +255 -0
  36. sliceagent/pfc.py +449 -0
  37. sliceagent/plugins.py +127 -0
  38. sliceagent/policy.py +234 -0
  39. sliceagent/procman.py +187 -0
  40. sliceagent/prompt.py +239 -0
  41. sliceagent/records.py +108 -0
  42. sliceagent/recovery.py +119 -0
  43. sliceagent/regions.py +678 -0
  44. sliceagent/registry.py +128 -0
  45. sliceagent/retriever.py +19 -0
  46. sliceagent/safety.py +332 -0
  47. sliceagent/sandbox.py +143 -0
  48. sliceagent/scheduler.py +92 -0
  49. sliceagent/search_index.py +289 -0
  50. sliceagent/seed.py +465 -0
  51. sliceagent/sensory_cortex.py +500 -0
  52. sliceagent/session.py +222 -0
  53. sliceagent/skill_provenance.py +71 -0
  54. sliceagent/skill_usage.py +123 -0
  55. sliceagent/skills.py +209 -0
  56. sliceagent/subagent.py +332 -0
  57. sliceagent/subdir_hints.py +222 -0
  58. sliceagent/swap.py +182 -0
  59. sliceagent/taskstate.py +57 -0
  60. sliceagent/telemetry.py +59 -0
  61. sliceagent/terminal.py +240 -0
  62. sliceagent/text_utils.py +56 -0
  63. sliceagent/tool_summary.py +93 -0
  64. sliceagent/tools.py +1194 -0
  65. sliceagent/tui.py +1377 -0
  66. sliceagent/web.py +354 -0
  67. sliceagent-0.1.0.dist-info/METADATA +262 -0
  68. sliceagent-0.1.0.dist-info/RECORD +71 -0
  69. sliceagent-0.1.0.dist-info/WHEEL +4 -0
  70. sliceagent-0.1.0.dist-info/entry_points.txt +2 -0
  71. sliceagent-0.1.0.dist-info/licenses/LICENSE +21 -0
sliceagent/regions.py ADDED
@@ -0,0 +1,678 @@
1
+ """Typed-region renderers — per-kind views over the EXISTING Slice dataclass fields.
2
+
3
+ The slice is an address space of TYPED REGIONS (open files, ghosts, conversation, skills,
4
+ threads, …); each region knows how to render itself and to SUPPRESS itself when empty.
5
+ seed.py's render_slice is the layout pass that orders these region renderers into the one
6
+ user string (the moat); the renderers themselves live here.
7
+
8
+ This module is a pure rendering/metadata layer: it reads Slice fields (pfc.py) and low-level
9
+ helpers (safety.wrap_untrusted, the working-set bounds OWNED by swap.py) but imports NOTHING
10
+ from pfc.py/seed.py — they import FROM here (one direction), so there is no import cycle.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import re
16
+
17
+ from .safety import wrap_untrusted
18
+ from .swap import MAX_REVIEWED
19
+ from .text_utils import normalize_ws, one_line
20
+
21
+ MANIFEST_TURNS = 8 # PAGED-OUT HISTORY manifest window — bounded locator count (the moat: constant
22
+ # size regardless of session length; content is paged in on demand, never accumulated into the slice).
23
+ MAX_OPEN_THREADS = 6 # OTHER OPEN THREADS tier cap — bounded presentation of parked topics
24
+ MAX_FINDINGS = 8 # bounded ring of distilled conclusions (anti-re-derivation; not a transcript)
25
+ MAX_FINDING_CHARS = 300 # each finding is ONE compact line — distilled, never narration (causal tail matters)
26
+ MAX_REQUIREMENTS = 20 # bounded STANDING REQUIREMENTS contract (count) — the moat's no-unbounded-growth
27
+ MAX_REQ_CHARS = 300 # each requirement is ONE compact line (contracts — a long signature must survive)
28
+ MAX_PLAN_ITEMS = 20 # bounded PLAN (TodoWrite) — same no-unbounded-growth rule as requirements
29
+ MAX_PLAN_CHARS = 300 # each plan step is ONE compact line (multi-file scope must survive)
30
+ _PLAN_MARK = {"done": "x", "in_progress": "~", "pending": " "}
31
+ MAX_MISSION_CHARS = 500 # the MISSION (session north-star) is ONE compact objective line (don't clip scope)
32
+
33
+ MAX_REPORT_CHARS = 280 # OPEN USER REPORT — one compact verbatim line (bounded; never a transcript)
34
+ MAX_ACTION_LOG = 24 # bounded anti-loop tally (no-transcript: the action_log can't grow per-topic forever)
35
+ MAX_ACTION_SHOWN = 12 # cap on REPEATED/FAILING entries rendered (highest-signal first)
36
+
37
+ # Working-set view caps (the OPEN FILES region). A working-set file is shown IN FULL up to
38
+ # FULL_FILE_LINES; only a PATHOLOGICALLY huge file collapses to its RELEVANT REGION (REGION_LINES).
39
+ # Co-located here because they parameterize the OPEN FILES region renderer (build_artifacts in
40
+ # seed.py imports them from here — one direction). DISCOVERY_K is the RELATED CODE region's k.
41
+ FULL_FILE_LINES = 1200
42
+ REGION_LINES = 400
43
+ DISCOVERY_K = 6
44
+ MAX_CONVERSATION = 4 # RECENT CONVERSATION ring — last N user<->assistant exchanges (short-range continuity)
45
+ CONVO_MSG_CHARS = 800 # per-message GIST cap in the conversation tier (count-bounded by MAX_CONVERSATION;
46
+ # the cache holds the full text and recall pages it back, so this is a display-gist size, not the only copy)
47
+
48
+
49
+ # ── PER-REGION RENDER: UNCAPPED-BY-RELEVANCE ──────────────────────────────────
50
+ # _NO_CAP — the "no render cap" sentinel. OPEN FILES / YOUR NOTES are bounded by RELEVANCE
51
+ # (record_note dedup/retire), never by an arbitrary size cap — bound ≠ size, the slice shows all that's
52
+ # relevant. The only hard limit is the physical context window, handled by the loop's overflow path
53
+ # (drop the oldest accumulated exchange), not by truncating a tier.
54
+ _NO_CAP = 1_000_000
55
+
56
+
57
+ # `one_line` is re-exported from text_utils (single definition — pfc.py/seed.py/neocortex.py import the
58
+ # real definition directly). Kept importable from regions too for the existing call sites here.
59
+
60
+
61
+ def render_reviewed(s) -> str:
62
+ """The recall_history RATCHET tier — lookbacks already done this task, so the model sees the
63
+ lookback advanced the state (and doesn't re-fetch the SAME turn). Only rendered when something's
64
+ been reviewed. Non-suppressive wording: it guards against re-fetching what's already paged in, but
65
+ still invites fetching a DIFFERENT turn (recall is a normal read, not a smell)."""
66
+ if not s.reviewed:
67
+ return ""
68
+ return ("# HISTORY REVIEWED (already paged in this task — their content is in YOUR NOTES / RECENT "
69
+ "above; fetch a DIFFERENT turn from PAGED-OUT HISTORY if you need more)\n"
70
+ f"{', '.join(s.reviewed[-MAX_REVIEWED:])}\n\n")
71
+
72
+
73
+ def render_cache_manifest(refs) -> str:
74
+ """PAGED-OUT HISTORY body: one locator line per earlier turn of THIS session (NOT in the slice),
75
+ each ending with the EXACT call to page it back — so reaching back is copy-paste, not a blind
76
+ guess. This is the TRIGGER the dead recall channel was missing: a cache the model can't see is a
77
+ cache it never calls (the read-side analogue of REPO MAP advertising file paths). ``refs`` are
78
+ locator-only PageRefs from PageTable._episodes_thissession (ONE read seam); this is pure
79
+ formatting. MOAT: locators only — turn/title/breadcrumb, never content; the turn's body pages in
80
+ on demand and is bounded by recall_history's own caps."""
81
+ if not refs:
82
+ return ""
83
+ lines = []
84
+ for r in refs:
85
+ if r.handle == "…older":
86
+ lines.append(f"- {r.preview}") # the "+N earlier" tail (no single-turn call)
87
+ else:
88
+ lines.append(f"- {r.preview} → recall_history(turns=[{r.handle}])")
89
+ return "\n".join(lines)
90
+
91
+
92
+ def render_focus(focus, extra_roots, *, home: str = "", workspace: str = "") -> str:
93
+ """CURRENT PROJECT body: the dir the agent is actively working in, when it has moved beyond the boundary
94
+ root. Surfaces the auto-granted file-tool reach + the moved relative-path base (otherwise INVISIBLE →
95
+ the model stays in the start-dir frame and can't resolve 'the project' / a bare filename to where the
96
+ work actually is, then re-asks or cold-searches — the hunter 'index.ts' miss). The boundary (the floor)
97
+ never moves; this is the frame on top of it. Self-suppresses for the common single-project case."""
98
+ def short(p: str) -> str:
99
+ return ("~" + p[len(home):]) if home and p.startswith(home) else p
100
+ roots = [r for r in (extra_roots or []) if r and r != workspace]
101
+ if not roots and not (focus and focus != workspace):
102
+ return ""
103
+ lines = []
104
+ if focus and focus != workspace:
105
+ lines.append(
106
+ f"You are now working in `{short(focus)}`. Bare relative paths resolve HERE, and your file "
107
+ f"tools — read_file, list_files, grep, edit_file — act here. Resolve a bare filename or "
108
+ f"\"the project\"/\"it\" against THIS and the RECENT CONVERSATION first; do NOT fall back to a "
109
+ f"boundary-wide search or re-ask when the referent is already clear from recent work.")
110
+ others = [short(r) for r in roots if r != focus]
111
+ if others:
112
+ lines.append("Also within your boundary (reachable by file tools): " + ", ".join(f"`{o}`" for o in others) + ".")
113
+ return "\n".join(lines)
114
+
115
+
116
+ def render_skills(active_skills: list[dict]) -> str:
117
+ if not active_skills:
118
+ return wrap_untrusted("", kind="skill")
119
+ joined = "\n\n".join(f"## SKILL: {sk['name']}\n{sk['body']}" for sk in active_skills)
120
+ return wrap_untrusted(joined, kind="skill")
121
+
122
+
123
+ def render_threads(refs) -> str:
124
+ """Render the bounded OTHER OPEN THREADS index (parked topics the model can resume)."""
125
+ if not refs:
126
+ return ""
127
+ lines = [f"- [{r.task_id}] {r.title} ({r.status})" for r in refs[:MAX_OPEN_THREADS]]
128
+ extra = len(refs) - min(len(refs), MAX_OPEN_THREADS)
129
+ if extra > 0:
130
+ lines.append(f"- …and {extra} more")
131
+ return "\n".join(lines)
132
+
133
+
134
+ def render_conversation(s) -> str:
135
+ """The RECENT CONVERSATION tier: the last few COMPLETED user<->assistant exchanges (the in-progress
136
+ one is excluded — its user message is the current task). Ends with a pointer to recall the rest."""
137
+ prior = [e for e in s.conversation[:-1] if e.get("user")]
138
+ if not prior:
139
+ return ""
140
+ lines = []
141
+ n = len(prior)
142
+ for idx, e in enumerate(prior):
143
+ lines.append(f"- user: {e['user']}")
144
+ if e.get("assistant"):
145
+ lines.append(f" you: {e['assistant']}")
146
+ if e.get("truncated"):
147
+ # the gist above is a CUT of a longer reply — advertise the exact recall so the model pages
148
+ # the FULL text back instead of confabulating detail past the cut (last=1 == this reply).
149
+ lines.append(f" ⋯ (shortened to a gist — recall_history(last={n - idx}) for the FULL "
150
+ f"reply before answering about its specifics; do NOT guess past the cut)")
151
+ older = s.turns - len(prior) - 1 # turns beyond the ring (minus the current in-progress turn)
152
+ tail = (f"\n(+{older} earlier turn(s) this session not shown — they're listed in PAGED-OUT HISTORY "
153
+ "below; recall_history(turns=[N]) to view any)") if older > 0 else ""
154
+ return "\n".join(lines) + tail
155
+
156
+
157
+ # I1 PROVENANCE — narration filter. A FINDING must be a durable FACT, never the model's running
158
+ # narration. Notes that merely announce intent ("Let me run it", "I'll check the file", "Now I'll
159
+ # edit X", "Next, …") carry no established fact: folding them made FINDINGS read like a transcript
160
+ # and let "**Done — built it**" ratchet as an ESTABLISHED truth (F1/C3/G5). Task-agnostic + cheap:
161
+ # pure lexical, no LLM. Matched at the START of the note (the leading clause sets its kind).
162
+ _NARRATION_RE = re.compile(
163
+ r"^\s*(?:ok(?:ay)?[,. ]+)?(?:"
164
+ r"(?:let'?s|let me|let us|i['’]?ll|i will|i['’]?m going to|i am going to|now i|now let|"
165
+ r"then i|i need to|i should|going to|gonna|i plan to)\b"
166
+ r"|(?:next|first|then)\b[,. ]" # leading sequencing adverbs ("Next, …", "First …")
167
+ r")",
168
+ re.I,
169
+ )
170
+ # A note that ASSERTS completion ("done", "all set", "task complete", "finished") is a CLAIM, not an
171
+ # observation — durable ONLY if a real tool RESULT backed it (see slice_sink). Detected so the source
172
+ # can be DOWNGRADED to "claim" (rendered "unverified — confirm against OPEN FILES"), never silently
173
+ # promoted to an established truth. Task-agnostic lexical signal; no LLM.
174
+ _DONE_CLAIM_RE = re.compile(
175
+ r"\b(?:done|all set|all done|complete(?:d|ly)?|finished|it works|works now|ready to use|"
176
+ r"task (?:is )?(?:done|complete)|already (?:done|complete|built|implemented)|"
177
+ r"successfully (?:built|created|implemented|added|completed))\b",
178
+ re.I,
179
+ )
180
+
181
+
182
+ def is_done_claim(text: str) -> bool:
183
+ """True when `text` asserts the work is finished — a claim that needs an observation to be durable."""
184
+ return bool(_DONE_CLAIM_RE.search(text or ""))
185
+
186
+
187
+ # RECALL-ON-CUT marker (see memory: recall-ring-truncation-gap). A silent one_line() cut with no signal
188
+ # reads as "this is the whole thing" — the model then RE-DERIVES the missing part from scratch instead of
189
+ # recalling it, and a re-derived answer usually does NOT match the original (confabulation, not correction).
190
+ # Found live TWICE via two independent cut sites (a bug-hunt reply cut in the RECENT CONVERSATION ring, then
191
+ # again cut here in FINDINGS/OPEN USER REPORT) — any NEW site that bounds model- or user-authored text with
192
+ # one_line() should go through this helper rather than a bare one_line() call.
193
+ _RECALL_ON_CUT_MARK = " [cut — PARTIAL; see PAGED-OUT HISTORY or recall_history(search=...) for the rest, don't guess]"
194
+
195
+
196
+ def _cut_with_recall_marker(text: str, cap: int) -> str:
197
+ """one_line(text, cap), but if the cut actually removed content, replace the tail with a marker
198
+ naming the cut + the two general recall paths (no turn number is available at these call sites,
199
+ unlike the RECENT CONVERSATION ring which knows an exact recall_history(last=K)) + an explicit
200
+ don't-guess instruction."""
201
+ was_cut = len(one_line(text, cap + 1)) > cap
202
+ if not was_cut:
203
+ return one_line(text, cap)
204
+ return one_line(text, max(0, cap - len(_RECALL_ON_CUT_MARK))) + _RECALL_ON_CUT_MARK
205
+
206
+
207
+ def record_note(s, text: str, source: str = "tool-note") -> bool:
208
+ """Fold the model's per-turn note (a distilled FACT it established) into the FINDINGS tier.
209
+ Returns True iff a GENUINELY NEW finding was added (not narration, not a dedup refresh) — the
210
+ convergence check uses this so 'actively learning' doesn't count as 'spinning' (review #5).
211
+
212
+ The slice carries no transcript, so a reasoning model would otherwise re-derive the
213
+ situation each turn (costly reasoning bursts). This lets it carry its OWN conclusions
214
+ forward — bounded (ring of MAX_FINDINGS) and deduped so it stays distilled, not a log.
215
+
216
+ I1 PROVENANCE: a finding is a FACT FROM THE WORLD, never raw narration. Notes that announce
217
+ intent ("Let me…", "I'll…") are dropped — they're transcript, not established state. `source`
218
+ tags where the fact came from ("observed" > "tool-note" > "claim"); a completion ("done") note
219
+ is downgraded to "claim" unless the caller passed an observed source, so it can't ratchet into
220
+ an ESTABLISHED truth. No extra LLM call — pure lexical, captured from the note arg on a real call.
221
+
222
+ RECALL BRIDGE: a long AssistantText reply (e.g. a multi-item bug-hunt report) folds in here as a
223
+ "claim" — a hard per-item cut to MAX_FINDING_CHARS, since findings must stay compact. See
224
+ _cut_with_recall_marker: without a signal, a later "what were those bugs" sees ONLY the surviving
225
+ fragment and re-derives the rest from the code instead of recalling it — a confirmed fabrication."""
226
+ note = _cut_with_recall_marker(text, MAX_FINDING_CHARS)
227
+ if not note:
228
+ return False
229
+ if _NARRATION_RE.match(note): # pure intent/narration — carries no durable fact
230
+ return False
231
+ # a "done" claim is durable only if an observation backed it; otherwise it's a hypothesis
232
+ if source != "observed" and is_done_claim(note):
233
+ source = "claim"
234
+ is_new = note not in s.findings # genuinely new knowledge vs a refresh of an existing finding
235
+ if not is_new: # already established — refresh its recency, don't duplicate
236
+ s.findings.remove(note)
237
+ s.findings.append(note)
238
+ # BOUNDED = SEAL THE LOOP, not cut within it: findings are NOT truncated or retired inside a loop —
239
+ # every distinct conclusion the loop established stays whole (any within-loop cut harms the LLM). The
240
+ # only reduction is exact-duplicate dedup above (same fact refreshed, no information lost). The bound
241
+ # is the loop-boundary SEAL (TurnEnd archive + a fresh next loop), never a within-section filter.
242
+ s.finding_source[note] = source
243
+ # keep the source map bounded to the LIVE finding set (no unbounded growth across turns)
244
+ live = set(s.findings)
245
+ for k in [k for k in s.finding_source if k not in live]:
246
+ del s.finding_source[k]
247
+ return is_new
248
+
249
+
250
+ # I1 PROVENANCE — per-source trust framing. The slice's #1 ground truth is OPEN FILES (disk);
251
+ # FINDINGS are the model's own prior notes, which must be VERIFIED, never blindly reused. We never
252
+ # render model-sourced text as "do not re-derive" (that authored the "already done" ratchet).
253
+ _SOURCE_TAG = {
254
+ "observed": "", # backed by a tool result — trust, but OPEN FILES still wins
255
+ "tool-note": " (your note — verify against OPEN FILES)",
256
+ "claim": " (UNVERIFIED claim — confirm against OPEN FILES/a tool result before relying on it)",
257
+ }
258
+
259
+
260
+ def render_findings(findings: list[str], sources: dict | None = None) -> str:
261
+ if not findings:
262
+ return ""
263
+ sources = sources or {}
264
+ return "\n".join(f"- {f}{_SOURCE_TAG.get(sources.get(f, 'tool-note'), '')}" for f in findings)
265
+
266
+
267
+ def render_world(world: dict) -> str:
268
+ """The agent's durable WORLD MODEL — a maintained key→value scratchpad (maze map, inventory,
269
+ system state, plan). Long/multiline values render as their own block; short ones as bullets.
270
+ No cap (bound = the seal, not a cut): the whole maintained state renders into each turn's seed."""
271
+ if not world:
272
+ return ""
273
+ parts = []
274
+ for k, v in world.items():
275
+ v = str(v)
276
+ if "\n" in v or len(v) > 80:
277
+ parts.append(f"## {k}\n{v}")
278
+ else:
279
+ parts.append(f"- {k}: {v}")
280
+ return "\n".join(parts)
281
+
282
+
283
+ def render_requirements(requirements: list[dict]) -> str:
284
+ """The STANDING REQUIREMENTS contract body: the constraints that must hold when the task is DONE.
285
+ Self-suppresses when empty (a greeting/question has no contract → no bytes, no binding spec — the
286
+ structural kill for the 'first message becomes the spec' bug). Append-order + status-flip-in-place
287
+ (open '- [ ]', satisfied '- [x] … (done)') so a change touches only its own line and unrelated
288
+ turns stay byte-identical (warm STABLE prefix). Bounded by MAX_REQUIREMENTS (folded in slice_sink)."""
289
+ if not requirements:
290
+ return ""
291
+ return "\n".join(f"- [{'x' if r.get('done') else ' '}] {r.get('text', '')}" + (" (done)" if r.get("done") else "")
292
+ for r in requirements)
293
+
294
+
295
+ def render_plan(plan: list[dict]) -> str:
296
+ """The PLAN tier body: the model's ordered execution steps with live status (todo list).
297
+ Numbered + status-marked ('[~]' in-progress, '[x]' done, '[ ]' pending). Self-suppresses when empty.
298
+ Bounded by MAX_PLAN_ITEMS (folded in slice_sink). Volatile WORKING state — distinct from STANDING
299
+ REQUIREMENTS (acceptance criteria): this is the step sequence and the agent's live progress through it."""
300
+ if not plan:
301
+ return ""
302
+ return "\n".join(f"{i}. [{_PLAN_MARK.get(it.get('status'), ' ')}] {it.get('step', '')}"
303
+ for i, it in enumerate(plan, 1))
304
+
305
+
306
+ # ── ANTI-LOOP / RECENT / CURRENT ERROR ────────────────────────────────────────
307
+ # the underlying operations inside an execute_code body — so the anti-loop tally can see
308
+ # THROUGH code-as-action (otherwise every script is a unique signature and loops hide)
309
+ _CODE_OP_RE = re.compile(
310
+ r"\b(read_file|write_file|append_file|str_replace|list_files|run)\(\s*['\"]?([^'\",)]*)"
311
+ )
312
+
313
+
314
+ def code_ops(code: str) -> list[str]:
315
+ """Normalized operation list inside an execute_code body (op + the tail of its literal arg)."""
316
+ out, seen = [], set()
317
+ for op, arg in _CODE_OP_RE.findall(code or ""):
318
+ arg = arg.strip().split("/")[-1][:24]
319
+ sig = f"{op} {arg}".strip()
320
+ if sig not in seen:
321
+ seen.add(sig)
322
+ out.append(sig)
323
+ return out
324
+
325
+
326
+ def observe(out, n: int = 260) -> str:
327
+ """A one-line observation that PRESERVES THE TAIL. For most command output the decisive part —
328
+ the verdict, the final status, the exception — is at the END, so head-only truncation hides it
329
+ and the agent re-runs to 'see the result'. Task-agnostic: we don't interpret the outcome, we
330
+ just guarantee the end is visible. Keep a little head for context plus the whole tail."""
331
+ o = normalize_ws(out)
332
+ if len(o) <= n:
333
+ return o
334
+ if n < 8: # too small to split head+sep+tail; a plain head-cut is the bound
335
+ return o[:n] # (else tail = n-head-3 <= 0 and o[-0:] returns the WHOLE string)
336
+ head = n // 4
337
+ tail = n - head - 3 # 3 = len(" … "); head + sep + tail == n
338
+ return o[:head] + " … " + o[-tail:]
339
+
340
+
341
+ def action_sig(name: str, args: dict) -> str:
342
+ if name == "run_command":
343
+ return f"run_command `{one_line(args.get('command', ''), 50)}`"
344
+ if name == "execute_code":
345
+ ops = code_ops(args.get("code", ""))
346
+ return "execute_code[" + ", ".join(ops[:4]) + "]" if ops else "execute_code(script)"
347
+ if name in ("edit_file", "append_to_file", "str_replace", "read_file"):
348
+ return f"{name} {args.get('path', '')}"
349
+ if name == "list_files":
350
+ return f"list_files {args.get('path', '.')}"
351
+ return name
352
+
353
+
354
+ def record_action(s, name: str, args: dict, out: str, failing: bool | None = None) -> None:
355
+ """Fold one tool result into the action tally + error/exploration state (deterministic — no LLM).
356
+
357
+ `failing` is the AUTHORITATIVE flag from the tool layer (ToolText.ok / event.failing); the loop
358
+ passes it. The prose heuristic is a back-compat fallback only — relying on it misclassified a grep/
359
+ log line that legitimately starts with "Error" as a failure (corrupting last_error/anti-loop)."""
360
+ s.turn_actions = getattr(s, "turn_actions", 0) + 1 # per-turn exploration counter (finding-independent)
361
+ if failing is None:
362
+ failing = out.startswith("Error") or out.startswith("Exit code")
363
+ if failing:
364
+ s.last_error = out if len(out) <= 3000 else out[:2000] + "\n…[trace truncated]…\n" + out[-900:]
365
+ elif name in ("run_command", "execute_code"):
366
+ s.last_error = "" # a successful run/script clears the error (both are execution — general)
367
+ sig = action_sig(name, args)
368
+ prev = s.action_log.get(sig, {"count": 0})
369
+ s.action_log[sig] = {"count": prev["count"] + 1, "failing": failing, "last": observe(out, 100)}
370
+ if len(s.action_log) > MAX_ACTION_LOG:
371
+ # bounded like every tier (no-transcript): evict lowest-signal first — oldest one-shot,
372
+ # non-failing entries — so failing/repeated ones (the anti-loop signal) survive longest.
373
+ for k in [k for k, a in s.action_log.items() if a["count"] < 2 and not a["failing"]]:
374
+ if len(s.action_log) <= MAX_ACTION_LOG:
375
+ break
376
+ del s.action_log[k]
377
+ while len(s.action_log) > MAX_ACTION_LOG:
378
+ del s.action_log[next(iter(s.action_log))]
379
+
380
+
381
+ # POSIX-general signal that a command is UNAVAILABLE (not that the agent's code is wrong): the
382
+ # shell couldn't find/execute it (exit 127 = not found, 126 = not executable). Task-agnostic — no
383
+ # tool/language/runner name. Re-running an unavailable command can never succeed.
384
+ # Deliberately NOT "no such file" (a path mistake is usually fixable, not an unavailable command).
385
+ _CMD_UNAVAILABLE = ("command not found", "[exit 127]", "exit code 127",
386
+ "[exit 126]", "exit code 126", "not executable", "executable not found")
387
+
388
+
389
+ def render_action_history(action_log: dict) -> str:
390
+ entries = [(sig, a) for sig, a in action_log.items() if a["count"] >= 2 or a["failing"]]
391
+ if not entries:
392
+ return "- (nothing repeated or failing)"
393
+ entries.sort(key=lambda e: (e[1]["failing"], e[1]["count"]), reverse=True) # highest-signal first
394
+ extra = max(0, len(entries) - MAX_ACTION_SHOWN)
395
+ entries = entries[:MAX_ACTION_SHOWN]
396
+ lines = []
397
+ for sig, a in entries:
398
+ last_low = (a.get("last") or "").lower()
399
+ unavailable = sig.startswith(("run_command", "execute_code")) and any(m in last_low for m in _CMD_UNAVAILABLE)
400
+ if a["failing"] and a["count"] >= 2 and unavailable:
401
+ # the command itself is unavailable here — re-running can't fix it (general: env/tooling
402
+ # gap, not your code). Don't repeat it; finish if the work is done, else change command.
403
+ warn = (" ⚠ this command is UNAVAILABLE here — re-running can't fix it; if your work is "
404
+ "complete write the final summary, otherwise use a different command")
405
+ elif a["failing"] and a["count"] >= 3:
406
+ # same command, same failure, repeatedly — re-running won't change the outcome
407
+ warn = (" ⚠ REPEATEDLY FAILING the same way — re-running won't change it; fix the root cause, "
408
+ "or if your work is already complete, finish")
409
+ elif a["count"] >= 3:
410
+ # a non-failing action repeated this much is a soft loop (e.g. a str_replace whose
411
+ # old_string never matches, run via execute_code so it never trips the failing flag)
412
+ warn = " ⚠ REPEATED with no progress — STOP; change approach (read the full file, make ONE precise edit)"
413
+ elif a["failing"]:
414
+ warn = " (failing)"
415
+ else:
416
+ warn = ""
417
+ lines.append(f"- {sig} ×{a['count']}{warn} → {a['last']}")
418
+ if extra:
419
+ lines.append(f"- …and {extra} more repeated/failing (omitted)")
420
+ return "\n".join(lines)
421
+
422
+
423
+ # ── CONVERGENCE ───────────────────────────────────────────────────────────────
424
+ STOP_NUDGE_AFTER = 2 # non-edit tool calls since the last edit (with no error) before nudging to converge
425
+ READONLY_NUDGE_AFTER = 4 # read-only tool calls with NO edit at all before nudging to answer/act
426
+ EXPLORE_NUDGE_AFTER = 5 # tool calls in ONE turn with no edit before nudging to ANSWER or ask_user — keyed on
427
+ # turn_actions (finding-INDEPENDENT), so a read-heavy Q&A that records a note each step still converges
428
+ CLOSURE_MAX_SHOWN = 3 # max dangling-dependent locators in one CLOSURE block (bounds tokens; symbol-aware
429
+ # staleness keeps the set tiny + self-extinguishing, so no window cap is needed to prevent a cascade)
430
+
431
+
432
+ def render_closure(s) -> str:
433
+ """CHANGE-SET CLOSURE — the PRECISE half of 'verify before done'. After an edit settles, name the
434
+ dependents whose code STILL references a symbol your edit removed or moved: a dangling call-site a
435
+ coordinated change must fix (re-observation-reach = action-reach). SYMBOL-AWARE (SwapManager.prefetch
436
+ computes stale_deps from the code graph — a SENSORY CORTEX derived view, re-derived on file change,
437
+ not a persisted store: pre-edit defs - current defs, intersected with each
438
+ dependent's current ref tokens), so it is SILENT on feature-adds (nothing removed → never inflates a
439
+ non-refactor task) and on already-fixed sites (their tokens no longer name the symbol). Locator-only,
440
+ advisory, self-extinguishing (kept to UNOPENED stale deps), bounded; empty on a no-graph host. It does
441
+ NOT police a WRONG edit at an already-reached site (e.g. s3's depth bug) — that needs behavioral verify."""
442
+ if os.environ.get("SLICEAGENT_NO_CLOSURE"): # safety kill-switch for the gated rollout
443
+ return ""
444
+ stale = getattr(s, "stale_deps", None) or set()
445
+ # SYMBOL-AWARE: stale_deps (computed in SwapManager.prefetch) are the dependents whose CURRENT tokens
446
+ # still reference a symbol the edit removed/moved — silent on feature-adds (nothing removed). Keep only
447
+ # the UNOPENED ones so the nudge self-extinguishes the instant the model opens the site to fix/confirm
448
+ # (precise + terminating: no cascade on tasks whose callers don't need changing). Capped small.
449
+ if not stale or s.last_error or s.since_edit < STOP_NUDGE_AFTER:
450
+ return ""
451
+ active = set(s.active_files)
452
+ unclosed = sorted(p for p in stale if p not in s.edited_files and p not in active)[:CLOSURE_MAX_SHOWN]
453
+ if not unclosed:
454
+ return ""
455
+ edited = ", ".join(sorted(s.edited_files)[:4])
456
+ body = "\n".join(f"- {p} — still references a symbol you changed/moved in {edited}; open it and update "
457
+ f"it (or confirm it's correct)" for p in unclosed)
458
+ return ("# CHANGE-SET CLOSURE\nyour edit removed or moved a symbol these files still reference — a "
459
+ "coordinated change must fix every call-site, not just the definition:\n" + body + "\ndo NOT "
460
+ "declare done until each is updated or confirmed.\n\n")
461
+
462
+
463
+ def render_convergence(s) -> str:
464
+ """Convergence pressure against over-verification. Once a change exists and the agent has spent
465
+ several tool calls since its last edit with NO current error, it is re-checking something already
466
+ settled — tell it to finish. General + Markov: purely a function of state (edited? error?
467
+ calls-since-edit), no task/tool/language assumptions. Fires ONLY post-edit and ONLY when nothing
468
+ is broken, so it never cuts off active fixing (a failing check keeps last_error set → no nudge).
469
+ This SHRINKS wasted steps/tokens/time; the model still decides (it may continue for a real edit)."""
470
+ if not s.edited_files:
471
+ # EXPLORER children are SUPPOSED to do many read-only calls (their deliverable is the
472
+ # investigation); the read-only nudge below is for the TOP-LEVEL agent over-exploring instead
473
+ # of answering the user, and it was cutting delegated reviews short BEFORE the key (large) files
474
+ # were read. max_steps bounds an explorer, not this nudge.
475
+ if getattr(s, "explore_mode", False):
476
+ return ""
477
+ # READ-ONLY spin: many tool calls, nothing changed. Edit-gated convergence never fires here,
478
+ # so a trivial/answer-only task (greeting, "show the path", "summarize") over-explores. Nudge
479
+ # it to answer/act. General + Markov (edits vs non-edits, no task-type); dormant once anything
480
+ # is edited (→ the post-edit path below), so real edit-tasks are unaffected.
481
+ ta = getattr(s, "turn_actions", 0)
482
+ if not s.last_error and ta >= EXPLORE_NUDGE_AFTER:
483
+ strong = "STOP exploring NOW — " if ta >= EXPLORE_NUDGE_AFTER + 3 else ""
484
+ return (
485
+ f"# CONVERGENCE CHECK\n{strong}you've made {ta} tool calls this turn and edited nothing. Decide "
486
+ f"NOW — stop exploring (do NOT re-read what you've seen). If the task needs a CODE CHANGE, make "
487
+ f"your best-effort minimal edit immediately: never finish, and never run out of steps, having "
488
+ f"edited nothing — an empty result is a failure, a best-effort patch is not. If the task only "
489
+ f"needs an ANSWER, answer the user now (cite OPEN FILES) and make NO tool call; if it is "
490
+ f"genuinely ambiguous, call ask_user with ONE concise question.\n\n")
491
+ return ""
492
+ if s.last_error or s.since_edit < STOP_NUDGE_AFTER:
493
+ return ""
494
+ if render_closure(s): # an unreached dependent outranks the done-nudge (targeted > frequency):
495
+ return "" # show CLOSURE instead of STOP so the model finishes the refactor first
496
+ strong = "STOP NOW — " if s.since_edit >= STOP_NUDGE_AFTER + 2 else ""
497
+ return (
498
+ f"# CONVERGENCE CHECK\n{strong}you have edited {len(s.edited_files)} file(s) and made "
499
+ f"{s.since_edit} tool calls since your last edit with no error — the change appears complete and "
500
+ f"verified as well as the environment allows. Write your final summary and make NO tool "
501
+ f"call. Continue ONLY to make a SPECIFIC new edit you have identified — do NOT re-read or re-run a "
502
+ f"check you have already passed.\n\n"
503
+ )
504
+
505
+
506
+ # ── OPEN USER REPORT ──────────────────────────────────────────────────────────
507
+ # I3 — OPEN USER REPORT capture heuristic. A user follow-up that looks like a FAILURE REPORT ("it
508
+ # can't play", "it doesn't work", "still broken", "cd: no such file") is the user pushing back on a
509
+ # (possibly false) "done" — the dialectic a Markov snapshot loses. We carry it as a blocker the model
510
+ # must verify against the REAL artifact before re-claiming done (it drove the "already done" ratchet:
511
+ # F1's user-pushback half). Task-agnostic + LLM-agnostic: pure lexical, no command/tool parsing, no
512
+ # model call. Two signals: (a) negation/failure phrasing about the work, (b) a literal error/diagnostic
513
+ # pasted from a terminal (a shell/runtime error string the user is reporting back).
514
+ _USER_REPORT_RE = re.compile(
515
+ r"(?:"
516
+ # explicit failure/negation about the artifact
517
+ r"\b(?:doesn'?t|does not|don'?t|do not|won'?t|will not|can'?t|cannot|can ?not)\b\s*"
518
+ r"(?:\w+\s+){0,3}?(?:work|works|run|runs|play|plays|load|loads|open|opens|start|starts|build|builds|compile|compiles)\b"
519
+ r"|\b(?:not|isn'?t|aren'?t|wasn'?t)\s+(?:\w+\s+){0,2}?(?:work|working|run|running|play|playing|load|loading|right|correct)\b"
520
+ r"|\b(?:still\s+)?(?:broken|failing|fails|failed|crash(?:es|ed|ing)?|errored|buggy|not working)\b" # bare 'error'/'bug' dropped (dev vocabulary, not a report); re-admitted with context below
521
+ r"|\b(?:it|this|that)\s+(?:still\s+)?(?:doesn'?t|does not|won'?t|can'?t|cannot)\b"
522
+ # a pasted terminal/runtime diagnostic the user is reporting
523
+ r"|\b(?:no such file|command not found|traceback|exception|permission denied|"
524
+ r"syntaxerror|nameerror|typeerror|modulenotfound|exit code|segmentation fault)\b"
525
+ r"|:\s*no such file or directory\b"
526
+ # phrasings the first pass missed: hangs / no-output, red|failing tests/build,
527
+ # "didn't fix it", "same error still", HTTP 4xx/5xx in a failure context, ModuleNotFoundError.
528
+ r"|\b(?:hang(?:s|ing|ed)?|frozen|freeze(?:s|ing)?|stuck)\b"
529
+ r"|\bnothing (?:happen(?:s|ed)?|shows?|showed|loads?|loaded|renders?|rendered)\b"
530
+ r"|\b(?:tests?|the build|build|ci|pipeline)\b(?:\s+\w+){0,2}?\s+(?:are|is|still|now)?\s*(?:red|failing|fail|broken)\b"
531
+ r"|\b(?:failing|red|broken)\s+(?:tests?|build|ci)\b"
532
+ r"|\bdid(?:n'?t|\s+not)\b(?:\s+\w+){0,2}?\s*fix\b"
533
+ r"|\b(?:still|same)\b(?:\s+\w+){0,3}?\s+(?:error|issue|problem|bug|failure|failing|broken)\b"
534
+ r"|\bhttp\s*[45]\d\d\b|\b[45]\d\d\s+(?:error|not found|internal server)\b" # dropped bare 'status'/'response' (feature-spec phrasing, e.g. 'return a 404 status')
535
+ r"|\b(?:return(?:s|ed|ing)?|get(?:s|ting)?|got|throw(?:s|n|ing)?|give[sn]?)\s+(?:an?\s+)?(?:http\s*)?[45]\d\d\b"
536
+ r"|\bmodulenotfounderror\b"
537
+ r")",
538
+ re.I,
539
+ )
540
+
541
+
542
+ def is_user_report(text: str) -> bool:
543
+ """True when a user message looks like a FAILURE REPORT about prior work — captured as an OPEN
544
+ USER REPORT blocker. Conservative + task-agnostic (pure lexical); a normal directive that merely
545
+ contains 'add'/'fix' is NOT a report unless it carries an explicit failure/negation signal."""
546
+ return bool(_USER_REPORT_RE.search(text or ""))
547
+
548
+
549
+ def capture_user_report(s, message: str) -> bool:
550
+ """If `message` looks like a failure report, store it (verbatim, bounded) as the OPEN USER REPORT
551
+ blocker on the slice and return True. A NEWER report replaces an older one (most-recent wins,
552
+ inherently bounded). Returns False (and leaves any prior report intact) for a non-report message —
553
+ so a benign follow-up does NOT clear a still-open report.
554
+
555
+ The CAPTURING turn also shows the message in full via CURRENT REQUEST (no cap there); the risk is a
556
+ LATER turn, where this bounded field is the only surviving copy — see _cut_with_recall_marker."""
557
+ if not is_user_report(message):
558
+ return False
559
+ s.open_report = _cut_with_recall_marker(message, MAX_REPORT_CHARS)
560
+ return True
561
+
562
+
563
+ # ── REGION_ORDER — the slice layout, region-by-region ─────────────────────────
564
+ # The slice is an address space of TYPED REGIONS. REGION_ORDER encodes their EXACT render order and
565
+ # the stable/volatile split that governs prompt-cache locality. A prefix cache matches only up to the
566
+ # first byte that differs from the previous request, so the STABLE BULK (OPEN FILES, RELATED CODE,
567
+ # skills, memory, conversation — byte-identical across the common read-only / reasoning steps) LEADS,
568
+ # and the VOLATILE tier (findings, action tally, RECENT, error, convergence — changes most steps) is
569
+ # the recency-salient TAIL: the immediate state and the high-authority blocker/error sit right above
570
+ # NOW. Each region renders its OWN framed fragment (header + body + spacing) and SUPPRESSES itself
571
+ # when empty (returns ''); render_regions joins the fragments. This replaces render_slice's
572
+ # hand-ordered parts[] list — the iteration MUST equal the old concatenation byte-for-byte.
573
+ #
574
+ # `slot` groups fragments into the original parts[] elements (fragments in the same slot are
575
+ # concatenated, in REGION_ORDER order, into one "\n".join part); the slot sequence + blank-line
576
+ # glue is fixed in render_regions. `tier` documents the stable/volatile split.
577
+ STABLE, VOLATILE = "stable", "volatile"
578
+
579
+
580
+ # Each region is (name, tier, render(ctx)->framed-fragment, slot). The renderer OWNS its header
581
+ # literal + spacing and SUPPRESSES itself (returns '') when empty. `tier` documents the
582
+ # stable-bulk/volatile-tail split (prompt-cache locality). `slot` maps the fragment onto the former
583
+ # CURRENT REQUEST (the live user ask) and the NOW footer render OUTSIDE the <context> envelope in
584
+ # slice.build() — NOT as REGION_ORDER entries. The envelope marks "reference STATE"; the live INSTRUCTION must
585
+ # frame it from OUTSIDE, at both ends (primacy + recency), with NOW as the outermost tail. ONE `goal` source
586
+ # feeds both request copies (no primacy/recency divergence).
587
+ _CURRENT_REQUEST_HDR = ("# CURRENT REQUEST (what the user is asking for RIGHT NOW — your PRIMARY instruction; "
588
+ "address THIS)\n")
589
+ _NOW_FOOTER = ("# NOW: address the CURRENT REQUEST above. If it asks a QUESTION or for an explanation, answer "
590
+ "it directly (read/grep to ground the answer if useful — you need NOT edit); if it asks for a "
591
+ "CHANGE, make it with tools based on OPEN FILES; once the request is fully handled and verified "
592
+ "as well as the environment allows, write your final summary and make NO tool call.")
593
+
594
+
595
+ def render_current_request(goal: str) -> str:
596
+ """The live user ask, rendered OUTSIDE the context fence (used at BOTH primacy and recency from
597
+ one source). Empty goal → '' (no header)."""
598
+ g = (goal or "").strip()
599
+ return f"{_CURRENT_REQUEST_HDR}{g}\n\n" if g else ""
600
+
601
+
602
+ def render_now(hints: str = "") -> str:
603
+ """The intent-aware NOW footer — the OUTERMOST tail (after the fence closes), so the final instruction
604
+ reads as an instruction, not as 'context'. `hints` = pre-framed SUBDIRECTORY CONTEXT prefix (may be '')."""
605
+ return (hints or "") + _NOW_FOOTER
606
+
607
+
608
+ # parts[] grouping: fragments sharing a slot are concatenated, in order, into one "\n".join part —
609
+ # so the iteration equals the old hand-ordered concatenation BYTE-FOR-BYTE. (Provenance framing for
610
+ # # YOUR NOTES / the # OPEN USER REPORT blocker / the # REPEATED-FAILING header all live in the
611
+ # literals below — relocated verbatim from render_slice, not duplicated.)
612
+ REGION_ORDER = (
613
+ # STANDING REQUIREMENTS — the live contract that must hold when the task is DONE: a model-curated set
614
+ # of constraints (exact signature, output format, stated rule, an added requirement), maintained in-band
615
+ # via require/requirement_done/drop_requirement. NOT the frozen first message — EMPTY by default, so a
616
+ # greeting/question renders nothing (the structural kill for the 'first message = binding spec' bug).
617
+ # STABLE/slot-0 but write-RARELY (changes only on a require/drop/done event) → the prefix stays cache-warm.
618
+ # MISSION — the session-spanning NORTH STAR (goal mode): the overarching objective that persists
619
+ # ACROSS topic switches, above any single topic's goal. Self-suppresses when unset → zero bytes by
620
+ # default (no bloat), real opt-in-by-use feature. STABLE/slot-0, changes rarely → prefix stays cache-warm.
621
+ ("mission", STABLE, lambda c: (f"# MISSION (your overarching objective for this whole session — keep steering toward it across tasks until you call mission_done)\n{c['s'].mission}\n\n" if getattr(c['s'], 'mission', '') else ""), 0),
622
+ ("requirements", STABLE, lambda c: (f"# STANDING REQUIREMENTS (the contract that must HOLD when the task is done — honor each EXACTLY; '[x]' = already satisfied)\n{render_requirements(c['s'].requirements)}\n\n" if getattr(c['s'], 'requirements', None) else ""), 0),
623
+ ("open_files", STABLE, lambda c: "# OPEN FILES (live — your ground truth; edit based on this. Lines are numbered for citation/reference; the leading number is NOT part of the file — never include it in a str_replace old_string)\n" + c["artifacts"], 0),
624
+ ("related_code", STABLE, lambda c: (f"\n# RELATED CODE (repo map — relevant files & their definitions; read/grep for the actual code)\n{c['discovery']}\n" if c["discovery"] else ""), 1),
625
+ # REPO MAP moved to the BYTE-STABLE system prefix (make_build_slice) so it's a prompt-cache PREFIX
626
+ # shared across every turn + subagent, instead of full-price in the volatile user slice. (Region removed.)
627
+ ("skills", STABLE, lambda c: (f"# ACTIVE SKILL(S) (loaded instructions — FOLLOW these for the task)\n{render_skills(c['s'].active_skills)}\n\n" if render_skills(c["s"].active_skills) else ""), 2),
628
+ ("memory", STABLE, lambda c: (f"# RELEVANT MEMORY (lessons from past sessions — apply if useful)\n{c['memory']}\n\n" if c["memory"] else ""), 2),
629
+ ("conversation", STABLE, lambda c: (f"# RECENT CONVERSATION (the last few exchanges this session — for continuity; older turns are paged out — see PAGED-OUT HISTORY below for the recall_history call to fetch each)\n{render_conversation(c['s'])}\n\n" if render_conversation(c["s"]) else ""), 2),
630
+ ("findings", VOLATILE, lambda c: (f"# YOUR NOTES FROM PRIOR TOOL CALLS (established facts to REUSE — don't re-derive these; OPEN FILES stays the ground truth for current file contents. Per-note tags mark trust: no tag = observed, '(your note)' = your summary, '(UNVERIFIED claim)' = not yet confirmed)\n{render_findings(c['s'].findings[-c['max_findings']:], c['s'].finding_source)}\n\n" if render_findings(c["s"].findings[-c["max_findings"]:], c["s"].finding_source) else ""), 3),
631
+ ("plan", VOLATILE, lambda c: (f"# PLAN (your ordered steps & live progress — keep exactly ONE step in_progress; '[~]'=in progress, '[x]'=done, '[ ]'=pending; update with update_plan)\n{render_plan(c['s'].plan)}\n\n" if getattr(c['s'], 'plan', None) else ""), 3),
632
+ ("world", VOLATILE, lambda c: (f"# WORLD MODEL (durable task state YOU maintain — your map / inventory / progress; update with world_set, it persists across turns until the task changes)\n{render_world(c['s'].world)}\n\n" if c['s'].world else ""), 3),
633
+ ("reviewed", VOLATILE, lambda c: render_reviewed(c["s"]), 3),
634
+ ("threads", VOLATILE, lambda c: (f"# OTHER OPEN THREADS (parked topics — resume one with switch_topic; do NOT mix them into the current task)\n{c['threads']}\n\n" if c["threads"] else ""), 3),
635
+ # PAGED-OUT HISTORY — the cache MANIFEST: earlier turns of THIS session that are NOT in the slice,
636
+ # each with the exact recall_history call to page it back. Sits beside GHOST INDEX (same "it's paged
637
+ # out, here's the one call to get it" idiom — files there, turns here) so the model has a SEEN target
638
+ # to call; an unseen cache is the dead channel. Locators only (moat); suppresses itself when empty.
639
+ ("cache_manifest", VOLATILE, lambda c: (f"\n# PAGED-OUT HISTORY (earlier turns of THIS session — NOT in the slice; page any back with the call shown)\n{c['cache_manifest']}\n" if c.get("cache_manifest") else ""), 3),
640
+ # # REPEATED/FAILING ACTIONS header (always present; body says "(nothing…)" when empty) closes slot 3.
641
+ ("action_header", VOLATILE, lambda c: "# REPEATED/FAILING ACTIONS", 3),
642
+ ("action_history", VOLATILE, lambda c: render_action_history(c["s"].action_log), 4), # body — own part
643
+ # (CURRENT REQUEST renders OUTSIDE the fence in build() — see render_current_request above — not here.)
644
+ # REPO STATE — the LIVE world-state region (SENSORY CORTEX — a derived view, tier A): current branch
645
+ # + changed-file set, re-probed every build (not the session-start snapshot, and never persisted).
646
+ # High-authority current-state ground truth, so it rides in the salient tail just above the blocker/
647
+ # error. Suppresses itself when not a repo.
648
+ # CURRENT PROJECT — where the agent is working RIGHT NOW (the frame on top of the immutable boundary):
649
+ # the moved relative-path base + auto-granted file-tool reach, otherwise invisible. Rides the salient
650
+ # tail so a follow-up's referent resolves HERE. Self-suppresses for the single-project case.
651
+ ("focus", VOLATILE, lambda c: (f"# CURRENT PROJECT (where you are working RIGHT NOW — bare relative paths resolve here and your file tools reach here)\n{c['focus']}\n\n" if c.get("focus") else ""), 6),
652
+ ("worktree", VOLATILE, lambda c: (f"# REPO STATE (LIVE — current branch & changed files, re-read THIS turn; this is the up-to-date git state — trust it over any session-start project facts)\n{c['worktree']}\n\n" if c.get("worktree") else ""), 6),
653
+ # OPEN USER REPORT rides ABOVE the error (a stale "done" note can't outrank a user's BROKEN report);
654
+ # both are the highest-authority, freshest tail right above NOW.
655
+ ("user_report", VOLATILE, lambda c: (f"# OPEN USER REPORT (the user reports this is BROKEN — treat it as an UNRESOLVED blocker; do NOT claim it is done or already working until you have VERIFIED the fix against the real artifact, e.g. run/open it and observe success)\n{c['s'].open_report}\n\n" if c["s"].open_report else ""), 6),
656
+ ("error", VOLATILE, lambda c: (f"# CURRENT ERROR (unresolved — fix this, verbatim)\n{c['s'].last_error}\n\n" if c["s"].last_error else ""), 6),
657
+ ("closure", VOLATILE, lambda c: render_closure(c["s"]), 6),
658
+ ("convergence", VOLATILE, lambda c: render_convergence(c["s"]), 6),
659
+ # (NOW footer renders OUTSIDE the fence as the outermost tail in build() — see render_now above — not here.)
660
+ )
661
+
662
+
663
+ def render_regions(ctx: dict) -> str:
664
+ """Iterate REGION_ORDER, render each typed region into its framed fragment, and assemble the ONE
665
+ user string (the moat). Each region suppresses itself when empty; the slot grouping + the blank-line
666
+ separator between the action tally (slot 4) and the high-authority tail (slot 6) keeps the stable bulk
667
+ leading for prompt-cache locality and the volatile salient tail trailing. `ctx` carries the Slice + the
668
+ pre-rendered passthroughs (artifacts / discovery / memory / threads) + the max_findings cap."""
669
+ slots: dict[int, str] = {}
670
+ for _name, _tier, render, slot in REGION_ORDER:
671
+ slots[slot] = slots.get(slot, "") + render(ctx)
672
+ if not slots:
673
+ return ""
674
+ # #17: assemble by iterating ALL slot positions rather than a hand-synced literal index list — that
675
+ # list KeyError'd if a leading slot was empty and SILENTLY DROPPED any region added at a gap slot
676
+ # (e.g. 5). Slot 5 stays the reserved blank separator between the stable bulk (≤4, cache-leading) and
677
+ # the volatile high-authority tail (≥6); an empty slot renders as "" (a blank line), as before.
678
+ return "\n".join(slots.get(i, "") for i in range(max(slots) + 1))