# nagent Review v2.3 — Full Rewrite of the Latest Corpus

**Status:** Stand-alone delta. The current comprehensive review of the latest nagent corpus.
**Date:** 2026-06-12
**Corpus state:** nagent at commit `eb6be32a` (2026-06-12 00:25:50 UTC; the latest push as of this review)
**Length:** Long-form. Combines the breadth of the prior deep-dive review with the terse style (tables, SSDL tags, forth/array notation) the user prefers.

> **Reading guide.** This is a self-contained review of the latest nagent corpus. The 14 patterns from the README (the teaching-arc structure: build → rename → own → exploit → name → apply → compare) are covered in §2, one section per pattern. The 8 new commits since 2026-06-08 are catalogued in §1.3 and each new pattern is deep-dived in §3. The harvest pipeline is in §4; the cache strategy in §5; the compaction pattern in §6. Architecture + protocol + file-ops + candidates + artifacts + next-steps + references round out §7-§13.
>
> **Notation.** Tables (no JSON code blocks), SSDL shape tags (`[I]`, `->`, `o->`, `=>`, `[B]`, `[M]`, `[N]`, `[Q]`, `[S]`, `[T]`, `───`), forth/array notation (`a b +` for postfix math), file:line citations into both the nagent source and Manual Slop source. Boxes (`+--+--+`, `|...|`) for visual structure. The 7-column "Symbol, Name, Signature, Semantics, Example, Source, Shape" layout is the canonical table format throughout.
>
> **What this is not.** Not a delta on a prior review. Not a comparison table only. The 14 patterns + 12 new additions + 4 deep-dives + 16 future-track candidates + 15+ proposed artifacts are all here, in depth.

---

## 0. TL;DR

### 0.1 The headline finding

The latest nagent corpus (post-2026-06-08) introduces **knowledge harvest** as a first-class subsystem (`nagent-gc` → `~/.nagent/knowledge/`) with provenance-aware bullet lists, sha256-of-content ledger gating deletion, bounded digest injection, and per-file knowledge notes. This is the single largest design addition; it joins the existing 14 patterns as a 15th (knowledge as data, harvested, with provenance). The README was restructured into 7 Parts with a teaching arc; the structured-tag protocol gained a strict explicit parser (`nagent_tags.py`); a 5th provider (`claude-code` via the Claude Agent SDK) was added; stable-to-volatile context ordering became formal via `--cache-prefix-chars`; conversation compaction (`--compact`) joined the existing save/load/branch/edit maintenance set; project context files (`context.yaml` at the git toplevel) joined install + root context; a shared `context/data-oriented-design.md` was added; a `CLAUDE.md` for the agent harness that imports the shared DOD file via `@import` was added; the O(n²) splitter scoring was fixed to O(n) (13.6s → 0.008s on a 100KB cpp file); save-conversation was made graceful under summary-LLM failure; and the harvest was made dry-run by default with sha256 dedup.

### 0.2 The patterns (compact table)

| # | Pattern | SSDL shape | Manual Slop equivalent | Verdict | New track? |
|---|---|---|---|---|---|
| 1 | Text in, text out | `[I]` | `ai_client.send()` | PARITY | none |
| 2 | Visible output protocol (8 tags) | `->B->` | (provider-native function calling) | ARCH-DIFF | none |
| 3 | The loop (append, call, parse, act) | `o->` | 3 loops (LLM/MMA/App) | PARITY | none |
| 4 | Tool discovery (`--description`) | `[I]` | `mcp_client.dispatch` (45 tools) | GAP (auto) | (subsumed by `mcp_architecture_refactor`) |
| 5 | You did not build an agent (data is the thing) | (philosophical) | (philosophical — Manual Slop agrees) | PARITY | none |
| 6 | Conversations are editable state | `[I]` | `disc_entries` + branching + UISnapshot | PARITY-DIFF-FOCUS | none |
| 7 | Repository history as data | `[I]` | `aggregate.py` diff injection; no git history | PARTIAL | 6 (MED) |
| 8 | Harvest knowledge, reclaim space | `o->` | (absent) | **GAP (Application)** | **11 (HIGH)** |
| 9 | Everything else files buy you | (lens) | (lens) | N/A | (no track) |
| 10 | Data-oriented design (named principles) | (philosophical) | (philosophical — Manual Slop agrees) | PARITY | none |
| 11 | Artifact neighborhoods | `[I]` | (no coedit tool) | GAP | 8 (LOW) |
| 12 | Managing context + large files | `[I]` | `aggregate.py` + tree-sitter + per-file slices | PARITY-DIFF-MECH | 9 (DEFER) |
| 13 | Per-file write conversations | `[I]` | (per-file discussion memory absent) | PARTIAL | 7 (LOW) |
| 14 | Own the inputs (vs framework abstractions) | (lens) | (lens) | N/A | (no track) |
| **NEW** | **Knowledge harvest** (`nagent-gc`) | `o->` | (absent) | **GAP** | **11 (HIGH)** |
| **NEW** | **Stable-to-volatile cache ordering** | `->M->` | (mechanism present, ordering not enforced) | **PARTIAL** | **12a (MED)** |
| **NEW** | **Cache TTL GUI controls** | `=>` | (no GUI for TTL) | **GAP (UX)** | **12b (MED)** |
| **NEW** | **Conversation compaction** | `->B->` | (have summarize, not compact) | **GAP** | **13 (MED)** |
| **NEW** | **Project context files** | `[I]` | `manual_slop.toml` (TOML ≠ YAML) | PARITY-DIFF-MECH | 14 (LOW) |
| **NEW** | **claude-code provider** (5th, sub auth) | `[I]` | `_send_gemini_cli` (parallel) | PARITY | none (provider add) |
| **NEW** | **AGENTS.md `@import` pattern** | `[I]` | `AGENTS.md` exists, no canonical file | **GAP** | **16 (HIGH)** |

### 0.3 The 16 future-track candidates (summary)

| # | Candidate | Pri | Effort | Shape |
|---|---|---|---|---|
| 1 | `SubConversationRunner` (1:1 sub-convos) | HIGH | Med | `=>` |
| 2 | RAG pre-staging via sub-convo | MED (down) | Sm | `o->` |
| 3 | Stateless `LLMClient` class | MED | Lg | `[I]` |
| 4 | Intent DSL for Meta-Tooling | LOW | research | `[I]` |
| 5 | Self-describing MCP tools | LOW (subsumed) | Med | `[I]` |
| 6 | `src/git_history.py` (nagent §7) | MED | Med | `[I]` |
| 7 | Per-file conversation log | LOW | Sm | `[I]` |
| 8 | `py_/ts_c_coedited_files` tools | LOW | Sm | `[I]` |
| 9 | Explicit `split_lib.py` / `patch_lib.py` | DEFER | Med | `[I]` |
| 10 | Raw-transcript persistence per Take | LOW | Sm | `[I]` |
| **11** | **Knowledge memory (3rd dimension)** | **HIGH** | Lg | `o->` |
| **12a** | **Stable-to-volatile cache ordering** | MED | Sm | `->M->` |
| **12b** | **Cache TTL GUI controls** | MED | Med | `=>` |
| **13** | **Conversation compaction** | MED | Sm | `->B->` |
| **14** | Project context file | LOW | Sm | `[I]` |
| **15** | Save-with-graceful-summary-failure | TBD | Sm | `->B->` |
| **16** | **AGENTS.md `@import` + canonical DOD** | **HIGH** | Sm | `[I]` |

### 0.4 The proposed new artifacts (15+ files)

| File path | Type | Purpose |
|---|---|---|
| `conductor/code_styleguides/data_oriented_design.md` | NEW | Canonical DOD reference (cloned/adapted) |
| `AGENTS.md` | UPDATE | Add `@import` line |
| `./docs/AGENTS.md` | NEW | Agent-facing mirror of `docs/Readme.md` |
| `conductor/code_styleguides/agent_memory_dimensions.md` | NEW | Codify the 4 memory dimensions |
| `conductor/code_styleguides/rag_integration_discipline.md` | NEW | Codify the conservative-RAG rule |
| `conductor/code_styleguides/cache_friendly_context.md` | NEW | Codify stable-to-volatile ordering + TTL GUI |
| `conductor/code_styleguides/knowledge_artifacts.md` | NEW | Codify the knowledge harvest pattern |
| `conductor/code_styleguides/feature_flags.md` | NEW | Codify "delete to turn off" |
| `docs/guide_knowledge_curation.md` | NEW | The knowledge memory guide |
| `docs/guide_caching_strategy.md` | NEW | Caching across providers |
| `docs/guide_agent_memory_dimensions.md` | NEW | Cross-cutting: 4 memory dimensions |
| `conductor/workflow.md` | UPDATE | Add TDD protocol for new patterns |
| `conductor/product-guidelines.md` | UPDATE | Add memory dimensions section |
| `docs/guide_mma.md` | UPDATE | Use "context management" framing |
| `docs/guide_ai_client.md` | UPDATE | Add cache TTL section |

---

## 1. The latest nagent corpus (2026-06-08 → 2026-06-12)

### 1.1 Repo state at this review

| Field | Value |
|---|---|
| Repo | `https://github.com/macton/nagent` |
| Head | `eb6be32a` (2026-06-12 00:25:50 UTC) |
| Pushed at | 2026-06-12T00:25:52Z |
| Created | 2026-06-06T02:49:46Z |
| Size | 469 KB |
| Stars | 86 |
| Watchers | 86 |
| Forks | 2 |
| Open issues | 0 |
| License | MIT |
| Language | Python (the only language; zero C, zero C++) |
| Default branch | `main` |

### 1.2 Full file tree (33 files)

```
nagent/
├── .gitignore                                                29B
├── CLAUDE.md                                              5,832B   NEW (agent-facing rules)
├── LICENSE                                                1,067B
├── README.md                                            36,161B   REWRITTEN (teaching arc)
├── config.example.json                                      49B
├── requirements.txt                                         94B   NEW (claude-agent-sdk)
├── context.yaml                                             34B   NEW (paths: [context/data-oriented-design.md])
├── context/
│   └── data-oriented-design.md                         13,084B   NEW (the canonical DOD)
├── prompts/
│   ├── compact-conversation.md                          3,237B   NEW
│   ├── create-readme.md                                28,245B   (workflow tool)
│   └── harvest-conversation.md                          1,674B   NEW (strict JSON output)
├── bin/
│   ├── nagent                                          88,078B   2,524 lines (main loop)
│   ├── nagent-llm-text                                 2,479B
│   ├── nagent-llm-upload                               4,584B
│   ├── nagent-file-edit                                4,123B
│   ├── nagent-file-patch                               2,876B
│   ├── nagent-file-split                               6,909B
│   ├── nagent-file-summarize                           4,781B
│   ├── nagent-gc                                       5,155B   NEW (the harvest CLI)
│   └── helpers/
│       ├── nagent-cli.py                               2,642B
│       ├── nagent-llm.py                              20,366B   + claude-code provider
│       ├── nagent-tags.py                              6,036B   NEW (explicit parser)
│       ├── nagent-gc-lib.py                           27,289B   NEW (the harvest library)
│       ├── nagent-file-edit-lib.py                    5,232B
│       ├── nagent-file-split-lib.py                  15,427B   + O(n) fix
│       ├── nagent-file-patch-lib.py                   5,086B
│       ├── nagent-file-summarize-lib.py               3,884B
│       └── nagent-file-split-{py,cpp,js,ts,json,yaml,md,xml,txt,go,rs,java}  (12 splitters, ~225B each)
├── tests/
│   ├── test-nagent.py                                106,128B
│   ├── test-nagent-file-edit.py                      28,393B
│   ├── test-nagent-file-split.py                     11,525B
│   ├── test-nagent-file-patch.py                      8,001B
│   ├── test-nagent-file-summarize.py                  9,106B
│   ├── test-nagent-gc.py                             27,306B   NEW
│   └── test-nagent-tags.py                            5,902B   NEW
```

### 1.3 The 8 new commits (chronological)

| # | Date (UTC) | Commit | Subject |
|---|---|---|---|
| 1 | 2026-06-11 03:32:50 | `2c3c78b` | Add conversation compaction and restore initial context on load |
| 2 | 2026-06-11 23:09:57 | `67a3ea5` | Add knowledge harvest, tag parser, and claude-code provider |
| 3 | 2026-06-11 23:10:12 | `d86bce8` | Add CLAUDE.md importing the shared data-oriented design rules |
| 4 | 2026-06-11 23:10:12 | `ee72cb4` | Rewrite README prompt around a teaching arc and regenerate README |
| 5 | 2026-06-12 00:17:34 | `0b9d1a2` | Ignore scratch files |
| 6 | 2026-06-12 00:17:34 | `5e269ca` | Add project context, prompt caching, and conversation direction |
| 7 | 2026-06-12 00:17:34 | `99e1270` | Regenerate README for project context, caching, and conversation direction |
| 8 | 2026-06-12 00:25:50 | `eb6be32` | Remove resolved issue files |

The 4 substantive commits are #1, #2, #6, #4. Commits #3, #5, #7, #8 are companion/cleanup.

### 1.4 The 4 substantive commits (long-form)

**Commit `2c3c78b` — Add conversation compaction and restore initial context on load** (2026-06-11 03:32:50)

> Introduce `--compact` with compaction guidance, preserve initial_context through edit flows, and ensure loaded conversations regain protocol preamble when missing.

**Commit `67a3ea5` — Add knowledge harvest, tag parser, and claude-code provider** (2026-06-11 23:09:57) — **the big one**

- `nagent-gc`: classify dead artifacts; harvest facts/decisions/tasks/questions/playbooks into `~/.nagent/knowledge/` with provenance and a sha256 ledger gate; inject a bounded digest into initial context; dry-run by default
- `nagent_tags.py`: explicit parser for the tag protocol replacing regex parsing; block helpers remove `re.sub` escape hazards
- claude-code provider via the Claude Agent SDK using the local Claude Code login; omitted model or "default" means Claude Code's configured model
- Install context: load `context.yaml`/`context.md` from the nagent folder before root context; ship `context/data-oriented-design.md` via repo `context.yaml`
- Fix `re.sub` escape corruption in `refresh_initial_context`, O(n²) splitter scoring (13.6s → 0.008s on a 100KB cpp file), binary reads crashing the loop, pid drift between nagent and `nagent-file-edit`, and write-path `expanduser` mismatch
- Save-conversation indexes the copy even when the summary LLM fails; fresh conversations build initial context once; compact prompt resolves root-first; edit/compact roll up child token stats; gc progress spinner and per-item status lines

**Commit `5e269ca` — Add project context, prompt caching, and conversation direction** (2026-06-12 00:17:34) — **the second big one**

- Initial context restructured stable-to-volatile: role instructions and the tag protocol (with inline per-tag guidance) lead; instance facts and environment trail, so request prefixes stay byte-identical across conversations of the same mode
- Protocol rules stated outright: raw bodies, first-close-wins, nothing outside tags, the loop contract (results appended, never fabricate), and errors-as-data
- New conversations-as-data block directs the model to reuse named workers (`conversation-file="name"`), resume saved conversations, author worker briefings under `/tmp`, and hand off to a fresh sub-conversation when its own context grows noisy
- Project context: a `context.yaml`/`context.md` at the git toplevel of the working directory is injected between install and root context, deduplicated when the project is the install or root directory
- Provider prompt caching: `call_llm` passes stable prefix boundaries via `--cache-prefix-chars`; the anthropic provider splits the message into `cache_control` blocks at those offsets; cached prompt tokens fold back into reported input counts

**Commit `ee72cb4` — Rewrite README prompt around a teaching arc and regenerate README** (2026-06-11 23:10:12)

> The prompt now organizes the README as a progression: build it, rename it, own the data, exploit the files, name the principles, the data structures that fall out (neighborhoods, context and large files, per-file conversations), and the framework comparison. Coverage updated for knowledge harvest, install context, the shared tag parser, compaction/branching, and the claude-code provider.

### 1.5 The new README structure (7 Parts, 14 numbered sections)

| Part | Section | Name | New? |
|---|---|---|---|
| Part I — Build It | 1 | Text In, Text Out | (existing) |
| Part I — Build It | 2 | Teach the Model an Output Format | (existing) |
| Part I — Build It | 3 | The Loop | (existing) |
| Part I — Build It | 4 | Tool Discovery | (existing) |
| Part II — Rename It | 5 | You Did Not Build an Agent | (existing) |
| Part III — Own the Data | 6 | Conversations Are Editable State | (existing, +compaction) |
| Part IV — Exploit the Files | 7 | Repository History as Data | (existing) |
| Part IV — Exploit the Files | 8 | **Harvest Knowledge, Reclaim Space** | **NEW** |
| Part IV — Exploit the Files | 9 | **Everything Else Files Buy You** | **NEW** |
| Part V — Name the Principles | 10 | **Data-Oriented Design** | **NEW (formal section)** |
| Part VI — The Data Structures That Fall Out | 11 | **Artifact Neighborhoods** | (renamed from "Neighborhoods") |
| Part VI — The Data Structures That Fall Out | 12 | **Managing Context and Large Files** | (renamed from "Large Files") |
| Part VI — The Data Structures That Fall Out | 13 | **Per-File Write Conversations** | (renamed from "Per-File Memory") |
| Part VII — How This Differs From Frameworks | 14 | **Own the Inputs** | (renamed from "Differences from Frameworks") |

3 new Parts (IV, V, VI explicit in v1.2; was a flat 14 in v1), 4 new numbered sections (8, 9, 10, and the new 11/12/13 expansions), 13-step Build Your Own (was 12; the new step 10 is the knowledge harvest).

### 1.6 What the 8 commits add (substantive summary)

| Sub-system | What it is | File:line | What it means for Manual Slop |
|---|---|---|---|
| Knowledge harvest | `nagent-gc` + `nagent_gc_lib.py` | `bin/nagent-gc:1-150` + `bin/helpers/nagent_gc_lib.py:1-700` | A *third memory dimension* (provenanced, user-editable, sha256-ledger-gated) |
| Stable-to-volatile cache ordering | `--cache-prefix-chars` flow | `bin/nagent:970-987,1013-1014` + `bin/helpers/nagent_llm.py:cache_prefix_blocks` | Caching is in place; ordering not formally enforced; needs the `aggregate.py:run` re-order |
| Conversation compaction | `--compact` + editable prompt | `bin/nagent:1975-2019` + `prompts/compact-conversation.md` | A rewriter (not a summarizer); the 10-question self-review is the contract |
| Project context files | `context.yaml` at git toplevel | `bin/nagent:641-656` | Per-project context files; parallels `manual_slop.toml` (different syntax) |
| claude-code provider | 5th provider, subscription auth | `bin/helpers/nagent_llm.py:65-80,195-220` | Parallels `_send_gemini_cli` |
| Per-file knowledge notes | `knowledge/files/{file_id}.md` | `bin/helpers/nagent_gc_lib.py:merge_harvest "files" branch` | New dimension of per-file memory; `FileItem.notes` absent |
| "Delete to turn off" feature flags | `rm digest.md` | `bin/helpers/nagent_gc_lib.py:regenerate_digest` | File presence = flag state |
| Save-with-graceful-summary-failure | Save indexes even on summary LLM fail | `bin/nagent:2150-2180` + `bin/helpers/nagent_gc_lib.py:run_gc` | The "errors are data" pattern applied to save |
| Shared `data-oriented-design.md` | 13,084-byte canonical rules | `context/data-oriented-design.md` | Manual Slop's parallel: a canonical DOD styleguide |
| `CLAUDE.md` `@import` pattern | Imports the shared DOD | `CLAUDE.md:1-150` | Manual Slop's parallel: update `AGENTS.md` with `@import` |
| O(n²) → O(n) splitter scoring | `nagent_file_split_lib.py` perf fix | `bin/helpers/nagent_file_split_lib.py:SCORE_BY_TYPE` | 13.6s → 0.008s on 100KB cpp |
| Strict tag parser | `nagent_tags.py` (replaces regex) | `bin/helpers/nagent_tags.py:1-160` | Tag protocol made explicit; parsing is data, not magic |
| 5 providers | `openai, anthropic, google, gemini (alias), cursor, claude-code` | `bin/helpers/nagent_llm.py:65-80` | Manual Slop has 8; same shape |

---

## 2. The 14 patterns in depth

### 2.1 Pattern 1: Text In, Text Out

**nagent's claim.** The smallest useful primitive is: file in, text out. LLMs forget. Therefore put the prompt in a file and treat the model as a temporary function over that data.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `bin/nagent-llm-text` | CLI front-end (50 lines) | Reads a text file, resolves provider + model, calls `generate_text_with_usage()`, prints plain text or `--json` |
| `bin/nagent-llm-upload` | CLI front-end (80 lines) | Sibling for images, PDFs, office files; rejects `.zip`, enforces 50MB limit |
| `bin/helpers/nagent_llm.py:generate_text_with_usage` | 5+1 providers (openai, anthropic, google, cursor, claude-code) | The single primitive: `file in → text out` |
| `bin/helpers/nagent_llm.py:PROVIDERS, DEFAULT_MODELS, CREDENTIAL_ENV` | Lines 65-80 | Provider + model + credential registry; `claude-code` has empty `CREDENTIAL_ENV` (local login) |

**The codepath** (SSDL):

```
[Q:file_path]
   │
   ▼
[Q:provider, model]    (resolved from CLI → config → defaults)
   │
   ▼
[I:generate_text_with_usage(message, provider, model)]
   │
   ▼
[I:provider-specific API call]
   │
   ▼
[I:_result_with_usage(text, usage, input_text)]
   │
   ▼
[T:print text]   or   [T:emit JSON with usage]
```

**Manual Slop equivalent.**

| Manual Slop | Where | What it does |
|---|---|---|
| `src/ai_client.py:send(...)` | Line ~2683 (the module is 2,883 lines) | 5 providers (gemini, anthropic, deepseek, minimax, gemini_cli); 8+ send paths |
| `src/ai_client.py:set_provider, set_model_params` | (module-level) | Provider + model selection |
| `src/ai_client.py:_ANTHROPIC_CHUNK_SIZE, _ANTHROPIC_MAX_PROMPT_TOKENS, _CHARS_PER_TOKEN` | (constants) | Token accounting (nagent uses `estimate_token_count` at 1 token per 4 chars) |
| `src/ai_client.py:get_token_stats` | (exported) | Per-conversation + per-prompt token totals |
| `src/ai_client.py:_result_with_usage` analogue | (each `_send_<provider>`) | Each provider returns a usage-metadata struct |

**Verdict.** **PARITY.** Both systems have file-in/text-out as the primitive. Manual Slop's `send()` is a strict superset (it also handles tool calls, RAG injection, tier attribution, patch mode). Provider churn is isolated to `_send_<provider>` in Manual Slop; to `nagent_llm.py` in nagent.

**SSDL shape.** `[I]` (single instruction).

**Manual Slop next steps.**
- Add `_send_claude_code` if user wants the 5th provider.
- Otherwise, none.

### 2.2 Pattern 2: Teach the Model an Output Format (Visible Output Protocol)

**nagent's claim.** Free-form model output is hard to execute. Use a visible protocol. The startup prompt lists the only tags the model may emit. The parser is strict: recognized tags and whitespace. Nothing else.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| The tag list | `bin/nagent:696-706` (in `build_initial_context`) | 8 tags: `nagent-response, nagent-read, nagent-file-read, nagent-file-patch, nagent-write, nagent-shell, nagent-next, nagent-conversation` |
| The protocol rules | `bin/nagent:708-713` | "Tag bodies are raw text. Do not escape characters. Never emit a literal close tag inside a body. Emit nothing outside tags. After your action tags run, each result is appended as a `<nagent-*-result>` block. Multiple tags in one turn run in order. A result with an error status is data: change your approach." |
| The explicit parser | `bin/helpers/nagent_tags.py:1-160` (replaces regex) | `TagNode` dataclass; `parse_tag_document`, `parse_element`, `find_block_span`, `extract_block`, `replace_first_block`, `remove_first_block`, `unwrap_whole_element` |
| `MAX_FORMAT_RETRIES = 3` | `bin/nagent` (in the loop) | On parse failure, append a `<system>` correction to the conversation and retry |

**The 8 tags** (full table from `bin/nagent:696-706`):

| Tag | Self-closing? | Meaning |
|---|---|---|
| `<nagent-response>{text}</nagent-response>` | no | Human-facing reply. A turn containing only this tag ends the run |
| `<nagent-read path="{file}"/>` | yes | Read a small file (≤64KB) inline |
| `<nagent-file-read path="{file}"/>` | yes | Read a file of any size; large files are split automatically |
| `<nagent-file-patch index="{index}"/>` | yes | Merge edited split segments back into the source file |
| `<nagent-write path="{file}">{content}</nagent-write>` | no | Write to an allowed path (main mode: tmpdir; file-edit mode: target) |
| `<nagent-shell>{commands}</nagent-shell>` | no | Run shell; output is appended to the conversation |
| `<nagent-next>{prompt}</nagent-next>` | no | Append a prompt to yourself and continue reasoning |
| `<nagent-conversation conversation-file="name">{prompt}</nagent-conversation>` | no | Continue a named worker conversation, or start from a file you wrote |
| `<nagent-conversation conversation-name="saved-name">{prompt}</nagent-conversation>` | no | Start a child from a saved conversation |

**Manual Slop equivalent.** Manual Slop uses **provider-native function calling** (Gemini `genai.types.FunctionDeclaration`, Anthropic `tool_use` blocks, etc.). The protocol is encoded in JSON the provider parses. The user cannot read a `function_call` from the comms log and reason about it without knowing the provider's schema.

**Verdict.** **ARCHITECTURAL DIFFERENCE** — the Application's choice is correct (parallel tool calls, JSON-mode constraints, native tool calling). The Meta-Tooling could legitimately use nagent's regex-tag protocol for its own work (per the `mcp_architecture_refactor_20260606` track's intent-based DSL placeholder, now substantively specced by the related `intent_dsl_survey_20260612` report). See §3.1 of that report (out of scope here).

**SSDL shape.** `->B->` (codepath with branch; the parser branches on tag name).

**Manual Slop next steps.**
- The new `mcp_architecture_refactor_20260606` sub-MCP extraction (already in plan) is the natural place to consider a tag-style protocol for the Meta-Tooling DSL.
- Otherwise, no Application-side change.

### 2.3 Pattern 3: The Loop (Append, Call, Parse, Act, Repeat)

**nagent's claim.** "Agent behavior" is mostly: append, call, parse, act, append, repeat. Heavier systems add infrastructure around the same steps.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| The 4-step reading order | `CLAUDE.md:55-57` | `main() → run_agent_loop() → call_llm() → parse_response() → process_tags()` |
| `main()` | `bin/nagent` | Sets up the conversation file, calls `run_agent_loop()` |
| `run_agent_loop()` | `bin/nagent` (in the main file) | The `while True` loop; appends user prompt, sends to LLM, appends response, runs tags, appends results, loops when action or response added state, stops on `nagent-response` |
| `call_llm()` | `bin/nagent:990-1019` | Spawns `nagent-llm-text` subprocess; passes `--cache-prefix-chars` for cache boundaries; returns `(response, input_tokens, output_tokens)` |
| `parse_response()` | `bin/nagent` | Uses `bin/helpers/nagent_tags.py:parse_tag_document`; strict; on failure, appends `<system>` correction and retries up to `MAX_FORMAT_RETRIES = 3` |
| `process_tags()` | `bin/nagent` (lines ~750-960+) | Dispatch on tag name; runs the action; appends the result; handles errors as data (not exceptions) |

**The codepath** (SSDL):

```
loop:
  [Q:conversation_text]    (read from disk)
     │
     ▼
  [I:append user_prompt]    (if first iteration)
     │
     ▼
  [I:subprocess nagent-llm-text --file conversation --cache-prefix-chars N --json]
     │
     ▼
  [I:append response]
     │
     ▼
  [B:response has action tags?]
     │
     ├──► [I:process_tags]    (run shell, read file, etc.)
     │       │
     │       ▼
     │   [I:append <nagent-*-result>]
     │
     └──► [B:response has <nagent-response>?]
                 │
                 ▼
              [T:print and stop]
```

**Manual Slop equivalent.** Manual Slop has *three* parallel loops:

| Loop | Where | Scope |
|---|---|---|
| `src/ai_client.py:_send_<provider>` | Each provider's send path | The per-provider tool-call loop (up to `MAX_TOOL_ROUNDS + 2 = 12` iterations) |
| `src/multi_agent_conductor.py:ConductorEngine.run` | (the MMA loop) | Per-ticket: reset session, build prompt, run worker lifecycle |
| `simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async` | (the 1:1 chat loop) | Per user turn: build markdown, send, wait, append response |

**Verdict.** **PARITY.** All three loops have the same shape (append, call, parse, act, repeat) but different data structures. nagent's `run_agent_loop` is ~50 lines, easy to reason about. Manual Slop's loops are 100-300 lines each, scattered.

**SSDL shape.** `o->` (codecycle; the loop repeats).

**Manual Slop next steps.**
- Candidate 3 (Stateless `LLMClient` class) would unify these three loops behind a single `run_loop(...)` function. Large refactor; not high priority.

### 2.4 Pattern 4: Tool Discovery (`--description` self-describing executables)

**nagent's claim.** Tool capability should be explicit data too. No central registry. Tools describe themselves.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `exit_on_description(description: str)` | `bin/helpers/nagent_cli.py` | If `--description` in `sys.argv`, print description and exit 0 |
| `collect_bin_tool_descriptions(bin_dir: Path)` | `bin/helpers/nagent_cli.py` | Iterate every executable in `bin/`, run with `--description`, parse stdout, concatenate |
| The 9 nagent tools | `bin/nagent-*` | Each starts with `exit_on_description(NAGENT_*_DESCRIPTION)` |

**The 9 nagent tools** (from the README's "Common Commands"):

| Tool | Role |
|---|---|
| `nagent` | Main structured conversation loop |
| `nagent-llm-text` | Send a text file to the configured LLM |
| `nagent-llm-upload` | Upload a supported file with a prompt |
| `nagent-file-edit` | Per-file conversation for one source file |
| `nagent-file-split` | Split large file into segments + `index.json` |
| `nagent-file-patch` | Merge segments, write patch, validate hashes |
| `nagent-file-summarize` | Summarize inline or via split summaries |
| `nagent-gc` | **NEW**: Reclaim dead nagent artifacts after distilling their knowledge |

Plus `nagent-llm-text --description` is the example shown in the README.

**Manual Slop equivalent.** Manual Slop's 45 MCP tools are in a flat `if/elif` chain in `src/mcp_client.py:dispatch` (per the v1 review). Adding a tool requires:
1. Edit `dispatch()` to add the branch
2. Update the security allowlist in `_resolve_and_check` (if filesystem access)
3. Update the AI capability declaration in `get_tool_schemas()`
4. Add tests

nagent's approach: drop a script in `bin/`, implement `exit_on_description`, done. The tool auto-appears.

**Verdict.** **GAP (Application).** nagent's pattern is genuinely better. The 45 tools in production make this a big refactor. The win is real (extensibility); the cost is real (rewrite the dispatch layer).

**SSDL shape.** `[I]` (single instruction, the description-fetch).

**Manual Slop next steps.**
- Subsumed by `mcp_architecture_refactor_20260606` (in plan) — the sub-MCP extraction is the natural scope for this pattern.

### 2.5 Pattern 5: You Did Not Build an Agent (Durable Work, Disposable Workers)

**nagent's claim.** Nothing in Part I has continuity, intent, or memory of its own. The process starts, transforms a file, and exits. The model is called fresh every turn with the whole conversation as input. "Agent" imports all three and delivers none — the word points you at the worker when everything that matters is in the artifacts.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| Durable state under `~/.nagent/` | `bin/nagent` (paths setup) | Conversations, file-index, saved-conversations index, knowledge, splits |
| The reframing table | `bin/nagent:730-731` | "The conversation persists across invocations, and the user may edit it between runs. The current file is the source of truth." |
| The hidden-state table | The README §"You Did Not Build an Agent" | 4 hidden states → 4 explicit artifacts |

**The reframing:**

| Hidden state | Explicit artifact |
|---|---|
| Prompt state in a running process | Conversation files under the nagent root |
| Private tool traces | Request tags and result wrappers appended as text |
| In-memory scratch state | Temp files, split segments, indexes, and patches |
| Framework-managed memory | User-editable files |

**Manual Slop equivalent.** Manual Slop has two parallel systems:

1. **MMA workers are real subprocesses** (`src/multi_agent_conductor.py:_spawn_worker` runs `mma_exec.py` via `subprocess.Popen`). Each Tier 3 worker is a fresh Python process with **Context Amnesia** (`ai_client.reset_session()` at the start of `run_worker_lifecycle`). The subprocess is the disposable worker; the artifacts (track state, ticket results) are the system.

2. **The Application AI is *not* a disposable worker.** `src/gui_2.py:App` is a long-lived Qt/ImGui process. The user types a prompt, hits Enter, gets a response, *keeps the process running for hours*. The `app_state` dataclass is the long-lived worker. This is intentional for the Application domain.

**Verdict.** **PARTIAL.** nagent's pattern lives in the Meta-Tooling + MMA, but the Application deliberately has long-lived workers. The two coexist because they serve different needs.

**SSDL shape.** (philosophical — not codifiable)

**Manual Slop next steps.**
- Candidate 1 (`SubConversationRunner` for 1:1 discussions) would extend the disposable-worker pattern to 1:1 chats. User-flagged as a want.

### 2.6 Pattern 6: Conversations Are Editable State

**nagent's claim.** The conversation file is not chat history. It is working state, and it belongs to you. Tool transcript. Correction channel. Continuation point. Mutable artifact. Memory goes stale; therefore editing history is maintenance, not corruption. **"The conversation does not own its memory. The user does."**

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `--save-conversation NAME` | `bin/nagent:2147-2156` | Copies the conversation, records it with an LLM-generated summary, indexes the saved entry. **If the summary fails, the save still completes** with a `(summary unavailable)` marker |
| `--load-conversation NAME` | `bin/nagent` (CLI) | Loads a named conversation into place |
| `--branch-conversation NAME` | `bin/nagent:2157-2170` | Archives the current file and copies a named conversation into place |
| `--summarize` | `bin/nagent` (CLI) | Prints an LLM summary of the loaded conversation |
| `--edit-conversation "prompt"` | `bin/nagent` (CLI) | Archives the conversation, runs a file-edit session against the archive with the prompt, loads the result |
| `--compact` | `bin/nagent:1975-2019` (NEW) | `--edit-conversation` driven by the user-editable `prompts/compact-conversation.md`; rewrites in place |
| Implicit maintenance | The conversation is just a file | `cat`, `vim`, `git diff`, `cp` — no special tooling needed |

**The session-vs-artifact-memory reframing:**

| Session memory | Artifact memory |
|---|---|
| Belongs to a running session | Belongs to a file on disk |
| Often opaque | Openable and diffable |
| Dies with the process | Survives worker replacement |
| Optimized for chat UX | Optimized for preserved work |

**Manual Slop equivalent.** Manual Slop's discussion editing lives at **three nested layers** (per the v1 review's per-entry operation matrix, A1-A7 per-entry, B1-B11 discussion-level, C1-C5 UISnapshot). The per-entry operations (the most important layer):

| # | Operation | Source code | What it does |
|---|---|---|---|
| A1 | Edit content in place | `gui_2.py:3841` | The entry's `content` field is editable multi-line text |
| A2 | Toggle read/edit mode | `gui_2.py:3799` | Read mode = Markdown render; Edit mode = multi-line text input |
| A3 | Toggle collapsed/expanded | `gui_2.py:3789` | Collapsed = 60-char preview; Expanded = full content |
| A4 | Change role | `gui_2.py:3793-3796` | Role is user-editable (User, AI, Tool, Context, etc.) |
| A5 | Insert entry before this one | `gui_2.py:3813` | `disc_entries.insert(index, ...)` |
| A6 | Delete this entry | `gui_2.py:3815-3816` | `disc_entries.remove(entry)` with membership-check |
| A7 | Branch at this entry | `gui_2.py:3821` | `branch_discussion(index)` creates a new Take |

**Verdict.** **PARITY (DIFFERENT FOCUS).** Both systems support comprehensive editing of the conversation-as-data. The difference is *what counts as "the conversation"*:
- nagent's "conversation" = the raw transcript text file (the bytes the LLM produced)
- Manual Slop's "conversation" = a typed entry list with role + content + metadata + optional thinking segments

Manual Slop's editing is *more granular and more pervasive* (per-entry content edit, per-entry insert/delete, per-entry role-change, per-entry branch, with undo/redo). nagent's editing is *deeper at the raw-transcript layer* (edit the actual AI response text before it's been abstracted into a typed entry). Both are real; both are deliberate.

**SSDL shape.** `[I]` (the operations are single-instruction).

**Manual Slop next steps.**
- Candidate 13 (Conversation compaction) would add a 4th maintenance verb (after Save/Load/Branch/Edit, the new Compact).

### 2.7 Pattern 7: Repository History as Data

**nagent's claim.** A repo is not only the current tree. History is data too. Repositories contain historical knowledge. Therefore transform git history into editing context. Not vague "retrieval" — explicit transformation of historical artifacts into working input.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `file_edit_history_and_summary_block(file_edit_path, ...)` | `bin/nagent` (in the file-edit path) | The orchestrator: gathers git history, computes co-edits, summarizes new commits |
| `git_file_history(repo_root, rel_path)` | `bin/nagent` (or `bin/helpers/nagent_file_edit_lib.py`) | `git log --follow --max-count=50` per file |
| `summarize_new_file_commits(...)` | `bin/nagent` | LLM call to one-line-summarize new commits; reuses cached summaries from prior initial context |
| `coedited_file_rows(repo_root, rel_path, commits)` | `bin/nagent` | Counts files in the same commits; labels high/medium/low co-edit rate |
| `format_file_history(...)` | `bin/nagent` | Produces a `{file-history}` block with editors, step-by-step, co-edited files, summarized commits |
| `run_file_summary()` | `bin/nagent` | Current-content summary |

**The output format** (`{file-history}` block, from the README):

```
{file-history}
File: src/foo.py
Individuals who edited this file:
  - Alice: 3 commits
Step-by-step history:
  - 2026-05-01 abc123 Alice: Adds validation.
{/file-history}
```

**Manual Slop equivalent.** Manual Slop's `_reread_file_items` (in `ai_client.py`) does mtime-based *current* content re-reading with diff injection as `[SYSTEM: FILES UPDATED]`. It does *not* do git history injection. The closest things Manual Slop has:
- **Git commit-linked discussion tracking** in the GUI: each discussion has an "Update Commit" button that stamps `git rev-parse HEAD` (per `docs/guide_gui_2.md` §"Discussions Sub-Menu")
- **`src/dag_engine.py`** tracks ticket-to-git-commit relationships, but for *MMA* workers, not for the AI's context

**Verdict.** **PARTIAL.** Manual Slop has current-content diff injection (the easy half) but lacks historical-context injection (the harder half). nagent's `summarize_new_file_commits` would be useful for "explain this file" questions where the LLM is meeting the file fresh.

**SSDL shape.** `[I]` (the history fetch + summary).

**Manual Slop next steps.**
- Candidate 6 (`src/git_history.py` mirroring nagent's `file_edit_history_and_summary_block`) — MEDIUM priority.

### 2.8 Pattern 8: Harvest Knowledge, Reclaim Space (THE NEW BIG ONE)

**nagent's claim.** Dead conversations accumulate, and deleting them loses what was learned. Therefore: distill, then delete — and feed the distillate back in. This is the strongest version of the "files create opportunities" argument. Session state that other tools discard becomes compounding, user-editable knowledge.

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `nagent-gc` | `bin/nagent-gc:1-150` | The CLI front-end: classify, estimate cost, harvest, reclaim |
| `run_gc(root, ...)` | `bin/helpers/nagent_gc_lib.py:330+` | The library: dry-run or apply; iterates harvest candidates; LLM-distills; appends to category files; regenerates digest; reclaims |
| `scan_root(root)` | `bin/helpers/nagent_gc_lib.py:80+` | Classifies every artifact: `live` / `user-kept` / `prune` / `harvest` / `keep` |
| `harvest_conversation(path, ...)` | `bin/helpers/nagent_gc_lib.py:235+` | For files >64KB, summarize first; otherwise use the full text; up to `HARVEST_MAX_ATTEMPTS=2` retries on parse failure |
| `merge_harvest(root, name, harvested, date)` | `bin/helpers/nagent_gc_lib.py:245+` | Appends harvested items to category files with `[from: conversation, date]` provenance |
| `regenerate_digest(root, max_bytes=4096)` | `bin/helpers/nagent_gc_lib.py:380+` | Rebuilds `digest.md` from category files; sections in fixed order: Open tasks, Open questions, Decisions, Facts, Playbooks; newest first |
| `load_ledger(root)` / `save_ledger(root, ledger)` | `bin/helpers/nagent_gc_lib.py:115-130` | sha256-of-content gate; "already harvested" path reclaims without re-distilling |
| `parse_harvest_json(text)` | `bin/helpers/nagent_gc_lib.py:180+` | Strict JSON parser with code-fence tolerance; validates 7 categories |

**The category schema** (from `prompts/harvest-conversation.md`):

| Category | Type | Description |
|---|---|---|
| `facts` | `[{statement, detail}]` | Durable statements about systems, repos, tools, environments, constraints — learned, not assumed |
| `decisions` | `[{statement, detail}]` | Choices that were made, with the why in `detail` |
| `tasks_done` | `[{statement, detail}]` | Concrete work completed in this conversation |
| `tasks_open` | `[{statement, detail}]` | Work started, planned, or requested but not finished |
| `questions` | `[{statement, detail}]` | Questions raised and never answered |
| `playbooks` | `[{name, steps}]` | Command sequences or processes that worked and are reusable |
| `files` | `[{path, note}]` | A note tied to one specific file path |

**The harvest prompt** (from `prompts/harvest-conversation.md`): "You are given one nagent conversation (or a summary of one). Extract only knowledge that stays useful after this conversation is deleted. Return only JSON in exactly this form (no prose, no markdown fence)." **"Empty arrays are valid and expected: most conversations contain nothing durable. Do not invent items to fill categories."**

**The constants** (from `bin/helpers/nagent_gc_lib.py`):

| Constant | Value | Meaning |
|---|---|---|
| `SUMMARIZE_THRESHOLD_BYTES` | 64 KB | Files > 64KB get summarized first; smaller files are sent in full |
| `MAX_HARVEST_SOURCE_BYTES` | 1 MB | Files > 1MB are kept (not harvested); budget guard |
| `DIGEST_MAX_BYTES` | 4 KB | The bounded digest size; truncates with "(truncated; see the category files for the rest)" |
| `HARVEST_MAX_ATTEMPTS` | 2 | Retry budget on parse failure |
| `ITEM_CATEGORIES` | `(facts, decisions, tasks_done, tasks_open, questions, playbooks, files)` | The 7 harvest categories |
| `CATEGORY_FILES` | `{facts, decisions, questions, playbooks} → (file, header)` | Maps category to file + initial header |
| `DIGEST_SECTIONS` | `(Open tasks, Open questions, Decisions, Facts, Playbooks)` | Digest section order |

**The CLI surface:**

| Flag | Effect |
|---|---|
| `--root PATH` | nagent root directory (default `~/.nagent`) |
| `--apply` | Actually harvest and delete; without this flag, dry-run only |
| `--no-harvest` | Delete dead conversations without the LLM harvest pass |
| `--max-harvest-bytes N` | Cap the conversation bytes sent to the LLM this run; the rest is deferred |
| `--json` | Print the full report as JSON instead of plain text |
| `--provider`, `--model`, `--config` | LLM arguments (forwarded to `nagent_llm.add_llm_arguments`) |

**The classification** (from `scan_root`):

| Class | Trigger | Action |
|---|---|---|
| `live` | `file-index-*`, `index-saved-conversations-*`, per-file conversations whose target still exists, `latest-*` active conversations | KEEP (no action) |
| `user-kept` | Path is in the saved-conversations index | KEEP (user marked it for retention) |
| `harvest` | Per-file conversations whose target is gone; archived conversations (name ends with UUID); delegated sub-conversations (name starts with UUID) | LLM-DISTILL → append to category files → reclaim |
| `prune` | Split directories with no `index.json`; split directories whose source is gone; split directories whose source hash doesn't match | DELETE |
| `keep` | Anything unclassified (default safe) | KEEP (no action) |

**The digest ordering** (from `regenerate_digest`): the categories are iterated in `(Open tasks, Open questions, Decisions, Facts, Playbooks)` order. Within each section, bullets are *reversed* (because the category files are append-only, so reversing gives newest-first). The header reads: `# Knowledge digest (regenerated by nagent-gc; edit the category files, not this file)`. If sections are empty, the digest is *deleted* (the "delete to turn off" pattern).

**The per-file knowledge notes branch** (in `merge_harvest` "files" category):

```
[Q:row.path is not None?]
   ├─ yes ──► [Q:target.is_file()?]
   │             ├─ yes ──► [I:file_id = file_id_for_path(target)]
   │             │              [I:append to knowledge/files/{file_id}.md]
   │             │
   │             └─ no  ──► [I:fall back: append to facts.md as "{path}: {note}"]
   │
   └─ no  ──► [T:skip]
```

**Manual Slop equivalent.** Manual Slop has 4 memory dimensions; the "knowledge" one is absent. The closest existing pattern is **RAG** (`src/rag_engine.py:1-384`, ChromaDB-backed), but RAG is:
- **Fuzzy** (vector similarity)
- **Opaque** (the vector store is not user-editable)
- **Not auditable** (no provenance from a specific conversation)
- **Not durable** across embedding-provider switches (the dim-mismatch fix at `16412ad5` shows this is a real issue)

**Verdict.** **GAP (Application).** The "knowledge memory" dimension is missing. Curation (FileItem + ContextPreset) and discussion (disc_entries + branching + UISnapshot) are present and strong. RAG is opt-in and is the wrong shape for "what did we learn from past sessions."

**SSDL shape.** `o->` (codecycle: scan → distill → append → digest → reclaim).

**Manual Slop next steps.**
- **Candidate 11: Knowledge memory (third dimension)** — HIGH priority. The single most important v2.3 finding. See §4 below for the deep-dive.

### 2.9 Pattern 9: Everything Else Files Buy You

**nagent's claim.** The mundane wins add up:
- `diff` two conversation states to see exactly what an editing pass changed.
- `--branch-conversation` before a risky direction; come back if it fails.
- Script maintenance: cron a `--compact`, grep your knowledge store.
- Audit exactly what the model saw — the conversation file *is* the request.
- Point the same conversation file at a different provider and replay.

None of these required a feature. They required the state to be files.

**Manual Slop equivalent.** Manual Slop's *philosophy* of "files are the system" is already present:
- `manual_slop.toml` is the project's source of truth
- `conductor/tracks/<id>/state.toml` is the track's state
- `personas.toml`, `tool_presets.toml`, `context_presets.toml` are all TOML
- The Hook API exposes this state via `POST /api/project` for external automation

What's *not* yet at that level: the AI's working state (the in-flight `disc_entries`, the provider history globals). Closing this gap is the theme of Candidates 3, 7, 10, and 11.

**Verdict.** **N/A** (lens, not pattern).

**SSDL shape.** (lens, not codifiable)

**Manual Slop next steps.**
- All the Candidate 11-16 work moves Manual Slop closer to "everything else files buy you" automatically.

### 2.10 Pattern 10: Data-Oriented Design (Named Principles)

**nagent's claim.** You have been using these principles since Part I. Here are their names:
- **The data is more important than the code operating on it.** The conversation file outlives every process that touches it.
- **Behavior is a transformation over explicit state.** The loop is append → transform → append.
- **Avoid hidden mutable state.** Retries, errors, and tool results are appended text, not control flow.
- **Separate durable artifacts from temporary execution.** Workers are disposable; artifacts are durable.
- **Optimize the shape, availability, and maintenance of the data.** Editable conversations, cached commit summaries, harvested digests.

**The system as one transformation** (verbatim from the README):

```
repository history + install + project + root context + conversation
+ artifact-local memory + artifact summary + historical coupling
+ harvested knowledge + user request
--> LLM transformation
--> updated artifacts
```

**The reframing table:**

| Object graphs | Data artifacts |
|---|---|
| Behavior distributed across services and objects | Behavior is transformation over files |
| State behind interfaces | State in an editor buffer |
| Runtime topology is central | Artifact shape is central |

**Manual Slop equivalent.** Manual Slop shares the data-oriented stance:
- `data_oriented_error_handling_20260606` track (Result[T] + ErrorInfo)
- `app_state` dataclass + `HistoryManager` + `UISnapshot` (data, not object graph)
- The `comms.log` JSON-L is the "behavior is append" pattern
- The harvest (if implemented) would join this list

**Verdict.** **PARITY** (philosophical). Manual Slop is data-oriented; nagent's formalization matches Manual Slop's actual practice.

**SSDL shape.** (philosophical, not codifiable)

**Manual Slop next steps.**
- The new canonical DOD file (`conductor/code_styleguides/data_oriented_design.md`) should adopt nagent's `context/data-oriented-design.md` (13,084 bytes, the canonical rules) as its foundation, with adaptations for Manual Slop's specific patterns (curation, discussion, MMA, etc.).

### 2.11 Pattern 11: Artifact Neighborhoods

**nagent's claim.** A file lives in a neighborhood of related artifacts. Files that change together in git history are hints: tests, headers, config, paired implementation. High co-edit rate means "look here maybe." Not "edit everything."

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `coedited_file_rows(repo_root, rel_path, commits)` | `bin/nagent` (or `bin/helpers/nagent_file_edit_lib.py`) | Counts files appearing in the same commits as the target; labels high/medium/low co-edit rates |
| `format_file_history(...)` | `bin/nagent` | Puts the table in file-edit context with guidance: inspect high co-edit files when the change may touch interfaces, tests, config, or paired code |
| Per-file knowledge notes | `bin/helpers/nagent_gc_lib.py` (NEW) | Harvested notes join the same neighborhood alongside the file history, current summary, and co-edited files |

**The example output:**

| file | commits together | historical co-edit rate |
|---|---|---|
| `src/foo_test.py` | 7 | high (70%) |
| `src/foo.h` | 5 | medium (50%) |

**The guidance text:** "Use these files as hints. Before editing, inspect high-likelihood co-edited files when the requested change may affect interfaces, tests, config, or paired code. Do not edit them unless the user request or evidence requires it." **"High co-edit files are candidates for inspection, not automatic edit targets."**

**Manual Slop equivalent.** None. Manual Slop has `py_get_hierarchy` (subclass scan) and `ts_c_*_get_*` AST tools, but **no tool that returns "files that historically co-edit with this file."** The closest is `derive_code_path` (call-graph trace), which is structural not historical.

**Verdict.** **GAP.** This is a real missing tool. The framing — "hints, not commands" — is exactly the right level. A small tool (`py_coedit_files(path) -> list[(path, count, likelihood)]`) would fill the gap.

**SSDL shape.** `[I]` (single instruction, the coedit fetch).

**Manual Slop next steps.**
- Candidate 8 (`py_coedited_files` + `ts_c_coedited_files` MCP tools) — LOW priority; bundle with Candidate 6 (git history).

### 2.12 Pattern 12: Managing Context and Large Files

**nagent's claim.** Context windows are a budget. Spend it explicitly. Large files exceed context windows. Therefore split them into explicit artifacts. Conversations grow too. Therefore compact them, bound the knowledge digest, and push noisy exploration into disposable sub-conversations.

**The data flow:**

```
large source file --> split index + segment files --> bounded edits
                  --> patch artifact --> updated source file
```

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `nagent-file-split` | `bin/nagent-file-split:1-170` | The CLI: `nagent-file-split <file> --output <dir> --split <type> [--summarize] [--refresh] [--target-bytes 32768]` |
| `nagent_file_split_lib.py` | `bin/helpers/nagent_file_split_lib.py:1-400` | The library: `EXTENSION_MAP` (12 languages: txt, md, cpp, py, xml, js, ts, json, yaml, go, rs, java), per-language `SCORE_BY_TYPE` (regex + line counts + brace/JSON/XML depth), `index.json` writer, `natural` mode, `--target-bytes` |
| `nagent-file-patch` | `bin/nagent-file-patch:1-80` | The CLI: `nagent-file-patch <index> [--patch PATH] [--dry-run] [--force]` |
| `nagent_file_patch_lib.py` | `bin/helpers/nagent_file_patch_lib.py:1-130` | `validate_index` (strict hash check), `merge_segments`, `make_unified_patch`, `apply_segment_patches` |
| `nagent-file-summarize` | `bin/nagent-file-summarize:1-100` | The CLI: `nagent-file-summarize <file> [--limit-word-count] [--output DIR] [--json]` |
| `nagent_file_summarize_lib.py` | `bin/helpers/nagent_file_summarize_lib.py:1-110` | `summarize_content` (per-segment LLM call, retries), `combined_summary_from_index`, cascades to `--split` for files > 64KB |
| 12 language splitters | `bin/helpers/nagent-file-split-{py,cpp,js,ts,json,yaml,md,xml,txt,go,rs,java}` | Each ~225B: thin wrappers that call `nagent_file_split_lib.py` with the right SCORE_BY_TYPE |

**The inline read threshold:** 64KB. Reads above this cascade to `nagent-file-split`.

**The target size:** 32KB. The default split size.

**The hash validation:** `source_sha256()` is computed at split time and stored in `index.json`. `validate_index` rejects if the source hash has changed (unless `--force`).

**The conversation-side budget tools:**

| Verb | What it does |
|---|---|
| `--compact` | Rewrites the conversation against editable guidance; behavior-preserving compression |
| `digest.md` | Bounded (4KB) before injection |
| `<nagent-conversation>` | Spawns a child nagent with isolated conversation file; parent keeps coordination, child keeps the noise |

**The delegations-as-context-management reframing:**

| Long-lived agent abstractions | Disposable workers |
|---|---|
| Identity is central | Output artifact is central |
| Shared context gets noisy | Child context is isolated |
| Parent absorbs all exploration | Parent gets a concise result |
| Delegation implies personality | Delegation is context management |

**The sub-conversation reuse pattern:**

```
Reuse a named worker:    conversation-file="name"  //  continues that conversation with its accumulated context
Resume saved work:       conversation-name="saved-name"  //  starts a child from a saved conversation
Author a worker's context:  write a briefing to /tmp, then spawn  conversation-file="/tmp/..."
Hand off when noisy:     distill goal/state/decisions into a fresh sub-conversation
```

**Manual Slop equivalent.** Manual Slop has all the *parts* of nagent's split/patch/summarize, but they live in different files and use different mechanisms:

| nagent | Manual Slop |
|---|---|
| `nagent-file-split` with per-language `SCORE_BY_TYPE` (regex + line counts + brace/JSON/XML depth) | `aggregate.py:build_file_items` + `py_get_skeleton` (tree-sitter) + `ts_c_*_get_skeleton` (tree-sitter) + `outline_tool.py` |
| `index.json` with `source_path, sourcesha256, segments[]` | No explicit `index.json`. The "split" is implicit in `_reread_file_items` (mtime-based) and the `py_get_skeleton` tool returns the structural view on demand |
| `nagent-file-patch` with strict `validate_index` (hash check) | `set_file_slice` / `edit_file` with `result of file.read_text()` pre-write validation. No hash-based pre-validation |
| `nagent-file-summarize` with per-segment LLM call + retry | `run_subagent_summarization` (in-process, no retry budget) |
| Combined `combined_summary_from_index` | No equivalent; `aggregate.build_markdown_no_history` builds a single markdown per call |
| `nagent-file-summarize` cascades to `--split` for > 64KB | `RAGEngine._chunk_code` cascades to chunking for Python (mtime-based, ChromaDB) |

**Crucial difference: Manual Slop uses tree-sitter, nagent does not.** nagent's per-language scoring functions are *all regex-based* (`cpp_score` looks for closing braces at depth 0; `py_score` looks for blank lines followed by `def`/`class` keywords; no AST parsing). Manual Slop's `py_get_skeleton` and `ts_c_*_get_skeleton` use the tree-sitter library for actual AST traversal.

**Verdict.** **PARITY (DIFFERENT MECHANISM).** Both have the "split / patch / summarize as explicit data artifacts" insight. nagent uses subprocesses + per-language scoring + hash validation. Manual Slop uses tree-sitter + in-process calls + mtime validation. The key safety property — *"the patch operation validates the source hasn't changed"* — is done by nagent via SHA-256; Manual Slop does it implicitly by re-reading the file and string-matching.

**SSDL shape.** `[I]` (the split/patch/summarize operations).

**Manual Slop next steps.**
- Candidate 9 (Explicit `split_lib.py` / `patch_lib.py` mirroring nagent's design) — DEFERRED until a very-large-file scenario actually surfaces. The current tree-sitter + per-file slices + RAG aggregation handles 99% of real workloads.

### 2.13 Pattern 13: Per-File Write Conversations

**nagent's claim.** Work recurs around individual files. Give each file its own persistent conversation — memory and write authority attached to the artifact, not to a session.

**The data flow:**

```
main conversation
  +-- file A memory
  +-- file B memory
  +-- file C memory
```

**nagent's implementation.**

| Component | Where | What it does |
|---|---|---|
| `bin/nagent-file-edit` | `bin/nagent-file-edit:1-120` | The CLI: `nagent-file-edit --file <path> "add error handling"` |
| `bin/nagent --file-edit` | `bin/nagent` (in the main loop) | The mode: `nagent --file-edit <path> --invocation user` |
| `file_id_for_path(path) -> "{st_dev}:{st_ino}"` | `bin/helpers/nagent_file_edit_lib.py` | Stable file identity across renames (inode is preserved) |
| `file_index_path(root, pid) -> conversations/file-index-{pid}.json` | `bin/helpers/nagent_file_edit_lib.py` | The per-pid registry of `{file_id: {path, conversation}}` |
| `resolve_file_edit_conversation(root, pid, file_path)` | `bin/helpers/nagent_file_edit_lib.py` | Gets or creates a per-file conversation |
| `validate_write_path(path, file_edit_path, ...)` | `bin/nagent` (in the write boundary) | In per-file-edit mode, the path must be the target file (by path or file id), or one of its split segments |

**The write boundary** (table):

| Mode | Structured write boundary |
|---|---|
| Main conversation | `/tmp`, `/var/tmp`, or `$TMPDIR` only |
| Per-file edit | Target file (by path or file id), or split segments for that source |

**The example file index:**

```json
{
  "by_file_id": {
    "2050:123456": {
      "file_id": "2050:123456",
      "path": "/repo/src/foo.py",
      "conversation": "foo-0c2f..."
    }
  }
}
```

**The CLI surface:**

```bash
nagent-file-edit --file src/foo.py "add error handling"
nagent-file-edit --file src/foo.py --clear
nagent --list-file-edits
```

**Manual Slop equivalent.** Manual Slop has the *curation* dimension of per-file memory (`FileItem` with `path + view_mode + ast_mask + custom_slices`) and the *discussion* dimension of per-Take memory (`disc_entries` with branching). What's missing is:
- The *file-id* concept (key by inode for rename-survival; Manual Slop keys by path)
- The *per-file knowledge notes* dimension (`knowledge/files/{file_id}.md`)
- A *per-file conversation* (a sub-conversation that lives in `knowledge/files/{file_id}.md` and gets injected as initial context when the file is in scope)

**Verdict.** **PARTIAL.** The per-file conversation is missing; the per-file *curation* memory is present. The "key by inode" pattern is also missing.

**SSDL shape.** `[I]` (the per-file memory fetch + the write boundary).

**Manual Slop next steps.**
- Candidate 7 (per-file conversation log) — LOW priority.
- Candidate 11.1 (per-file knowledge notes; bundle with Candidate 11) — the more useful of the two.
- Add `file_id: str` to `FileItem` (keyed by `st_dev:st_ino`) — additive; non-breaking.

### 2.14 Pattern 14: Own the Inputs (vs Framework Abstractions)

**nagent's claim.** Use a framework when it buys something concrete. The question to ask first is who owns the data. nagent uses plain files, Python, subprocesses, and structured text. The interesting part is artifact management and explicit data flow, not tool calling. The point is not "frameworks bad." The point is that the inputs to the system — prompts, conversations, tool results, summaries, indexes, patches, harvested knowledge — should not be trapped inside an opaque layer that hides, rewrites, stores, or modifies them beyond the transformations LLM providers already perform.

**The framework-vs-nagent table:**

| Framework-style system | nagent |
|---|---|
| hidden or managed state | explicit files |
| session memory | artifact memory |
| object/service graph | data artifacts |
| central tool registry | executable descriptions |
| long-lived agent abstraction | disposable workers |
| opaque orchestration | visible transformations |

**The reframing table (extended):**

| Common term | nagent framing |
|---|---|
| memory | editable artifact |
| retrieval | preserved work / historical context |
| agent | temporary transformation function |
| context | explicit input data |
| tool call | structured tag with typed attributes |
| shell | tmpdir-bounded subprocess with append-only transcript |

**Manual Slop equivalent.** Manual Slop's stance is the same. `docs/guide_meta_boundary.md` is the load-bearing doc:
- The Application has a long-lived state (`app_state` dataclass) and a GUI.
- The Meta-Tooling (Gemini CLI, OpenCode, Claude Code) has its own state, and the bridge between them is the API hooks on `127.0.0.1:8999`.

The "own the inputs" principle means: Manual Slop's TOML files (`manual_slop.toml`, `personas.toml`, `tool_presets.toml`, `context_presets.toml`) are all human-editable, diff-able, version-control-able. The Hook API exposes them via `GET /api/project`. The Application's `comms.log` is JSON-L, not opaque binary.

**Verdict.** **N/A** (lens, not pattern).

**SSDL shape.** (lens, not codifiable)

**Manual Slop next steps.**
- The new canonical DOD file (`conductor/code_styleguides/data_oriented_design.md`) should adopt nagent's "own the inputs" principle as one of its named principles.
---


## 3. The new big additions (2026-06-08 → 2026-06-12) — deep-dive

The 8 new commits between 2026-06-08 and 2026-06-12 add 5 first-class subsystems, 1 new sub-pattern, 5 supporting changes, and 1 architectural file. Each is a candidate-track-worthy finding.

### 3.1 Knowledge harvest (`nagent-gc`)

See §2.8 above for the pattern itself. This section is the *subsystem architecture* in detail.

**The CLI surface** (from `bin/nagent-gc:75-130`):

| Flag | Type | Default | Effect |
|---|---|---|---|
| `--root PATH` | str | `~/.nagent` | The nagent root |
| `--apply` | bool | off (dry-run) | Mutate: harvest + reclaim; without, just classify + estimate |
| `--no-harvest` | bool | off | Reclaim only, skip the LLM pass |
| `--max-harvest-bytes N` | int | unlimited | Cap the conversation bytes sent to the LLM this run; the rest is deferred |
| `--json` | bool | off | Print the full report as JSON instead of plain text |
| `--provider P` | enum | from config | LLM provider |
| `--model M` | str | from config or default | LLM model |
| `--config PATH` | path | `~/.nagent/config.json` | Config file path |

**The full classification table** (from `scan_root`):

| Artifact | Classification | Reason |
|---|---|---|
| `conversations/file-index-{pid}.json` | `live` | Index file |
| `conversations/index-saved-conversations-{pid}.json` | `live` | Index file |
| Conversation in saved-conversations index | `user-kept` | Saved conversation |
| Per-file conversation whose target still exists | `live` | Per-file conversation for `<target>` |
| Per-file conversation whose target is gone | `harvest` | Per-file conversation; target gone: `<target>` |
| Conversation whose name ends with a UUID | `harvest` | Archived conversation |
| Conversation whose name starts with a UUID | `harvest` | Delegated sub-conversation |
| Conversation starting with `latest-` | `live` | Active conversation |
| Conversation of any other name | `keep` | Unclassified; kept |
| Split directory with no `index.json` | `keep` | No readable index.json; kept |
| Split directory with `index.json` whose `source_path` is gone | `prune` | Split source gone |
| Split directory with `index.json` whose `source_sha256` doesn't match | `prune` | Split stale (source changed) |
| Split directory with `index.json` whose source is current | `live` | Split current |

**The harvest codepath** (SSDL):

```
[Q:root exists?]
   │
   ├── no ──► [T:exit 1]
   │
   ▼
[I:scan_root(root)]    (artifacts list with klass)
   │
   ▼
[Q:harvest candidates exist?]
   │
   ├── no ──► [T:exit 0 (clean root)]
   │
   ▼
[Q:apply?]
   │
   ├── no ──► [I:print dry-run report]   [T:exit 0]
   │
   ▼
[Q:harvest?]
   │
   ├── no ──► [I:reclaim without LLM pass]   [T:exit 0]
   │
   ▼
[Q:provider, model resolved?]
   │
   ├── no ──► [T:exit 1]
   │
   ▼
[I:run_gc(root, apply=True, harvest=True, ...)]
   │
   ▼
[loop: each harvest candidate]
   │
   ├──► [I:sha256_of(artifact.path)]
   │
   ├─ [Q:ledger has this hash with status=harvested?]
   │  │
   │  ├── yes ──► [I:reclaim without re-distill]   [S:ledger entry updated]
   │  │
   │  └─ no  ──►
   │            │
   │            [Q:artifact.size > 1MB?]
   │            │
   │            ├── yes ──► [S:ledger entry "too-large"]   [S:keep]
   │            │
   │            └─ no  ──►
   │                      │
   │                      [Q:over --max-harvest-bytes?]
   │                      │
   │                      ├── yes ──► [S:ledger entry "deferred"]
   │                      │
   │                      └─ no  ──►
   │                                │
   │                                [try]
   │                                  [Q:size > 64KB?]
   │                                    ├── yes ──► [I:summarize via nagent-file-summarize subprocess]
   │                                    └── no  ──► [I:read full text]
   │                                  [I:build_harvest_prompt(template, name, content, retry)]
   │                                  [I:LLM call via generate(prompt, provider, model)]
   │                                  [I:parse_harvest_json(response)]
   │                                [catch]
   │                                  [I:retry with "Return only JSON" suffix]   (up to 2 attempts)
   │                                  [S:ledger entry "harvest-failed"]
   │                                [success]
   │                                  [I:merge_harvest(root, name, harvested, date)]   (append to category files)
   │                                  [S:ledger entry "harvested" with items count]
   │                                  [I:reclaim (unlink)]
   │
[I:prune stale split directories]   (no LLM pass)
   │
[I:prune file-index entries whose target is gone]
   │
[I:prune saved-conversations entries whose path is gone]
   │
[I:save_ledger]
   │
[I:regenerate_digest]
   │
[T:return report]
```

**The merge_harvest function** (from `bin/helpers/nagent_gc_lib.py:245+`):

```python
def merge_harvest(root, conversation_name, harvested, date) -> dict[str, int]:
    knowledge = knowledge_dir(root)
    provenance = f"[from: {conversation_name}, {date}]"
    counts = {category: 0 for category in ITEM_CATEGORIES}

    # For each category in CATEGORY_FILES (facts, decisions, questions, playbooks):
    #   - collect bullets from harvested[category]
    #   - each bullet = "{text} {provenance}"
    #   - append to knowledge/{file_name} with the header

    # For tasks_open and tasks_done (special - the tasks.md file has ## Open and ## Done sections):
    #   - open_bullets go before "## Done"
    #   - done_bullets go after "## Done"

    # For the "files" category (special - per-file knowledge notes):
    #   - if the path resolves to an existing file: append to knowledge/files/{file_id}.md
    #   - if not: fall back to facts.md as "{path}: {note} {provenance}"

    return counts
```

**The regenerate_digest function** (from `bin/helpers/nagent_gc_lib.py:380+`):

```python
def regenerate_digest(root, max_bytes=DIGEST_MAX_BYTES) -> Path | None:
    knowledge = knowledge_dir(root)
    open_tasks, _done = _read_task_bullets(knowledge / "tasks.md")

    sections = []
    for title, category in DIGEST_SECTIONS:    # (Open tasks, Open questions, Decisions, Facts, Playbooks)
        if category == "tasks_open":
            bullets = open_tasks
        else:
            file_name, _header = CATEGORY_FILES[category]
            bullets = _read_bullets(knowledge / file_name)
        if bullets:
            # Newest first: category files are append-only
            sections.append((title, list(reversed(bullets))))

    target = digest_path(root)
    if not sections:
        if target.is_file():
            target.unlink()    # delete to turn off
        return None

    header = "# Knowledge digest\n(regenerated by nagent-gc; edit the category files, not this file)\n"
    parts = [header]
    used = len(header.encode("utf-8"))
    truncated = False
    for title, bullets in sections:
        section_header = f"\n## {title}\n"
        used += len(section_header.encode("utf-8"))
        if used > max_bytes:
            truncated = True
            break
        parts.append(section_header)
        for bullet in bullets:
            line = f"{bullet}\n"
            used += len(line.encode("utf-8"))
            if used > max_bytes:
                truncated = True
                break
            parts.append(line)
        if truncated:
            break
    if truncated:
        parts.append("\n(truncated; see the category files for the rest)\n")
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text("".join(parts), encoding="utf-8")
    return target
```

**The injection point** (in `bin/nagent:677-685`):

```python
knowledge_digest = load_context_file(digest_path(root))
knowledge_block = ""
if knowledge_digest:
    knowledge_block = (
        "\nThe {knowledge} block below is distilled from previous conversations "
        "by nagent-gc; items carry provenance. Treat them as hints and verify "
        "before relying on them.\n"
        f"{{knowledge}}\n{knowledge_digest}\n{{/knowledge}}\n"
    )
```

The digest is in the *stable* position (before the `Instance:` volatile block). Cache-friendly per §5.

**The ledger gate** (the deletion-lossless invariant):

```python
# In run_gc, the loop body for each harvest candidate:
sha = sha256_of(path)        # sha256-of-content, not sha256-of-path
existing = entries.get(sha)
if existing is not None and existing.get("status") == "harvested":
    # Distillation already proven for this exact content: just reclaim.
    reclaimed += artifact.size_bytes
    path.unlink()
    existing["deleted"] = True
    emit(f"reclaimed (already harvested): {label}")
    continue
```

This means: if two conversations have the same content, the second is reclaimed without paying the LLM cost again. The dedup is content-based, not name-based.

**The "delete to turn off" pattern:**

```python
# In regenerate_digest:
if not sections:
    if target.is_file():
        target.unlink()    # delete to turn off
    return None

# In build_initial_context:
if knowledge_digest:    # only if file exists and has content
    knowledge_block = ...
```

`rm ~/.nagent/knowledge/digest.md` → no injection. The file is the feature flag. The user can re-enable by running `nagent-gc --apply`.

**Manual Slop next steps.** **Candidate 11: Knowledge memory (third dimension)** — HIGH priority. See §4 below for the deep-dive of how this maps to Manual Slop.

### 3.2 Stable-to-volatile cache ordering (the `--cache-prefix-chars` flow)

**nagent's claim.** Context windows are a budget, but cache hit rate is the multiplier. The initial context's *ordering* determines cache effectiveness: stable prefix + volatile suffix means providers that cache on block boundaries (Anthropic) can reuse the shared context across conversations of the same mode.

**The block order in `build_initial_context`** (from `bin/nagent:691-745`):

| Layer | Position | Stable across turns? | SSDL |
|---|---|---|---|
| `NAGENT_PREAMBLE` | first | yes | `[I]` |
| `role_instructions` | 2 | yes | `[I]` |
| Protocol rules + tag list | 3 | yes | `[I]` |
| Context management rules | 4 | yes | `[I]` |
| Conversations-are-data rules | 5 | yes | `[I]` |
| `file_edit_rules` | 6 | yes | `[I]` |
| `tools_block` | 7 | yes | `[I]` |
| `install_context_block` | 8 | yes | `[I]` |
| `project_context_block` | 9 | yes | `[I]` |
| `root_context_block` | 10 | yes | `[I]` |
| `knowledge_block` | 11 | yes (within a gc cycle) | `[I]` |
| `file_edit_detail_block` | 12 | yes (for the same file_edit) | `[I]` |
| `Instance:` | 13 | **NO (volatile)** | `───` data |
| `Environment:` | 14 | **NO (volatile)** | `───` data |

The block order comment (line 687-690): *"Block order is stable-to-volatile on purpose: the protocol, rules, tools, and context blocks are byte-identical across conversations of the same mode, so request prefixes stay shareable; instance facts and environment go last."*

**The cache boundary computation** (from `bin/nagent:970-987`):

```python
def conversation_cache_boundaries(text: str) -> list[int]:
    """Character offsets ending the stable prefixes of a conversation file.

    Two boundaries when the file starts with an initial-context block: the
    start of the volatile Instance section (shared byte-for-byte across
    conversations of the same mode and root) and the end of the context block
    (stable across every turn of this conversation). Providers that cache on
    block boundaries reuse those prefixes instead of re-reading them."""
    span = find_block_span(text, INITIAL_CONTEXT_BLOCK)
    if span is None or span[0] != 0:
        return []
    boundaries = []
    volatile_at = text.find("\nInstance:", span[0], span[1])
    if volatile_at > 0:
        boundaries.append(volatile_at)
    if span[1] < len(text):
        boundaries.append(span[1])
    return boundaries
```

**The CLI flow** (from `bin/nagent:1013-1014` in `call_llm`):

```python
for boundary in conversation_cache_boundaries(conversation_text):
    command.extend(["--cache-prefix-chars", str(boundary)])
```

**The Anthropic-specific injection** (from `bin/helpers/nagent_llm.py:cache_prefix_blocks`):

```python
def cache_prefix_blocks(message, cache_boundaries):
    """Split a message into content blocks at the given character offsets, marking
    each prefix block with cache_control so providers that cache on block boundaries
    can reuse stable prefixes.

    Returns the plain string when no valid boundary exists.
    At most 3 prefix blocks (provider limit is 4 breakpoints per request)."""
    if not cache_boundaries:
        return message
    points = sorted({b for b in cache_boundaries if 0 < b < len(message)})[:3]
    if not points:
        return message
    blocks = []
    start = 0
    for point in points:
        blocks.append({
            "type": "text",
            "text": message[start:point],
            "cache_control": {"type": "ephemeral"},
        })
        start = point
    blocks.append({"type": "text", "text": message[start:]})
    return blocks
```

**The codepath** (SSDL):

```
[Q:conversation_text]    (read from disk)
   │
   ▼
[I:find_block_span(<initial_context>)]
   │
   ▼
[Q:offset of \nInstance:?]
   │
   ├──► [I:boundaries.append(offset)]
   │
   ▼
[Q:end of <initial_context> < len(text)?]
   │
   ├──► [I:boundaries.append(end)]
   │
   ▼
[I:nagent-llm-text --file conversation --cache-prefix-chars N1 --cache-prefix-chars N2 --json]
   │
   ▼
[I:anthropic.messages.create(content=prefix_blocks)]
   │
   ▼
[I:_result_with_usage(text, usage, input_text)]
   │
[T:return (text, input_tokens, output_tokens)]
```

**The Anthropic usage accounting** (from `bin/helpers/nagent_llm.py:_result_with_usage`):

```python
def _result_with_usage(text, usage, input_text=None):
    input_tokens = _usage_value(usage, "input_tokens", "prompt_tokens", "prompt_token_count")
    # Anthropic reports cached prompt tokens separately; fold them back in so
    # input_tokens stays "tokens sent" across providers. Other providers lack
    # these fields and contribute zero.
    input_tokens += _usage_value(usage, "cache_read_input_tokens")
    input_tokens += _usage_value(usage, "cache_creation_input_tokens")
    output_tokens = _usage_value(usage, "output_tokens", "completion_tokens", "candidates_token_count", "output_token_count")
    total_tokens = _usage_value(usage, "total_tokens", "total_token_count")
    if output_tokens == 0 and total_tokens and input_tokens:
        output_tokens = max(0, total_tokens - input_tokens)
    if input_tokens == 0 and input_text is not None:
        input_tokens = estimate_token_count(input_text)
    if output_tokens == 0:
        output_tokens = estimate_token_count(text)
    return LlmResult(text=text, input_tokens=input_tokens, output_tokens=output_tokens)
```

The fold-back is the load-bearing detail: `cache_read_input_tokens + cache_creation_input_tokens` are added to `input_tokens` so "input_tokens" stays "tokens sent" across providers. Caching is *invisible* in the accounting; the user sees one number.

**Manual Slop equivalent.** Manual Slop has the mechanism (per `src/ai_client.py:2883` summary):
- `_add_history_cache_breakpoint` (inserts a cache breakpoint in the conversation history at a chosen position)
- `_send_anthropic` uses `cache_control` blocks (per the function name)
- `_GEMINI_CACHE_TTL` is a constant (Gemini explicit caching)
- `get_gemini_cache_stats` is exported (per the summary)
- `_ANTHROPIC_CHUNK_SIZE, _ANTHROPIC_MAX_PROMPT_TOKENS` are constants (chunking for size management)

What's NOT explicit:
- Stable-to-volatile *ordering discipline* in `aggregate.py:run` (which builds the initial context)
- Cache TTL exposure in the GUI
- Per-discussion caching decision

**Verdict.** **PARTIAL.** Mechanism present; ordering not enforced; TTL not exposed.

**SSDL shape.** `->M->` (merge at volatile boundary).

**Manual Slop next steps.**
- **Candidate 12a: Stable-to-volatile cache ordering** — refactor `src/ai_client.py:_get_combined_system_prompt` and the Anthropic call site to enforce the ordering. See §5 below.
- **Candidate 12b: Cache TTL GUI controls** — see §3.3.

### 3.3 Cache TTL GUI controls (the GUI exposure gap)

**The gap.** nagent's `--cache-prefix-chars` flow is *transparent* — the user doesn't see the cache boundaries. The cache TTL is determined by the provider (Anthropic ephemeral: 5 min default; Gemini explicit: 1 h default; OpenAI implicit: provider-managed). nagent does NOT expose cache TTL in the GUI; it's provider-managed.

**What Manual Slop needs.** The user has explicitly asked for "more explicit controls in the future for handling discussion caching and what not.. also expose how long the caches are available for (gemini has a limit for example)."

**The proposed GUI surface** (an Operations Hub sub-panel):

```
+------------------------------------------------------+
| Caching                                              |
+------------------------------------------------------+
| [Anthropic]   in:340 cache:80  hit:23%  ttl:4:32   |
| [Gemini]      in:120 cache:0   hit:0%   ttl:0:00   |
| [OpenAI]      in:560 cache:200 hit:35%  ttl:n/a    |
+------------------------------------------------------+
| Discussion "refactor auth"                           |
|   cached: yes (Anthropic)                            |
|   expires: 2026-06-12T15:32 (in 4:32)                |
|   [Invalidate cache] [Disable caching for this]      |
+------------------------------------------------------+
| Global settings                                      |
|   [X] Enable Anthropic ephemeral caching             |
|   [X] Enable Gemini explicit caching                 |
|   [ ] Allow >1h Gemini caches (charges may apply)    |
|   Anthropic default TTL: [5 min v]                   |
|   Gemini default TTL:    [60 min v]                  |
+------------------------------------------------------+
```

**The provider-specific defaults:**

| Provider | Cache type | Default TTL | Configurable? |
|---|---|---|---|
| Anthropic | ephemeral | 5 min (per-request) | yes (via prompt cache breakpoints) |
| Google (Gemini) | explicit | 1 h (default) | yes (via `ttl` field) |
| OpenAI | implicit (auto) | 5-10 min (provider-managed) | no (provider-managed) |
| Cursor | (provider-managed) | varies | no |
| claude-code (via Claude Agent SDK) | (provider-managed) | varies | no |

**Manual Slop current state.**
- `src/ai_client.py:_send_anthropic` uses `cache_control: {"type": "ephemeral"}` (per the function code, the constants `_ANTHROPIC_CHUNK_SIZE` and `_ANTHROPIC_MAX_PROMPT_TOKENS`)
- `src/ai_client.py:_send_gemini` has explicit caching (per `_GEMINI_CACHE_TTL` constant and `get_gemini_cache_stats` exported)
- `src/ai_client.py:_send_gemini_cli` is the headless CLI path (no caching in the same way)
- `src/ai_client.py:_send_minimax, _send_deepseek, _send_grok, _send_qwen, _send_llama` — various; no GUI exposure

**What's missing in the GUI.** Per the v1 review's note on the AI Settings panel: there's a `RAG` section (RAG enable/disable, source selection, embedding provider) and a "Caching" tab is mentioned in the planned Phase 8 UI Polish (but not yet built).

**Verdict.** **GAP (UX).** The mechanism is in place for Anthropic + Gemini; the GUI exposure is missing.

**SSDL shape.** `=>` (wide: per-provider control surface; the same code path branches into 5+ provider-specific config UIs).

**Manual Slop next steps.**
- **Candidate 12b: Cache TTL GUI controls** — MEDIUM priority. See §5 below for the deep-dive.

### 3.4 Conversation compaction (the `--compact` flow)

**nagent's claim.** Summarization loses detail. Compaction rewrites the conversation against user-editable guidance, *preserving* the relevant content. Different tool, different purpose.

**The codepath** (from `bin/nagent:1975-2019`):

```python
def compact_conversation(conversation_file, root, process_path, invocation, ...):
    try:
        compact_guidance = compact_prompt_path(root).read_text(encoding="utf-8").strip()
    except OSError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1

    prompt = (
        f"{compact_guidance}\n\n"
        "Compact this conversation now, following the guidance above. "
        "Rewrite it in place so it is substantially smaller while preserving "
        "future capability."
    )
    return edit_conversation(
        conversation_file, root, process_path, invocation,
        conversation_name, pid, parent_conversation,
        prompt, provider, model, config_path,
        file_edit_path, file_edit_id,
        json_mode=json_mode,
        spinner_message="Compacting conversation",
    )
```

**The compact-conversation.md prompt** (3,237 bytes, full content from `prompts/compact-conversation.md`):

```markdown
# Compact This Conversation

You are not summarizing a chat log. You are maintaining a durable working artifact.
The conversation is a mutable data structure that exists to support future work.
Its purpose is not to preserve chronology. Its purpose is to preserve capability.

## Core Principle
The agent is not the thing. The data is the thing. Optimize the conversation
for future transformations. Preserve information that would be expensive to
rediscover. Remove information that no longer contributes to future work.

## Data-Oriented Rules

Keep:
  - accepted decisions
  - user requirements
  - constraints
  - discovered invariants
  - successful experiments
  - important failed experiments
  - artifact summaries
  - repository knowledge
  - file-local knowledge
  - historical coupling information
  - open questions
  - TODO items
  - durable context

Remove:
  - repeated reasoning
  - repeated shell output
  - repeated file reads
  - duplicated summaries
  - obsolete hypotheses
  - intermediate exploration
  - dead conversations
  - verbose deliberation
  - chronology that no longer matters

Keep conclusions. Remove exploration. Keep decisions. Remove deliberation.
Keep state. Remove history.

## Transformation Rules

Replace many shell commands with verified outcomes.
Replace long investigations with:
  - conclusion
  - evidence
Replace long discussions with:
  - decision
  - reason
  - rejected alternatives
Merge duplicate investigations. Collapse repeated facts.
Delete obsolete information. Rewrite aggressively.
The conversation is not sacred.

## Preserve Artifact Knowledge

Preserve references to:
  - root context
  - per-file conversations
  - file summaries
  - repository history summaries
  - historical coupling
  - split indexes
  - patch artifacts

Prefer references over duplication.

## Preserve Failure Knowledge

Keep:
  - failed experiments
  - rejected designs
  - dangerous edge cases
  - corrected assumptions

Future workers should not repeat expensive mistakes.

## Required Output Structure

# User Intent
# Current Objective
# Accepted Decisions
# Constraints
# Durable Knowledge
## Global
## Artifact Local
## Repository History
## Historical Coupling
# Verified Facts
# Important Failed Attempts
# Open Questions
# TODO
# Minimal Context Needed To Continue

## Explicit Instructions

Do not preserve chronology. Preserve state.
Do not preserve conversation flow. Preserve useful information.
Do not preserve intermediate worker behavior. Preserve durable artifacts.
If ten pages can become one paragraph without reducing future capability, do so.
If an investigation can be represented as a fact, store the fact.
If a discussion can be represented as a decision, store the decision.
If repeated information exists, keep the best version.

## Self Review

Before finishing, verify:
  - Can another worker continue immediately?
  - Would expensive investigation need to be repeated?
  - Are accepted decisions preserved?
  - Are constraints preserved?
  - Are important failures preserved?
  - Are artifact references preserved?
  - Has duplicated information been removed?
  - Has chronology been replaced with state?
  - Is the conversation substantially smaller?
  - Is future capability unchanged or improved?

If not, continue compacting.
```

**The 10-question self-review checklist** (the load-bearing detail — it's the contract for "is this compaction successful?"):

| # | Self-review question | Verifies |
|---|---|---|
| 1 | Can another worker continue immediately? | preserved capability |
| 2 | Would expensive investigation need to be repeated? | preserved artifacts |
| 3 | Are accepted decisions preserved? | decision retention |
| 4 | Are constraints preserved? | constraint retention |
| 5 | Are important failures preserved? | failure retention |
| 6 | Are artifact references preserved? | ref retention |
| 7 | Has duplicated information been removed? | dedup |
| 8 | Has chronology been replaced with state? | state vs flow |
| 9 | Is the conversation substantially smaller? | compression |
| 10 | Is future capability unchanged or improved? | outcome preservation |

**The compact_prompt_path resolution** (from `bin/nagent:1965-1972`):

```python
def compact_prompt_path(root):
    # The compaction prompt is user-editable data. A copy under the nagent
    # root wins over the repo copy shipped next to the executable, and the
    # repo copy keeps --compact working from a plain checkout.
    user_prompt = root / "prompts" / "compact-conversation.md"
    if user_prompt.is_file():
        return user_prompt
    return COMPACT_PROMPT_PATH
```

Same pattern as `harvest_prompt_path` (root-first resolution; user override).

**The output structure** (the required shape of a compacted conversation):

```
# User Intent
# Current Objective
# Accepted Decisions
# Constraints
# Durable Knowledge
## Global
## Artifact Local
## Repository History
## Historical Coupling
# Verified Facts
# Important Failed Attempts
# Open Questions
# TODO
# Minimal Context Needed To Continue
```

This is a 12-section structure. The sections are deduplicated (no "decisions" appears in both "Accepted Decisions" and "Durable Knowledge > Artifact Local"). The shape is *deliberate* — it forces the compactor to separate state (decisions, facts, failures) from flow (chronology, exploration).

**Manual Slop equivalent.** `src/ai_client.py:run_discussion_compression(disc_text)` is the existing "Compress" button in the GUI (`gui_2.py:4252` → `app_controller._handle_compress_discussion:3357`). The behavior is **summarization**: it calls the LLM to produce a shorter text and replaces the discussion with the result.

The 12-section output structure is NOT enforced. The failure mode is graceful (the LLM returns *some* text), but the structure is lossy (the multi-turn shape is flattened into a single string).

**Verdict.** **GAP.** Manual Slop has summarization; it does not have behavior-preserving compaction. The 10-question self-review is a contract that the existing `run_discussion_compression` lacks.

**SSDL shape.** `->B->` (codepath with branch — the compaction is one of two paths: `try { compact } recover { audit_failure }`).

**Manual Slop next steps.**
- **Candidate 13: Conversation compaction** — MEDIUM priority. See §6 below for the deep-dive.

### 3.5 Project context files (`context.yaml` at git toplevel)

**nagent's claim.** Per-project context travels with the repo. When you `cd` into a project, nagent picks up the project's `context.yaml`/`context.md` automatically. Different projects can have different "personality" without forking the nagent install.

**nagent's implementation** (from `bin/nagent:635-658`):

```python
# Install context ships with the nagent folder itself (context.yaml or
# context.md next to bin/); it is injected before the per-root context so
# root context can override or extend it.
install_dir = process_path.resolve().parent.parent
install_context = load_root_context(install_dir)
install_context_block = f"\n{install_context}\n" if install_context else ""

# Project context: a context.yaml/context.md at the git toplevel of the
# current working directory. Skipped when that directory is the install
# or root directory, so the same file is not included twice (e.g. running
# nagent from inside the nagent checkout).
project_context_block = ""
project_dir = git_toplevel_for_path(Path.cwd())
if project_dir is not None:
    try:
        project_resolved = project_dir.resolve()
        is_duplicate = project_resolved == install_dir or project_resolved == root.resolve()
    except OSError:
        is_duplicate = True
    if not is_duplicate:
        project_context = load_root_context(project_dir)
        if project_context:
            project_context_block = f"\n{project_context}\n"

root_context = load_root_context(root)
root_context_block = f"\n{root_context}\n" if root_context else ""
```

**The injection order:** install → project → root. The "more personal context can override the more general." When the project toplevel *is* the install or root directory, the file is included once, not twice.

**The root-context loader** (recursive):

```python
def load_root_context(root_dir):
    """context.yaml can be a list or { "paths": [...] }; nested context.yaml
    files expand recursively. context.md is a plain markdown file."""
    ...
```

**The shipped install context** (from `context.yaml:1-34`):

```yaml
paths:
  - context/data-oriented-design.md
```

The 34-byte file points to the 13KB canonical DOD reference. The install context is *imported by reference*, not duplicated.

**Manual Slop equivalent.** Per-project config is `manual_slop.toml` (per the project's `[conductor].dir` override pattern from `src/paths.py`):

```toml
[paths]
logs_dir = "~/.manual_slop/logs"
scripts_dir = "~/.manual_slop/scripts"

[agent]
provider = "anthropic"
model = "claude-sonnet-4-6"

[conductor]
dir = "./conductor"

[routes]
... etc ...
```

Manual Slop's `manual_slop.toml` is *configuration* (TOML, structured). nagent's `context.yaml` is *operating rules* (YAML or markdown, free-form).

**Verdict.** **PARITY (DIFFERENT MECHANISM).** Manual Slop has per-project config (TOML); nagent has per-project context (YAML/markdown). Same intent, different syntax and different scope:
- nagent's `context.yaml` injects *prompt text* (operating rules, persona directives, knowledge)
- Manual Slop's `manual_slop.toml` injects *config* (paths, presets, hooks)

**The gap:** Manual Slop doesn't have a project-level prompt-injection mechanism. If the user wants a project's `manual_slop_context.md` to add "always be terse; prefer 200-line responses; focus on file X" — there is no current way to do that without editing the system prompt preset.

**SSDL shape.** `[I]` (the file load + dedup check).

**Manual Slop next steps.**
- **Candidate 14: Project context file** — LOW priority, small effort. A new `[context_files]` section in `manual_slop.toml` (or a `manual_slop_context.md` at the project toplevel) read by `aggregate.py:run` at discussion start. See §7.5.

### 3.6 The `claude-code` provider (5th provider, subscription auth)

**nagent's claim.** A user with a Claude Code subscription should be able to use that subscription in nagent, not require a separate API key. The "claude-code" provider is a thin wrapper around the Claude Agent SDK that delegates auth to the local Claude Code install.

**nagent's implementation** (from `bin/helpers/nagent_llm.py:65-80,195-220`):

```python
PROVIDERS = ("openai", "anthropic", "google", "gemini", "cursor", "claude-code")
PROVIDER_ALIASES = {"gemini": "google"}

# For the claude-code provider, "default" means Claude Code's own configured
# model: the SDK is invoked with model=None and Claude Code decides.
CLAUDE_CODE_DEFAULT_MODEL = "default"

DEFAULT_MODELS = {
    "openai": "gpt-5.5",
    "anthropic": "claude-sonnet-4-6",
    "google": "gemini-2.5-flash",
    "cursor": "composer-2.5",
    "claude-code": CLAUDE_CODE_DEFAULT_MODEL,
}

# An empty tuple means the provider manages its own credentials; claude-code
# uses the local Claude Code login (subscription or API key), not an env var.
CREDENTIAL_ENV = {
    "openai": ("OPENAI_API_KEY",),
    "anthropic": ("ANTHROPIC_API_KEY",),
    "google": ("GOOGLE_API_KEY", "GEMINI_API_KEY"),
    "cursor": ("CURSOR_API_KEY",),
    "claude-code": (),
}

PACKAGE_HINTS = {
    "openai": "openai",
    "anthropic": "anthropic",
    "google": "google-genai",
    "cursor": "cursor-sdk",
    "claude-code": "claude-agent-sdk",
}
```

**The `claude-code` provider function** (from `bin/helpers/nagent_llm.py:195-220`):

```python
def _claude_code_generate(message, model, *, allowed_tools=None, max_turns=1):
    """Run one prompt through the local Claude Code via the Claude Agent SDK.
    Authentication is Claude Code's own login (subscription or API key) — no
    environment variable is read here. Tools are disabled by default so this
    behaves as plain text generation; pass allowed_tools to permit specific
    tools (e.g. Read for file analysis)."""
    anyio, query, ClaudeAgentOptions, AssistantMessage, ResultMessage, TextBlock = require_package(
        "claude-code"
    )
    # No model and "default" mean the same thing: Claude Code's configured model.
    options = ClaudeAgentOptions(
        model=None if not model or model == CLAUDE_CODE_DEFAULT_MODEL else model,
        max_turns=max_turns,
        tools=list(allowed_tools) if allowed_tools else [],
        allowed_tools=list(allowed_tools) if allowed_tools else [],
        cwd=os.getcwd(),
    )
    async def run_query():
        texts = []
        result_message = None
        async for sdk_message in query(prompt=message, options=options):
            if isinstance(sdk_message, AssistantMessage):
                for block in sdk_message.content:
                    if isinstance(block, TextBlock):
                        texts.append(block.text)
            elif isinstance(sdk_message, ResultMessage):
                result_message = sdk_message
        return texts, result_message

    texts, result_message = anyio.run(run_query)
    if result_message is not None and result_message.is_error:
        errors = getattr(result_message, "errors", None) or []
        detail = errors[0] if errors else (result_message.result or "claude-code query failed")
        raise RuntimeError(f"claude-code provider failed: {detail}")
    text = ""
    usage = None
    if result_message is not None:
        text = result_message.result or ""
        usage = result_message.usage
    if not text:
        text = "\n".join(texts)
    return _result_with_usage(text, usage, message)
```

**The provider table from the README:**

| Provider | Default model | Credential environment variable |
|---|---|---|
| `openai` | `gpt-5.5` | `OPENAI_API_KEY` |
| `anthropic` | `claude-sonnet-4-6` | `ANTHROPIC_API_KEY` |
| `google` | `gemini-2.5-flash` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
| `cursor` | `composer-2.5` | `CURSOR_API_KEY` |
| `claude-code` | `default` | None — uses the local Claude Code login |

**The key behavior details:**

- `model=None` for default mode (Claude Code picks the model).
- `model="default"` is the same as `model=None`.
- Any Claude model id or alias (`sonnet`, `opus`, `haiku`) overrides.
- `max_turns=1` for plain text generation.
- `max_turns=None` for upload mode (read-then-answer; passed to `nagent-llm-upload`).
- Tools are disabled by default.
- `nagent-llm-upload` permits only the `Read` tool so Claude Code can read the file locally.

**Manual Slop equivalent.** Manual Slop's 5 native providers + 3 OpenAI-compatible providers (per `src/ai_client.py:2883`):

| Manual Slop provider | Path | Auth |
|---|---|---|
| `anthropic` | `_send_anthropic` | `ANTHROPIC_API_KEY` |
| `gemini` | `_send_gemini` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
| `gemini_cli` (headless) | `_send_gemini_cli` | Local `gemini` CLI login (subscription OR API key) |
| `deepseek` | `_send_deepseek` | `DEEPSEEK_API_KEY` |
| `minimax` | `_send_minimax` | OpenAI-compatible (custom URL or default) |
| `grok` | `_send_grok` | OpenAI-compatible (xAI base URL) |
| `qwen` | `_send_qwen` | DashScope native SDK |
| `llama` (OpenAI-compatible) | `_send_llama` | Ollama, OpenRouter, or custom |
| `llama_native` | `_send_llama_native` | Ollama native SDK |

The `gemini_cli` path is the **direct analog** of nagent's `claude-code` provider: local subprocess with whatever auth the user has on their local install. No env var read; subscription auth OR API key.

**Verdict.** **PARITY.** Manual Slop already has the local-CLI subscription-auth pattern (Gemini CLI). The pattern nagent is adding for Claude Code is the same shape. No new Manual Slop work needed for the *pattern*; the question is whether to add a Claude Code provider *specifically* (which would be a provider addition, not a pattern).

**SSDL shape.** `[I]` (single instruction, the API call).

**Manual Slop next steps.**
- Not a new track; a provider addition (a new `_send_claude_code`) fits into a future "more providers" follow-up if the user wants Claude Code integration. ~200-400 lines.

### 3.7 The shared `context/data-oriented-design.md` (13,084 bytes)

**nagent's claim.** Operating rules, not philosophy: every rule tells you what to *do*. The rules are tiered (Tier 0/1/2) and the simplification pass is explicit.

**The structure** (per the `headings` summary of `context/data-oriented-design.md`):

| Section | Purpose |
|---|---|
| "Scale the ceremony to the task" | Tier 0/1/2 definitions (trivial / non-trivial / subsystem-scale) |
| "Precedence when rules conflict" | User instruction > this document > existing codebase |
| "Defaults to reject" | 3 anti-patterns: tools are the platform; design around a model; solution matters more than data |
| "Core defaults" | 8 principles: the problem is the data; state the cost; solve only the problem you have; where there's one there's many; the common case dominates; exploit every constraint; simplicity is removing work; "can't be done" is a cost claim |
| "Get the real data" | Inspect before assuming; label every assumption; never fabricate; 5 questions about the data |
| "Method" (tier 1+) | Frame it; get the data; state the cost; design the transform; simplification pass; define done; verify |
| "Simplification pass" (7 questions) | Not do this; only once; fewer times; approximate; small lookup; large lookup; small buffer |
| "Design rules" | Partition by case, not per-element; explicit out-of-range; complexity requires evidence |
| "Performance claims" | Never assert unmeasured; measure or label unverified; no requirement → no optimization |
| "Software specifics" | Batch-first transforms; memory/layout/access; data protocols; hardware is the platform |
| "Enforceable deliverables" (tier 2) | Batch transform contract; plural/batch path; pointer-heavy justification; out-of-range; local issue files |
| "Final self-check" (10 questions) | Plan, common case, simplification, no speculation, out-of-range, transforms, pointers, performance, done-criteria, deliverables |

**The 3 defaults to reject (verbatim):**

1. **"The tools are the platform."** Reality is the platform: the actual hardware, organization, deadline, physics. *Do instead:* before designing, name the real platform and the 2-3 of its fixed properties that constrain this solution, and design within them.
2. **"Design around a model of the world."** World models (objects, metaphors, idealized categories) hide the actual data and the actual cost. *Do instead:* design around the data. Do not introduce an abstraction until you can describe, concretely, the data it organizes and the transform it serves — and what the abstraction costs.
3. **"The solution matters more than the data."** The only purpose of any solution is to transform data from one form to another. *Do instead:* start every task from the actual inputs and required outputs, never from the machinery you'd like to build.

**The simplification pass (7 questions, applied recursively to every sub-problem):**

| # | Question | Reduces |
|---|---|---|
| 1 | Can we not do this at all? | Work that shouldn't exist |
| 2 | Can we do this only once (precompute, cache, amortize)? | Repeated work |
| 3 | Can we do this fewer times? | Frequency of work |
| 4 | Can we approximate the result so that no one notices the difference? | Precision cost |
| 5 | Can we use a small lookup table? | Branching cost |
| 6 | Can we use a large lookup table? | Branching cost (alternative) |
| 7 | Can we use a small buffer/FIFO to decouple producer from consumer? | Coupling cost |
| 8 | Can we constrain the problem further so a simpler machine suffices? | Generality cost |

**The 10-question final self-check:**

- [ ] The plan answered the framing, data, and cost questions — or every gap is labeled `ASSUMPTION` with what it affects.
- [ ] The most common case is identified and the design serves it straight-line; rare/error cases are out of the common path.
- [ ] The simplification pass ran; the work it removed (or why nothing could be removed) is stated.
- [ ] No speculative generality: no parameter, option, or abstraction exists for a need that isn't real yet.
- [ ] Out-of-range and error behavior is explicit at every boundary.
- [ ] Transforms are plural/batch, or the singleton exception is documented.
- [ ] Pointer-heavy hot paths carry their written justification; everything else uses indices.
- [ ] No unmeasured performance claim anywhere in code, comments, or summary; measurements included where possible, hypotheses labeled where not.
- [ ] Done-criteria from the plan were checked, and the summary reports what was verified and what wasn't.
- [ ] (Tier 2) Deliverables above are present; open questions are filed under `issues/`.

**Manual Slop equivalent.** None. Manual Slop's `conductor/code_styleguides/` has 5 files (`chroma_cache.md`, `config_state_owner.md`, `error_handling.md`, `python.md`, `workspace_paths.md`), but no canonical DOD reference.

**Verdict.** **GAP.** Manual Slop needs a canonical DOD reference, both for the project and for injection into the agent-facing files (per Candidate 16).

**SSDL shape.** (philosophical, not codifiable)

**Manual Slop next steps.**
- **Candidate 16: AGENTS.md `@import` + canonical DOD file** — HIGH priority. The canonical file is the foundation. See §10.

### 3.8 `CLAUDE.md` as the agent-facing rules file (the `@import` pattern)

**nagent's claim.** The agent-facing rules file imports the canonical DOD via `@path` syntax. The same file is injected via `context.yaml` for runtime. **One source of truth, two consumers.**

**The `CLAUDE.md` content** (verbatim, 5,832 bytes):

```markdown
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working
with code in this repository.

## Operating rules
@context/data-oriented-design.md
The same file is injected into every nagent conversation via the repo's
context.yaml — one source of truth for both harnesses. Edit it there; do
not duplicate rules into this file.

## What this is
**nagent** ("not-an-agent") is a small reference implementation of a
data-oriented LLM workflow loop. The thesis drives every design decision
and should drive yours: **the data is the thing, not the agent.** State
that matters lives in inspectable, editable files on disk — never hidden
in process memory. When output is wrong, fix the generator or its inputs
(the prompt), don't patch the artifact. `README.md` is the canonical
teaching document; read it before making non-trivial changes.

## Commands
```bash
# Setup
pip install -r requirements.txt
export PATH="$PWD/bin:$PATH"     # tools must be on PATH; nagent shells out to its siblings by name
mkdir -p ~/.nagent && cp config.example.json ~/.nagent/config.json

# Tests (no framework beyond stdlib unittest)
python3 -m unittest discover -s tests -v
python3 -m unittest tests.test_nagent_file_split -v      # one module
python3 -m unittest tests.test_nagent.SomeTest.test_case -v  # one test
```

There is no build step, linter, or package manifest — the `bin/`
scripts are run directly. Provider SDKs (`requirements.txt`) are only
needed for live LLM calls; most tests mock the provider.

## Architecture
The system is a set of standalone CLI executables in `bin/`, each of
which prints its own purpose when run with `--description`. There is
**no central registry**: `collect_bin_tool_descriptions()` discovers
tools by running every `bin/` executable with `--description` and
injecting the results into the startup prompt. A new tool becomes visible
to the loop simply by being an executable in `bin/` that handles
`--description` (via `exit_on_description()` in
`bin/helpers/nagent_cli.py`). Thin wrappers live in `bin/`; real logic
lives in `bin/helpers/*_lib.py`.

- `bin/nagent` (~2400 lines) — the main loop and the bulk of the system.
  Read this path first: `main() → run_agent_loop() → call_llm() →
  parse_response() → process_tags()`. The loop appends to a conversation
  file, sends the whole file to the LLM, parses structured tags, runs
  handlers, appends results, and repeats until a final `<nagent-response>`
  is emitted.
- `bin/helpers/nagent_llm.py` — provider abstraction.
  `generate_text_with_usage()` is the single primitive (file in → text
  out) for `openai`, `anthropic`, `google`, `cursor`, `claude-code`.
  Provider churn should stay isolated here.
- `bin/nagent-llm-text` / `bin/nagent-llm-upload` — CLI front ends for
  text and file-upload generation.
- `bin/nagent-file-edit` + `nagent_file_edit_lib.py` — per-file
  conversations and git-history context.
- `bin/nagent-file-split` / `-patch` / `-summarize` + their `_lib.py` —
  large-file handling (split → bounded edit → patch).

### The structured-tag protocol
The model communicates only through a fixed set of XML-ish tags
(`<nagent-response>`, `<nagent-read>`, `<nagent-file-read>`,
`<nagent-file-patch>`, `<nagent-write>`, `<nagent-shell>`,
`<nagent-next>`, `<nagent-conversation>`, plus `<nagent-conversation
conversation-file="...">` and `<nagent-conversation conversation-name="...">`).
The protocol is *defined inside the prompt* — `build_initial_context()` /
`create_initial_text()` embed the tag list inside `<nagent-conversation>`
so refreshed context always carries the current contract.
`parse_response()` enforces it strictly with regex; malformed output
triggers up to `MAX_FORMAT_RETRIES` (3) visible correction turns appended
to the conversation. If you add or change a tag, update **both** the
prompt-building functions and the parser/handler dispatch in
`process_tags()`, and add a test.

### Durable state lives under `~/.nagent/`
- `conversations/` — conversation files (the working state). Named per
  host+shell via `default_conversation_name()` / `default_pid()`.
- `conversations/file-index-{pid}.json` — maps stable file ids
  (`device:inode` from `file_id_for_path()`, not paths) to per-file
  conversations, so renames survive.
- `config.json` — provider/model defaults (overridable by `NAGENT_CONFIG`
  env var and then by CLI flags, in that precedence order).
- `context.yaml` / `context.md` — root context injected into every
  conversation; nested `context.yaml` files expand recursively.

### Write boundaries (conventions, not a sandbox)
Shell runs with full user permissions — there is no security boundary,
only checked structured writes. `validate_write_path()` allows
`<nagent-write>` to write only to `/tmp`, `/var/tmp`, or `$TMPDIR` in main
mode, or to the target file / its split segments in per-file-edit mode.
Project files are edited via `nagent-file-edit`, not direct writes from
the main loop.

### Large files
Inline reads cap at 64 KB. Beyond that, files are split into segment
files plus an `index.json` (carrying source hash, line ranges, split
type) by language-aware splitters in
`bin/helpers/nagent-file-split-*`. Edits target segments;
`nagent-file-patch` validates the source hash, merges segments, and
emits a unified-diff patch.

## Conventions for changes
- Prefer adding a self-describing `bin/` executable over wiring a new
  code path into the loop, unless it genuinely belongs in the loop.
- Keep provider-specific code inside `nagent_llm.py`.
- Tests in `tests/` double as executable specs (parser, conversation
  lifecycle, retries, token accounting, file ids, split/patch, providers,
  tool descriptions). Add or update the matching test for any behavioral
  change.
- `prompts/` holds reusable prompt documents (e.g. README-generation,
  conversation-compaction) used by the workflow, not application source.
```

**The `@import` pattern.** The line `@context/data-oriented-design.md` is the load-bearing detail. The same file is injected into the agent's context (when Claude Code reads `CLAUDE.md`) and into every nagent conversation (via `context.yaml` → `context/data-oriented-design.md`). One source of truth.

**Manual Slop equivalent.** Manual Slop has `AGENTS.md` (the project root, ~5.4KB) but no canonical rules file to import. The pattern needs to be mirrored:
1. Create `conductor/code_styleguides/data_oriented_design.md` (the canonical DOD reference, adapted from nagent's `context/data-oriented-design.md`)
2. Add `@conductor/code_styleguides/data_oriented_design.md` to `AGENTS.md` (the existing project-root agent-facing file)
3. Create `./docs/AGENTS.md` (a new agent-facing mirror of `docs/Readme.md`; the human-facing `docs/Readme.md` stays as-is)
4. Inject the same canonical file via `[agent].context_files` in `manual_slop.toml` (or equivalent project config) so the Application's RAG / context assembly picks it up

**The human Readme files stay human-facing.** Per the user's explicit instruction: don't restructure `Readme.md` or `docs/Readme.md` to be agent-facing. They are human-facing. The new agent-facing files are separate.

**Verdict.** **GAP.** Manual Slop has `AGENTS.md` but no canonical rules file. The `@import` pattern is absent.

**SSDL shape.** `[I]` (single instruction, the file load).

**Manual Slop next steps.**
- **Candidate 16: AGENTS.md `@import` + canonical DOD file** — HIGH priority. The foundation for all the other styleguides. See §10.

### 3.9 Per-file knowledge notes (`knowledge/files/{file_id}.md`)

**nagent's claim.** When you know things about a specific file, those notes should live next to the file's identity (inode), not next to a conversation or a session. Then, the next time the file is in scope, the notes come back automatically.

**nagent's implementation** (from `bin/helpers/nagent_gc_lib.py:merge_harvest` "files" branch):

```python
file_notes = 0
for row in harvested.get("files", []):
    if not isinstance(row, dict):
        continue
    path_text = str(row.get("path") or "").strip()
    note = str(row.get("note") or "").strip()
    if not note:
        continue
    target = Path(path_text) if path_text else None
    if target is not None and target.is_file():
        try:
            file_id = file_id_for_path(target)
        except OSError:
            file_id = None
        if file_id is not None:
            _append_bullets(
                file_knowledge_path(root, file_id), f"# {target.resolve()}",
                [f"{note} {provenance}"],
            )
        file_notes += 1
        continue
    # Target no longer resolvable: the note survives as a fact.
    prefix = f"{path_text}: " if path_text else ""
    _append_bullets(knowledge / "facts.md", "# Facts", [f"{prefix}{note} {provenance}"])
    file_notes += 1
counts["files"] = file_notes
```

**The codepath:**

```
[loop: each harvested["files"] row]
   │
   ├──► [Q:path_text is not empty?]
   │        │
   │        ├── no ──► [T:skip]
   │        │
   │        ▼
   │     [Q:target.is_file()?]
   │        │
   │        ├── no ──► [I:fall back to facts.md: append "{path_text}: {note} {provenance}"]
   │        │
   │        └── yes ──► [I:file_id = file_id_for_path(target)]
   │                       │
   │                       ├── [Q:file_id is not None?]
   │                       │     │
   │                       │     ├── no  ──► [T:skip]   (couldn't stat the file)
   │                       │     │
   │                       │     └── yes ──► [I:append to knowledge/files/{file_id}.md]
   │                       │
   │                       [S:file_notes += 1]
   │
   [T:counts["files"] = file_notes]
```

**The function `file_knowledge_path` (line ~50):**

```python
def file_knowledge_path(root, file_id):
    return knowledge_dir(root) / "files" / f"{file_id}.md"
```

**Manual Slop equivalent.** `models.FileItem` (per `src/models.py:510`) has 9 fields: `path, auto_aggregate, force_full, view_mode, selected, ast_signatures, ast_definitions, ast_mask, custom_slices`. **No `notes` field.** No per-file knowledge notes dimension.

**Verdict.** **GAP.** The per-file notes dimension is absent in Manual Slop. `FileItem` would need a `notes: str = ""` field; the Structural File Editor would need a "Notes" text area; `aggregate.py:run` would need a `{file-knowledge}` block in the initial context.

**SSDL shape.** `[I]` (the per-file note write).

**Manual Slop next steps.**
- **Candidate 11.1: per-file knowledge notes** — bundle with Candidate 11. See §4 below.

### 3.10 "Delete to turn off" feature flags

**nagent's claim.** Feature flags should be data, not config. If a feature is gated by the presence of a file, the user can turn it off by deleting the file. No GUI toggle, no env var, no `config.toml` edit. Just `rm`.

**nagent's implementation.** The pattern recurs in 3 places:

1. **The knowledge digest** (`bin/helpers/nagent_gc_lib.py:regenerate_digest`):

```python
if not sections:
    if target.is_file():
        target.unlink()
    return None
```

2. **The injection point** (`bin/nagent:677-685`):

```python
knowledge_digest = load_context_file(digest_path(root))
knowledge_block = ""
if knowledge_digest:
    knowledge_block = ...
```

3. **The pattern generalized.** The "feature flag is a file" is the load-bearing convention. nagent doesn't have a centralized feature flag mechanism; it has *file-presence-based* feature gating scattered through the codebase.

**Manual Slop equivalent.** Per the v1 review:
- `[ai_settings.toml]` toggles (`rag_enabled`, `auto_aggregate`, `force_full`, etc.) — per-feature flags
- GUI checkboxes (parallel to the TOML)
- `manual_slop.toml` per-project settings (TOML-based)

**Verdict.** **PARITY (DIFFERENT MECHANISM).** Manual Slop uses config + GUI; nagent uses file presence. Both are valid. nagent's pattern is more discoverable in the file tree (`ls ~/.nagent/knowledge/` and you see `digest.md` is there, so the knowledge injection is on); Manual Slop's pattern is more discoverable in the GUI.

**SSDL shape.** `[I]` (the check + the unlink).

**Manual Slop next steps.**
- New `conductor/code_styleguides/feature_flags.md` — codify both patterns (config + file presence) and document when to use each. NOT a new track; a styleguide update.

### 3.11 Save-with-graceful-summary-failure

**nagent's claim.** A save operation should not fail because a non-essential post-step (like an LLM-generated summary) failed. Degrade gracefully: save the artifact, mark the missing piece visibly.

**nagent's implementation** (from the `67a3ea5` commit message): *"Save-conversation indexes the copy even when the summary LLM fails; fresh conversations build initial context once."*

**The behavior, derived from the README:**

> "`--save-conversation NAME` copies the conversation and records it, with an LLM-generated summary, in a saved-conversations index. **If the summary fails (no credentials, provider down), the save still completes — the index gets a visible '(summary unavailable)' marker instead of losing the entry.**"

**The codepath** (inferred; not source-quoted because the source-reading for `--save-conversation` was shallow):

```
[I:save-conversation --name X]
   │
   ├──► [I:copy conversation to conversations/X-<uuid>]
   │
   ├──► [try: LLM call for summary]
   │        │
   │        ├── success ──► [I:append to saved-conversations index: path, name, summary]
   │        │
   │        └── failure ──► [I:append to saved-conversations index: path, name, "(summary unavailable)"]
   │
   [T:return 0]
```

**Manual Slop equivalent.** `src/ai_client.py:run_discussion_compression(disc_text)` is the equivalent of `--save-conversation` (the LLM post-step). The Compress button in the GUI calls this; on LLM failure, the discussion is *not* replaced (presumably — needs source verification).

**The open question.** Does `run_discussion_compression` raise on LLM failure (destructive — the Compress button would destroy the original) or fall back to the original (graceful)?

**Verdict.** **UNKNOWN** without reading the source. The pattern is the same; the implementation detail is what matters.

**SSDL shape.** `->B->` (codepath with branch — the summary is one of two paths: `try { summarize } recover { mark_unavailable }`).

**Manual Slop next steps.**
- **Candidate 15: Save-with-graceful-summary-failure** — TBD. The verification (read `src/ai_client.py:run_discussion_compression`) is cheap (one source read); the potential value is high (latent bug).

### 3.12 Delegation reframed as "context management, not parallelism"

**nagent's claim.** The reason to spawn a sub-conversation is to keep the parent's context clean. The fact that the child runs concurrently (sometimes) is incidental.

**nagent's implementation** (from `bin/nagent:726-731`):

```markdown
Conversations are data (create, reuse, hand off):
- Reuse a worker: <nagent-conversation conversation-file="{{name}}"> continues that
  conversation with its accumulated context. Name workers you will call
  again (e.g. "test-runner"); use a fresh unnamed sub-conversation when
  isolation matters more than memory.
- Resume saved work: conversation-name="{{saved-name}}" starts the
  child from a conversation the user saved with --save-conversation.
- Author a worker's context: write a curated briefing to a temp file
  with <nagent-write>, then spawn <nagent-conversation
  conversation-file="/tmp/...">. Include only what the worker needs:
  goal, constraints, paths, known facts.
- Hand off when noisy: if this conversation is mostly stale tool
  output, distill goal/state/decisions into a sub-conversation prompt,
  delegate the rest, and tell your caller about the handoff. Never
  rewrite your own conversation file while running.
- This conversation persists across invocations, and the user may edit
  it between runs. The current file is the source of truth.
```

**The reframing table** (from the README's §12 "Managing Context and Large Files"):

| Long-lived agent abstractions | Disposable workers |
|---|---|
| Identity is central | Output artifact is central |
| Shared context gets noisy | Child context is isolated |
| Parent absorbs all exploration | Parent gets a concise result |
| Delegation implies personality | Delegation is context management |

**Manual Slop equivalent.** Manual Slop's MMA already does this implicitly:
- `src/multi_agent_conductor.py` runs each MMA worker as a fresh subprocess with `ai_client.reset_session()` at the start of `run_worker_lifecycle`
- The worker returns a `Result[TaskOutput, ErrorInfo]` to the parent (the `ConductorEngine`)
- The parent's `disc_entries` doesn't accumulate the worker's intermediate reads/shell calls

But the 1:1 discussion path has no sub-agent primitive. The user types a prompt, the AI responds, the loop continues. If the user wants the AI to "investigate this file" or "look up this API," the answer has to come from the same conversation.

**Verdict.** **PARITY for MMA; GAP for 1:1 discussions.** MMA does it right; 1:1 needs the primitive.

**SSDL shape.** `=>` (wide codepath: parent + child codepaths run in parallel; the child is disposable; the parent gets a concise result).

**Manual Slop next steps.**
- **Candidate 1: `SubConversationRunner`** — HIGH priority. The MMA pattern as a reusable App-callable class. See §10.

---


## 4. The harvest pattern in detail (the new big one)

The harvest is the largest single new subsystem. This section is the implementation deep-dive, the data-shape catalog, the test surface, the Manual Slop implementation outline, and the recommended follow-on work.

### 4.1 The data shapes

**The category files** (per the `CATEGORY_FILES` dict in `bin/helpers/nagent_gc_lib.py:25-30`):

| Category | File | Header (initial) | Bullet format |
|---|---|---|---|
| `facts` | `facts.md` | `# Facts` | `- {statement, detail, provenance}` |
| `decisions` | `decisions.md` | `# Decisions` | `- {statement, detail, provenance}` |
| `questions` | `questions.md` | `# Questions` | `- {statement, detail, provenance}` |
| `playbooks` | `playbooks.md` | `# Playbooks` | `- **{name}**: {steps} {provenance}` |
| (combined) | `tasks.md` | `# Tasks\n\n## Open\n\n## Done` | `- {statement, detail, provenance}` |
| (per-file) | `knowledge/files/{file_id}.md` | `# {target.resolve()}` | `- {note} {provenance}` |

**The provenance string** (the load-bearing detail): `f"[from: {conversation_name}, {date}]"`. The `date` is the ISO-8601 date prefix of the harvest timestamp (`timestamp[:10]` where `timestamp = datetime.now(timezone.utc).isoformat()`).

**An example category file after 3 harvests:**

```markdown
# Facts

- The MCP dispatch uses a flat if/elif chain. 4 places, 45 tools. [from: 2026-05-12-investigate-dispatch, 2026-05-12]
- ai_client.py has 5 separate per-provider history lists, each with their own lock. Switching providers mid-session loses history. [from: 2026-05-13-state-mutation-matrix, 2026-05-13]
- RAG is opt-in. Default-off in new projects. [from: 2026-06-12-candidate-11-framing, 2026-06-12]
```

**The digest format** (per `regenerate_digest`):

```markdown
# Knowledge digest
(regenerated by nagent-gc; edit the category files, not this file)

## Open tasks
- Verify Candidate 15 by reading run_discussion_compression. [from: 2026-06-12-candidate-15, 2026-06-12]
- Create canonical DOD file at conductor/code_styleguides/. [from: 2026-06-12-candidate-16, 2026-06-12]

## Open questions
- Where does intent resolution live — per-verb, per-block, or global? [from: 2026-06-12-follow-up-b, 2026-06-12]

## Decisions
- Knowledge harvest is a complement to curation + discussion, not a RAG replacement. [from: 2026-06-12-candidate-11, 2026-06-12]

## Facts
- nagent has 5 providers; Manual Slop has 8. [from: 2026-06-12-v2.3-review, 2026-06-12]
- SSDL has 6 primitives + 7 modifiers. [from: 2026-06-12-style-reference, 2026-06-12]

## Playbooks
- **Knowledge Harvest**: scan -> classify -> LLM-distill -> append -> digest -> reclaim. [from: 2026-06-12-candidate-11, 2026-06-12]
- **Stable-to-Volatile Cache Ordering**: identify Instance: boundary -> pass to --cache-prefix-chars. [from: 2026-06-12-candidate-12, 2026-06-12]
```

The digest is bounded to `DIGEST_MAX_BYTES = 4 * 1024 = 4096 bytes`. If the sections don't fit, the rest is truncated with a visible "(truncated; see the category files for the rest)" note.

**The ledger format** (per `load_ledger` / `save_ledger`):

```json
{
  "entries": {
    "<sha256-of-conversation-content>": {
      "path": "/home/user/.nagent/conversations/<name>-<uuid>",
      "status": "harvested",
      "at": "2026-06-12T14:23:45.123456+00:00",
      "items": {
        "facts": 3,
        "decisions": 2,
        "tasks_done": 1,
        "tasks_open": 0,
        "questions": 1,
        "playbooks": 0,
        "files": 1
      },
      "deleted": true
    },
    "<sha256-of-another-conversation>": {
      "path": "...",
      "status": "harvest-failed",
      "at": "2026-06-12T14:24:00.000000+00:00",
      "deleted": false,
      "error": "provider 'openai' not available"
    }
  }
}
```

The ledger is sha256-of-content, not sha256-of-path. Two conversations with identical content share an entry. Status values: `harvested`, `harvest-failed`, `deleted-unharvested`, `too-large`.

### 4.2 The full harvest codepath (as a single SSDL diagram)

```
[Q:root = ~/.nagent (default) or --root PATH]
   │
   ▼
[Q:root is a directory?]
   │
   ├── no ──► [T:exit 1]   (Error: nagent root not found)
   │
   ▼
[Q:--no-harvest?]
   │
   ├── yes ──► [I:harvest = False]
   │
   └── no  ──► [I:harvest = True]
                │
                ▼
[Q:--apply?]
   │
   ├── no ──► [I:apply = False]
   │
   └── yes ──► [I:apply = True]
                │
                ▼
[Q:apply AND harvest?]
   │
   ├── no ──► [I:provider, model = None]
   │
   └── yes ──► [I:resolve provider + model from CLI / config / defaults]
                │
                ▼
[Q:provider available? (credentials set, package installed)]
   │
   ├── no ──► [T:exit 1]
   │
   ▼
[I:run_gc(root, apply=apply, harvest=harvest, ...)]

  ┌─── inside run_gc ───
  │
  │  [I:scan_root(root)]   (artifacts list with klass + reason + size)
  │   │
  │   ▼
  │  [I:filter to harvest candidates + prune candidates]
  │   │
  │   ▼
  │  [I:compute totals (live, user-kept, prune, harvest, keep) + sizes]
  │   │
  │   ▼
  │  [Q:harvest?]
  │   │
  │   ├── no ──► [I:build dry-run report]   [T:return report]
  │   │
  │   ▼
  │  [I:load_ledger(root)]
  │   │
  │   ▼
  │  [loop: each harvest candidate]
  │   │
  │   ├──► [I:sha256_of(artifact.path)]
  │   │
  │   ├──► [Q:ledger has this sha with status=harvested?]
  │   │     │
  │   │     ├── yes ──► [I:reclaim without re-distill]   [S:ledger updated]
  │   │     │
  │   │     └── no  ──►
  │   │               │
  │   │               [Q:size > MAX_HARVEST_SOURCE_BYTES (1MB)?]
  │   │               │
  │   │               ├── yes ──► [S:ledger entry "too-large"]   [S:keep]
  │   │               │
  │   │               └── no  ──►
  │   │                         │
  │   │                         [Q:over --max-harvest-bytes budget?]
  │   │                         │
  │   │                         ├── yes ──► [S:deferred]
  │   │                         │
  │   │                         └── no ──►
  │   │                                │
  │   │                                [Q:apply?]
  │   │                                │
  │   │                                ├── no ──► [S:counted in dry-run]
  │   │                                │
  │   │                                └── yes ──►
  │   │                                          │
  │   │                                          [try]
  │   │                                            [Q:size > SUMMARIZE_THRESHOLD (64KB)?]
  │   │                                              │
  │   │                                              ├── yes ──► [I:summarize via subprocess nagent-file-summarize]
  │   │                                              └── no  ──► [I:read full text]
  │   │                                            [I:build_harvest_prompt(template, name, content, retry=attempt > 0)]
  │   │                                            [I:LLM call via generate(prompt, provider, model)]
  │   │                                            [I:parse_harvest_json(response)]
  │   │                                          [catch (json.JSONDecodeError, ValueError)]
  │   │                                            [I:retry with "Return only JSON" suffix]   (up to HARVEST_MAX_ATTEMPTS=2)
  │   │                                            [S:ledger entry "harvest-failed"]
  │   │                                          [success]
  │   │                                            [I:merge_harvest(root, name, harvested, date)]   (append to category files)
  │   │                                            [S:ledger entry "harvested" with items count]
  │   │                                            [I:reclaim (unlink)]
  │   │
  │   ▼
  │  [Q:prune candidates exist?]
  │   │
  │   └──► [I:prune stale split directories]
  │        [I:prune file-index entries whose target is gone]
  │        [I:prune saved-conversations entries whose path is gone]
  │   │
  │   ▼
  │  [I:save_ledger]
  │   │
  │   ▼
  │  [I:regenerate_digest(root, max_bytes=DIGEST_MAX_BYTES=4096)]
  │   │
  │   ▼
  │  [I:build report: totals, reclaimed, harvested items, failures, deferred, ledger path, digest path]
  │   │
  │   └─── [T:return report]
  └───────────────────

[T:return report to caller]
```

### 4.3 The retry budget (load-bearing detail)

`HARVEST_MAX_ATTEMPTS = 2` (from `bin/helpers/nagent_gc_lib.py:15`). The retry is at the parse level (not the API level):

```python
def harvest_conversation(root, path, provider, model, config_path, *, generate, summarize=None):
    size = path.stat().st_size
    if size > SUMMARIZE_THRESHOLD_BYTES:
        summarize_fn = summarize or _default_summarize
        content = summarize_fn(path, provider, model, config_path)
    else:
        content = path.read_text(encoding="utf-8")
    template = harvest_prompt_path(root).read_text(encoding="utf-8").strip()
    last_error = None
    for attempt in range(HARVEST_MAX_ATTEMPTS):
        prompt = build_harvest_prompt(template, path.name, content, retry=attempt > 0)
        response = generate(prompt, provider, model)
        try:
            return parse_harvest_json(response)
        except (json.JSONDecodeError, ValueError) as exc:
            last_error = exc
    raise RuntimeError(f"harvest output invalid after {HARVEST_MAX_ATTEMPTS} attempts: {last_error}")
```

**The retry-suffix string** (per `build_harvest_prompt`):

```python
suffix = "\nYour previous reply was not valid JSON. Return only the JSON object.\n" if retry else ""
```

So the LLM sees its previous (malformed) response and a one-line correction on retry.

**The parse function** (strict, with code-fence tolerance):

```python
def parse_harvest_json(text: str) -> dict:
    stripped = text.strip()
    fence = JSON_FENCE.match(stripped)
    if fence:
        stripped = fence.group(1).strip()
    payload = json.loads(stripped)
    if not isinstance(payload, dict):
        raise ValueError("harvest output is not a JSON object")
    harvested = {}
    for category in ITEM_CATEGORIES:
        rows = payload.get(category, [])
        harvested[category] = rows if isinstance(rows, list) else []
    return harvested
```

Where `JSON_FENCE = re.compile(r"```(?:json)?\s*(.*?)\s*```", re.DOTALL)`. So the parser accepts both raw JSON and `\`\`\`json ... \`\`\`` (Markdown code fence). The validation is strict (the categories must be the 7 known ones; rows must be lists).

### 4.4 The per-file knowledge notes (sub-pattern)

**The `files` category** is special. Each row is `{path, note}`. The path is resolved; if it exists, the note goes to `knowledge/files/{file_id}.md` (keyed by inode); if not, the note falls back to `facts.md` as `{path}: {note} {provenance}`.

**The example per-file note file** (`knowledge/files/2050-123456.md`):

```markdown
# /repo/src/foo.py

- This file uses the `nagent-tags` parser for structured-tag handling. [from: 2026-06-12-investigate-parser, 2026-06-12]
- The file_id_for_path is `2050:123456` (stable across renames within the same filesystem). [from: 2026-06-12-file-id-pattern, 2026-06-12]
- Don't introduce new tags without updating both the prompt and the parser. [from: 2026-06-12-tag-discipline, 2026-06-12]
```

**The injection point** (per `bin/nagent:673-675`):

```python
file_knowledge = load_context_file(file_knowledge_path(root, file_edit_id)) if file_edit_id else ""
if file_knowledge:
    file_edit_detail_block += f"\n{{file-knowledge}}\n{file_knowledge}\n{{/file-knowledge}}\n"
```

The per-file knowledge is injected *as part of the file-edit block*, in the stable position (per §5). When a file is in scope for editing, its knowledge comes back automatically.

**The relevance to Manual Slop.** This is the per-file memory dimension that v1 of the nagent review didn't fully capture. nagent's per-file memory is now (a) per-file conversation + (b) per-file knowledge notes. Manual Slop has (a) curation memory (FileItem + ContextPreset) but not (b).

### 4.5 The size limits (the budgets)

| Constant | Value | Why |
|---|---|---|
| `SUMMARIZE_THRESHOLD_BYTES` | 64 KB | Files > 64KB get summarized first (cascade to `nagent-file-summarize`); smaller files are sent in full |
| `MAX_HARVEST_SOURCE_BYTES` | 1 MB | Files > 1MB are kept (not harvested); budget guard against huge conversations |
| `DIGEST_MAX_BYTES` | 4 KB | The bounded digest size; truncates with "(truncated; see the category files for the rest)" |
| `HARVEST_MAX_ATTEMPTS` | 2 | Retry budget on parse failure (JSON malformed) |
| `READ_SPLIT_THRESHOLD_BYTES` | 64 KB | The inline read threshold (same as summarize threshold; shared constant) |

**The cascade pattern.** Inline reads at 64KB cascade to `nagent-file-split` (the splitter). Files > 64KB in harvest cascade to `nagent-file-summarize` (the summarizer). The thresholds are *the same* (64KB) because the same pipeline serves both purposes.

**The "too-large" branch** (the budget guard):

```python
if artifact.size_bytes > MAX_HARVEST_SOURCE_BYTES:
    entries[sha] = {
        "path": str(artifact.path),
        "status": "too-large",
        "at": timestamp,
        "deleted": False,
    }
    emit(f"kept (too large): {label}")
    continue
```

The user sees "kept (too large)" in the progress output. The file is NOT deleted; it's just not harvested. The ledger records the reason. Next `nagent-gc` run, if the file is still too large, it's kept again. (Or the user manually splits/patches it first.)

### 4.6 The test surface

`tests/test_nagent_gc.py` (27,306 bytes) is the test file. It covers:

- `scan_root` classification (every klass path)
- `merge_harvest` per-category appends + provenance
- `regenerate_digest` ordering + truncation + section ordering
- `parse_harvest_json` strict validation + code-fence tolerance
- `harvest_conversation` retry budget
- The "already harvested" path (no re-distill)
- The "too-large" path
- The "deferred" path
- The graceful-failure path
- The per-file notes branch (file exists vs not)
- The ledger save/load
- The CLI: dry-run vs --apply vs --no-harvest
- The file-id stability (renames don't break the index)

**The CLI behavior table** (the testable surface):

| Args | Effect | Test |
|---|---|---|
| (no args) | Dry-run; print classification + cost estimate | yes |
| `--apply` | Mutate: harvest + reclaim | yes |
| `--apply --no-harvest` | Mutate: reclaim only, skip LLM | yes |
| `--max-harvest-bytes N` | Cap the LLM input this run; defer the rest | yes |
| `--root PATH` | Use a custom nagent root | yes |
| `--provider P` | Override the LLM provider | yes |
| `--model M` | Override the LLM model | yes |
| `--json` | Print the report as JSON instead of plain text | yes |
| (any) + missing root | Error: nagent root not found | yes |
| (any) + missing credentials | Error: missing credentials for provider | yes |
| (any) + missing package | Error: Python package 'X' is required | yes |

### 4.7 The Manual Slop implementation outline

**The proposed Manual Slop architecture** (mirroring nagent, with Manual Slop conventions):

| nagent | Manual Slop | Where in Manual Slop |
|---|---|---|
| `~/.nagent/knowledge/` | `~/.manual_slop/knowledge/` | (new) |
| `bin/nagent-gc` CLI | `python -m src.knowledge_harvest` (CLI entry) | `src/knowledge_harvest_cli.py` (new) |
| `bin/helpers/nagent_gc_lib.py` | `src/knowledge_store.py` (the library) | `src/knowledge_store.py` (new) |
| `prompts/harvest-conversation.md` | `src/knowledge_prompts/harvest-conversation.md` (user-editable, root-first resolution) | (new; root at `~/.manual_slop/prompts/`) |
| `bin/nagent` injecting `{knowledge}` | `aggregate.py:run` injecting `{knowledge}` at the stable position | (modify `src/aggregate.py`) |
| `bin/nagent` injecting `{file-knowledge}` | (NEW) `aggregate.py:run` injecting `{file-knowledge}` per FileItem | (modify `src/aggregate.py` + add `FileItem.notes`) |
| The 7 categories | The 7 categories (same) | (mirror) |
| The 4-section digest (Open tasks, Open questions, Decisions, Facts, Playbooks) | Same 4-section digest | (mirror) |
| sha256-of-content ledger | sha256-of-content ledger (using `hashlib`) | (use stdlib) |
| `Result[T, ErrorInfo]` envelope | `Result[T, ErrorInfo]` envelope (per `data_oriented_error_handling_20260606`) | (existing convention) |

**The proposed schema changes:**

| File | Change | Type |
|---|---|---|
| `src/models.py` | Add `FileItem.notes: str = ""` | additive; non-breaking |
| `src/aggregate.py` | Add `{knowledge}` block injection in stable position | additive |
| `src/aggregate.py` | Add `{file-knowledge}` block injection per FileItem | additive |
| `src/ai_client.py` | (no change; the LLM call path is unchanged) | n/a |
| `src/knowledge_store.py` | NEW: `KnowledgeStore`, `KnowledgeHarvester`, `KnowledgeDigest` | new module |
| `src/knowledge_harvest_cli.py` | NEW: CLI wrapper, mirror of `bin/nagent-gc` | new file |
| `src/paths.py` | Add `knowledge_dir()` helper | additive |
| `src/gui_2.py` | NEW: "Knowledge" panel (parallel to Logs Management) | new panel |

**The recommended phasing** (for a Candidate 11 track):

| Phase | Scope | Tasks |
|---|---|---|
| 1 | Foundation | `KnowledgeStore` + digest regeneration + 5 unit tests; `paths.py` helper; `FileItem.notes` |
| 2 | Harvester | `KnowledgeHarvester` + harvest-conversation prompt + 8 unit tests; CLI wrapper; ledger save/load |
| 3 | Injection | `aggregate.py:run` `{knowledge}` block + `{file-knowledge}` per FileItem + 3 integration tests; the stable-to-volatile ordering (§5) |
| 4 | GUI | "Knowledge" panel + browse/edit/prune + 4 live_gui tests; cache health display |
| 5 | File notes (sub-candidate 11.1) | `FileItem.notes` field + Structural File Editor "Notes" text area + 2 unit tests; per-file `knowledge/files/{file_id}.md` writer |
| 6 | Docs + archive | `docs/guide_knowledge_curation.md`; `conductor/code_styleguides/knowledge_artifacts.md`; track archive |

**Effort.** Large. 5-6 phases, 6-8 weeks. The foundation is well-defined (the nagent source is the spec); the risk is in the GUI surface design (the "Knowledge" panel needs careful UX).

**Recommended priority.** **HIGH** (re-ranks from MEDIUM in v1 to HIGH in v2.3 because of the user's explicit framing — the 4 memory dimensions are now formally the project's design).

### 4.8 The key design properties (the "why this matters" summary)

| Property | How nagent implements it | Why it matters for Manual Slop |
|---|---|---|
| **Provenance** | Every bullet has `[from: conversation, date]` | Auditable, traceable; user can verify the source of any "fact" |
| **User-editable** | The category files are plain markdown, not a vector store | User can correct wrong "facts" before any model sees them; matches existing FileItem/ContextPreset pattern |
| **Bounded digest** | 4KB cap; truncates with a visible "(truncated; see the category files for the rest)" | Caching-friendly (stable prefix); context-budget-friendly |
| **Delete to turn off** | `rm digest.md` → no injection | Zero-config opt-out; the file is the switch |
| **sha256 ledger gate** | Deletion requires proof of harvest | Lossless: cannot delete a conversation that hasn't been distilled |
| **Dry run default** | `nagent-gc` without `--apply` does nothing destructive | Safe by default; explicit opt-in for mutation |
| **Per-file mirror** | `knowledge/files/{file_id}.md` keyed by inode | Per-file knowledge becomes first-class (extends FileItem curation memory) |
| **Digest regenerates from category files** | Edits to category files propagate to digest on next regen | The "knowledge" is a layer, not a snapshot; the user is the editor |
| **Errors as data** | The harvest-failed path writes to the ledger; the user sees it | Matches the `data_oriented_error_handling_20260606` convention; no exceptions swallowed |

**The pattern summary.** Knowledge harvest is a *layer*, not a snapshot. The category files are the source of truth; the digest is a projection; the ledger is the audit log. The user is the editor. Errors are data, not exceptions. The LLM is a transformation over the data, not a stateful singleton.

---

## 5. The cache strategy in detail

The cache strategy spans two new patterns: stable-to-volatile ordering (the existing mechanism, formalized) and cache TTL GUI controls (the new UX gap). This section is the deep-dive.

### 5.1 Stable-to-volatile context ordering (the formalization)

**The block order** (from `bin/nagent:691-745`, the full layer stack):

```
<initial_context>
  NAGENT_PREAMBLE
  role_instructions
  [tag list with inline per-tag guidance]   <-- the protocol
  [protocol rules]                          <-- raw bodies, first-close-wins, etc.
  [context management rules]                <-- each nagent has own private conversation
  [conversations-are-data rules]           <-- reuse workers, resume, author briefings, hand off
  file_edit_rules
  {tools_block}                             <-- self-describing tools
  {install_context_block}                   <-- context.yaml from the nagent folder
  {project_context_block}                   <-- context.yaml from the git toplevel
  {root_context_block}                      <-- ~/.nagent/context.yaml
  {knowledge_block}                         <-- ~/.nagent/knowledge/digest.md (if exists)
  {file_edit_detail_block}                  <-- per-file history + summary + per-file knowledge
Instance:                                   <-- VOLATILE
  - invocation
  - conversation
  - parent conversation
  - file_edit details
Environment:                                <-- VOLATILE
  - nagent root
  - nagent process
  - cwd
  - host (uname)
  - git context
</initial_context>
```

**The cache boundaries** (computed per-conversation, per `bin/nagent:970-987`):

| Boundary | Where | Why |
|---|---|---|
| Boundary 1 | `\nInstance:` offset (the start of the volatile section) | Shared byte-for-byte across conversations of the same mode and root |
| Boundary 2 | End of the `<initial_context>` block | Stable across every turn of *this* conversation |

**The boundary-to-cache-control flow** (per `bin/helpers/nagent_llm.py:cache_prefix_blocks`):

```
[Q:cache_boundaries?]
   │
   ├── no ──► [T:return message as-is]
   │
   ▼
[Q:at least 1 valid boundary? (0 < b < len(message), deduplicated, max 3)]
   │
   ├── no ──► [T:return message as-is]
   │
   ▼
[loop: each boundary point in sorted order]
   │
   ├──► [I:append block {type: text, text: message[start:point], cache_control: {type: ephemeral}}]
   │
[I:append final block {type: text, text: message[start:]}]
   │
[T:return blocks]
```

**The Anthropic-specific call** (per `bin/helpers/nagent_llm.py:generate_text_with_usage`):

```python
if provider == "anthropic":
    anthropic = require_package(provider)
    client = anthropic.Anthropic()
    response = client.messages.create(
        model=model,
        max_tokens=8192,
        messages=[{"role": "user", "content": cache_prefix_blocks(message, cache_boundaries)}],
    )
    return _result_with_usage(_anthropic_text(response), getattr(response, "usage", None), message)
```

**The accounting fold-back** (per `_result_with_usage`):

```python
input_tokens = _usage_value(usage, "input_tokens", "prompt_tokens", "prompt_token_count")
# Anthropic reports cached prompt tokens separately; fold them back in so
# input_tokens stays "tokens sent" across providers. Other providers lack
# these fields and contribute zero.
input_tokens += _usage_value(usage, "cache_read_input_tokens")
input_tokens += _usage_value(usage, "cache_creation_input_tokens")
output_tokens = _usage_value(usage, "output_tokens", "completion_tokens", "candidates_token_count", "output_token_count")
```

**The "3 prefix blocks" cap.** The Anthropic API allows 4 cache breakpoints per request. nagent uses 2 (the 2 boundaries) but caps at 3 prefix blocks for safety. The cap is hardcoded in `cache_prefix_blocks`.

**The codepath diagram (SSDL):**

```
[Q:conversation_file]
   │
   ▼
[I:read_text(encoding="utf-8")]
   │
   ▼
[I:find_block_span(<initial_context>)]
   │
   ▼
[Q:span found AND span[0] == 0?]
   │
   ├── no ──► [I:boundaries = []]
   │
   ▼
[I:boundaries = []]
[I:offset = text.find("\nInstance:", span[0], span[1])]
[Q:offset > 0?]
   │
   ├── yes ──► [I:boundaries.append(offset)]
   │
   ▼
[Q:span[1] < len(text)?]
   │
   ├── yes ──► [I:boundaries.append(span[1])]
   │
   ▼
[loop: each boundary]
   │
   ├──► [I:command.extend(["--cache-prefix-chars", str(boundary)])]
   │
   ▼
[I:subprocess.run(nagent-llm-text --file conversation --cache-prefix-chars N1 --cache-prefix-chars N2 --json --provider anthropic --model claude-sonnet-4-6)]
   │
   ▼
[I:parse response JSON]
   │
   ▼
[I:return (text, input_tokens, output_tokens)]
```

**The Anthropic max_tokens:** `8192` (hardcoded in `generate_text_with_usage` for the Anthropic branch). This is the *response* max tokens, not the *context* max tokens. The context is the full message + the cache blocks.

### 5.2 The stable-to-volatile codepath in `aggregate.py:run` (Manual Slop target)

**The current Manual Slop state.** `src/aggregate.py:run` (per the v1 review and the v2.3 reading of the 518-line module) builds the initial context from:
- `self.context_files` (the active preset's FileItems)
- `self.aggregation_strategy` (full / summarize / skeleton / sig / def / agg)
- The discussion metadata
- The history (per `_reread_file_items`)
- The tool descriptions
- The persona profile
- The project context (if any)

The current layer order is **not** formally enforced as stable-to-volatile. The order is implementation-defined, not policy-defined.

**The proposed Manual Slop layer order** (the re-architect):

| # | Layer | Stable? | Source |
|---|---|---|---|
| 1 | Role instructions (model + provider) | yes | `_get_combined_system_prompt` |
| 2 | Tag protocol / function-calling schema | yes | per provider |
| 3 | Discovered tool descriptions | yes | `mcp_client.get_tool_schemas()` |
| 4 | System prompt (user's chosen preset) | yes | `app_state.ai_settings.system_prompt` |
| 5 | Persona profile (if any) | yes | `app_state.active_persona` |
| 6 | Project context (per `manual_slop.toml`) | yes | NEW (Candidate 14) |
| 7 | Knowledge digest (if Candidate 11 built) | yes (within gc cycle) | NEW |
| 8 | Discussion metadata (name, role count) | no (per turn) | `disc_entries[:1]` or `disc_meta` |
| 9 | Active preset (FileItem set) | no (per turn) | `self.context_files` |
| 10 | Per-file details (history, slices, notes) | no (per file) | per FileItem |
| 11 | Tool-call results from prior turns | no (per turn) | per `_reread_file_items` |
| 12 | The user message | no (per turn) | the input |

**The cache boundary** would be at layer 7/8 (the last stable layer). The Anthropic-specific call site in `_send_anthropic` would need to:
- Compute the cache boundary (character offset where layer 8 starts)
- Pass the boundary to `cache_prefix_blocks` (or its Manual Slop equivalent)
- Wrap the message in `content` blocks with `cache_control: {"type": "ephemeral"}` on the prefix

**The risk.** The current `_send_anthropic` likely uses some form of cache_control (per the function summary), but the *boundaries* are not aligned with the *stable layers*. The win comes from re-aligning the boundaries; the loss is if the re-alignment breaks existing tests.

**The mitigation.** A measurement pass before the re-architect: log the cache hit rate over a sample of representative discussions. The baseline. The post-architect rate should be measurably higher.

### 5.3 The cache TTL GUI controls (the new UX gap)

**The Manual Slop current state.** The user has *no GUI exposure* of:
- Which discussions are currently cached
- The TTL of each cached discussion
- The cache hit rate per provider
- A way to invalidate a specific discussion's cache
- A way to disable caching globally or per-discussion

**The provider-specific defaults:**

| Provider | Cache type | Default TTL | Configurable? | Cost implication |
|---|---|---|---|---|
| Anthropic | ephemeral | 5 min (per-request, non-extendable) | No (provider-controlled) | +25% per cached token (write), +10% per cached token (read) |
| Google (Gemini) | explicit | 1 h (default) | Yes (via `ttl` field) | Free below 1M tokens/hour; storage cost above |
| OpenAI | implicit (auto) | 5-10 min (provider-managed) | No | Free |
| Cursor | (provider-managed) | varies | No | varies |
| claude-code (via Claude Agent SDK) | (provider-managed) | varies | No | varies |

**The proposed GUI surface** (an Operations Hub sub-panel, parallel to the planned Vendor State tab):

```
+------------------------------------------------------+
| Caching                                              |
+------------------------------------------------------+
| Provider summaries                                   |
| [Anthropic]   in:340 cache:80  hit:23%  ttl:4:32   |
| [Gemini]      in:120 cache:0   hit:0%   ttl:0:00   |
| [OpenAI]      in:560 cache:200 hit:35%  ttl:n/a    |
+------------------------------------------------------+
| Active discussions                                   |
| Discussion "refactor auth"                           |
|   cached: yes (Anthropic)                            |
|   expires: 2026-06-12T15:32 (in 4:32)                |
|   [Invalidate cache] [Disable caching for this]      |
| Discussion "fix the parser"                           |
|   cached: no                                         |
|   [Enable caching for this]                         |
+------------------------------------------------------+
| Global settings                                      |
|   [X] Enable Anthropic ephemeral caching             |
|   [X] Enable Gemini explicit caching                 |
|   [ ] Allow >1h Gemini caches (charges may apply)    |
|   Anthropic default TTL: [5 min v]                   |
|   Gemini default TTL:    [60 min v]                  |
+------------------------------------------------------+
```

**The data sources:**

| Widget | Data source | Frequency |
|---|---|---|
| `in:N cache:N hit:N%` | `ai_client.get_token_stats()` (already exported per the v1 review) | per turn (or per session) |
| `ttl:4:32` | `ai_client._send_<provider>` usage metadata (`cache_creation_input_tokens` + `cache_read_input_tokens` + `cache_expiry` if Anthropic returns it) | per turn |
| `cached: yes/no` | per-discussion flag (NEW; Manual Slop tracks which discussions have active caches) | per discussion |
| `[Invalidate cache]` | calls `ai_client._invalidate_cache(discussion_id)` (NEW) | on click |

**The new AI client state:**

```python
# In src/ai_client.py (NEW)
@dataclass
class DiscussionCacheState:
    discussion_id: str
    provider: str
    cached_at: datetime
    expires_at: Optional[datetime]  # None for OpenAI implicit
    hit_count: int = 0
    tokens_cached: int = 0
    last_invalidated_at: Optional[datetime] = None
    caching_enabled: bool = True   # user can disable per-discussion

# In AppController (NEW)
self.discussion_caches: dict[str, DiscussionCacheState] = {}  # keyed by discussion_id
```

**The Hook API additions** (per the `api_hooks.py` design):

```
GET  /api/cache                        # list all discussion cache states
GET  /api/cache/<discussion_id>        # get one
POST /api/cache/<discussion_id>/invalidate
POST /api/cache/<discussion_id>/disable
POST /api/cache/<discussion_id>/enable
```

**The proposed phasing** (for a Candidate 12b track):

| Phase | Scope | Tasks |
|---|---|---|
| 1 | Telemetry | Hook the Anthropic `cache_creation_input_tokens` + `cache_read_input_tokens` from `_send_anthropic`; expose via `get_token_stats` |
| 2 | State | Add `DiscussionCacheState` + `app_controller.discussion_caches` + the per-discussion tracking |
| 3 | GUI | "Caching" Operations Hub sub-panel + the 3 widgets + the [Invalidate] button |
| 4 | Hook API | The 5 endpoints |
| 5 | Tests | 4 live_gui tests + 3 unit tests for the cache state machine |

**Effort.** Medium. 4-5 phases, 3-4 weeks.

**Recommended priority.** **MEDIUM** (the user explicitly asked for this; not as urgent as Candidate 11, but the value is real).

### 5.4 The "stable layers" discipline (the design rule)

**The discipline** (codified as a styleguide, not enforced as code):

1. **Identify the stable layers** in your context builder. A layer is stable if its content is byte-identical across turns of the same conversation.
2. **Identify the volatile layers.** A layer is volatile if its content changes per turn.
3. **Order them stable-to-volatile.** Stable first, volatile last.
4. **Pass the boundary to the LLM call.** For Anthropic, this is the cache boundary; for Gemini, the `cachedContent` resource; for OpenAI, the implicit prefix.
5. **Verify the ordering** with a byte-comparison test: render the context for turn 1, render it for turn 2, assert the first N characters are identical.
6. **Measure the cache hit rate** before and after; expect a measurable improvement.

**The byte-comparison test (the design contract):**

```python
# In tests/test_aggregate_caching.py (NEW)
def test_aggregate_stable_to_volatile_ordering():
    """The first N characters of the context should be identical across turns
    of the same conversation, when no stable-layer inputs change."""
    ctrl = mock_app_controller()
    ctrl.ai_settings.system_prompt = "Test system prompt"
    ctrl.active_persona = mock_persona()

    # Turn 1
    turn1 = aggregate.build_initial_context(ctrl, user_message="first prompt")

    # Turn 2 (same stable inputs, different user message)
    turn2 = aggregate.build_initial_context(ctrl, user_message="second prompt")

    # The first N characters should be identical (N = where the volatile layers start)
    N = aggregate.stable_prefix_length(ctrl)
    assert turn1[:N] == turn2[:N], f"Stable prefix mismatch: {turn1[:N]!r} != {turn2[:N]!r}"
```

**The test is the contract.** It encodes the design rule as a runnable assertion.

### 5.5 The cross-cutting RAG caveat

**The interaction with RAG.** RAG results are volatile (per turn; the user's question changes the search query). The stable-to-volatile boundary is at layer 7/8; RAG results are below the boundary (volatile). The cache is *not* invalidated by RAG changes.

**The interaction with knowledge digest.** The digest is stable across the same `gc` cycle. A new `nagent-gc --apply` regenerates the digest; the cache is invalidated for the next turn. The user has a way to force cache invalidation if needed (the `[Invalidate cache]` button).

**The interaction with file edit details.** Per `bin/nagent:659-675`, the file_edit detail block is appended *inside* the `<initial_context>` (in the stable position). This is correct: for the same `nagent-file-edit --file <path>` invocation, the per-file details are stable.

### 5.6 The Manual Slop implementation outline (for Candidate 12a + 12b)

**The proposed Manual Slop changes:**

| File | Change | Type |
|---|---|---|
| `src/aggregate.py:run` | Reorder the layer stack stable-to-volatile; add `stable_prefix_length()` helper | refactor |
| `src/ai_client.py:_send_anthropic` | Compute the stable prefix; pass to `cache_prefix_blocks` analogue; wrap in `cache_control` | refactor |
| `src/ai_client.py:_send_gemini` | Add explicit `cachedContent` resource creation; use the stable prefix | additive |
| `src/ai_client.py:get_token_stats` | Add `cache_creation_input_tokens` and `cache_read_input_tokens` per Anthropic usage; fold into the input_tokens total | additive |
| `src/ai_client.py` (NEW) | `DiscussionCacheState` dataclass; `discussion_caches` registry; `invalidate_cache(discussion_id)` | new |
| `src/app_controller.py` | Per-discussion cache tracking; the disable/enable flag | additive |
| `src/gui_2.py` | "Caching" Operations Hub sub-panel (per the ASCII sketch above) | new panel |
| `src/api_hooks.py` | The 5 new endpoints | additive |
| `tests/test_aggregate_caching.py` | The byte-comparison contract test | new |
| `tests/test_cache_state.py` | The cache state machine tests | new |
| `tests/test_gui_caching.py` | The live_gui tests for the panel | new |
| `docs/guide_caching_strategy.md` | The new docs | new |
| `conductor/code_styleguides/cache_friendly_context.md` | The new styleguide | new |

**The recommended phasing** (for a Candidate 12a + 12b combined track):

| Phase | Scope | Tasks |
|---|---|---|
| 1 | Refactor | `aggregate.py` re-ordering + `stable_prefix_length()` + the byte-comparison test |
| 2 | Anthropic cache | `_send_anthropic` cache control blocks at the stable boundary + 2 unit tests |
| 3 | Gemini cache | `_send_gemini` explicit `cachedContent` + 2 unit tests |
| 4 | Token accounting | `cache_creation_input_tokens` + `cache_read_input_tokens` fold-back + 1 unit test |
| 5 | State + GUI | `DiscussionCacheState` + the Operations Hub sub-panel + 3 live_gui tests |
| 6 | Hook API | The 5 endpoints + 1 live_gui test |
| 7 | Docs | The 2 new docs |

**Effort.** Medium. 6-7 phases, 4-5 weeks.

**Recommended priority.** **MEDIUM** (the user explicitly asked for it; not as urgent as Candidate 11, but the value is real).

---

## 6. The compaction pattern in detail

Compaction is the *rewrite-in-place* sibling of summarization. This section is the deep-dive.

### 6.1 The difference from summarization

| Aspect | Summarization | Compaction |
|---|---|---|
| **What it does** | Calls the LLM to produce a shorter text; replaces the original | Calls the LLM to *rewrite* the conversation in place; preserves the structure |
| **Output shape** | A single string (the LLM's response) | A `list[dict]` with the same role/content/collapsed shape as the original; OR a structured markdown with the 12 sections from the prompt |
| **Failure mode** | Original destroyed on LLM failure (potentially) | Original preserved on any failure (graceful) |
| **Self-review** | None | 10-question checklist (the contract) |
| **Guidance** | None (free-form) | Editable `prompts/compact-conversation.md` (root-first resolution) |
| **Token count** | Strict reduction (the LLM is told "be shorter") | Substantial reduction (the 12 sections force a structured compression) |
| **Capability preservation** | Partial (chronology → prose; structure lost) | Full (the 12 sections preserve decisions, facts, failures, references) |

**The key insight.** Compaction is *data-oriented*: the output is a *shape* (the 12 sections), not a *text*. The LLM is the transformation; the 12 sections are the target shape. A summarization is a transformation to *a text*; a compaction is a transformation to *a structure*.

### 6.2 The 12-section output structure

| # | Section | What it contains | Why it matters |
|---|---|---|---|
| 1 | User Intent | What the user originally asked for | The "why" of the conversation |
| 2 | Current Objective | Where we are now (sub-goal) | The "where" of the current turn |
| 3 | Accepted Decisions | Decisions that were made (with reasons) | The decision log |
| 4 | Constraints | Hard constraints that were discovered | The "can't" set |
| 5 | Durable Knowledge > Global | Cross-cutting knowledge | The "global" knowledge base |
| 6 | Durable Knowledge > Artifact Local | Per-file/per-artifact knowledge | The "local" knowledge base |
| 7 | Durable Knowledge > Repository History | Git history knowledge | The "where this came from" knowledge |
| 8 | Durable Knowledge > Historical Coupling | Co-edit knowledge | The "what's related" knowledge |
| 9 | Verified Facts | Things that were proven | The "evidence" knowledge |
| 10 | Important Failed Attempts | Things that were tried and failed | The "don't repeat this" knowledge |
| 11 | Open Questions | Questions still unanswered | The "still TODO" knowledge |
| 12 | TODO | Concrete next steps | The "what's next" knowledge |
| 13 | Minimal Context Needed To Continue | The smallest context that would let another worker continue | The "hand-off" knowledge |

13 sections, but the README collapses sections 5-8 under "Durable Knowledge" (4 sub-sections). The total is 12 + 1 (the Minimal Context) for the actual structure.

**The shape is *deliberate*.** It forces the compactor to separate:
- **State** (decisions, facts, failures, references) from **flow** (chronology, exploration)
- **Global** knowledge from **local** knowledge
- **What's known** (facts) from **what's needed** (questions, TODO)
- **What worked** (decisions) from **what didn't** (failures)

### 6.3 The 10-question self-review (the contract)

The contract for "is this compaction successful?" is the 10 yes/no questions at the end of the prompt. The compactor must answer them; if any answer is "no," continue compacting.

| # | Question | Verifies | Test |
|---|---|---|---|
| 1 | Can another worker continue immediately? | preserved capability | live_gui: spawn a worker after compaction, see if it succeeds |
| 2 | Would expensive investigation need to be repeated? | preserved artifacts | unit: same conversation, before/after compaction, run a "did we keep the conclusion?" check |
| 3 | Are accepted decisions preserved? | decision retention | unit: extract decisions from the original, check they're in the compacted |
| 4 | Are constraints preserved? | constraint retention | unit: extract constraints from the original, check they're in the compacted |
| 5 | Are important failures preserved? | failure retention | unit: extract failures from the original, check they're in the compacted |
| 6 | Are artifact references preserved? | ref retention | unit: extract file paths and tool names from the original, check they're referenced |
| 7 | Has duplicated information been removed? | dedup | manual review (or a diff between the original and compacted) |
| 8 | Has chronology been replaced with state? | state vs flow | manual review (the 12 sections are state; the absence of "at 3pm on Tuesday" prose is the test) |
| 9 | Is the conversation substantially smaller? | compression | unit: assert the compacted size is < 50% of the original |
| 10 | Is future capability unchanged or improved? | outcome preservation | live_gui: same task, before/after compaction, see if the worker succeeds at the same level |

**The test suite** (recommended for the Manual Slop implementation):

| Test | Verifies |
|---|---|
| `test_compact_preserves_decisions` | Question 3 |
| `test_compact_preserves_constraints` | Question 4 |
| `test_compact_preserves_failures` | Question 5 |
| `test_compact_preserves_artifact_refs` | Question 6 |
| `test_compact_removes_duplicates` | Question 7 |
| `test_compact_replaces_chronology_with_state` | Question 8 (manual review) |
| `test_compact_substantially_smaller` | Question 9 |
| `test_compact_preserves_capability` | Question 10 |
| `test_compact_12_section_structure` | the 12-section shape |
| `test_compact_continues_until_passes` | the iterative compacting (retry on self-review fail) |

### 6.4 The codepath in nagent

The `compact_conversation` function (from `bin/nagent:1975-2019`) is implemented as a call to `edit_conversation` with a specific prompt. `edit_conversation` is the general-purpose "edit a conversation file" codepath; compaction is one of its callers.

The flow:
1. Read the compaction guidance from `compact_prompt_path(root)` (root-first; user override)
2. Construct the prompt: `f"{compact_guidance}\n\nCompact this conversation now, following the guidance above. Rewrite it in place so it is substantially smaller while preserving future capability."`
3. Call `edit_conversation(conversation_file, root, process_path, invocation, conversation_name, pid, parent_conversation, prompt, provider, model, config_path, file_edit_path, file_edit_id, json_mode, spinner_message="Compacting conversation")`

`edit_conversation` then:
1. Archives the current conversation file (timestamped copy)
2. Runs a file-edit session against the archive with the compaction prompt
3. Validates the result (the 10-question self-review, in the model's judgment)
4. If passes, replaces the original with the compacted result
5. If fails, keeps the original (graceful failure)

### 6.5 The graceful failure (per the `67a3ea5` commit message)

> "Save-conversation indexes the copy even when the summary LLM fails; fresh conversations build initial context once; compact prompt resolves root-first; edit/compact roll up child token stats."

The implication: `--compact` is also graceful. The original is preserved if the LLM fails. The archive is kept (timestamped copy in `~/.nagent/conversations/`) so the user can recover.

### 6.6 The Manual Slop current state

**The Compress button** (`src/gui_2.py:4252`):

```python
imgui.button("Compress")
if imgui.is_item_clicked():
    app_controller._handle_compress_discussion(disc_index)
```

**The handler** (`src/app_controller.py:3357`):

```python
def _handle_compress_discussion(self, disc_index):
    disc_text = self._build_discussion_text(self.disc_entries)
    compressed = ai_client.run_discussion_compression(disc_text)
    self.disc_entries = compressed
    self._save_state()
```

**The compression call** (`src/ai_client.py:run_discussion_compression`):

The function is the LLM call; behavior on LLM failure is **TBD** (Candidate 15 verification).

**The gap.** The Compress button:
- Has no editable prompt (no `prompts/compact-discussion.md`)
- Has no 10-question self-review (no contract)
- Has no 12-section output structure (the LLM is free-form)
- May or may not gracefully fail (TBD)
- Is "Compress" not "Compact" (the labels differ; semantics are different)

**The proposed Manual Slop changes:**

| File | Change | Type |
|---|---|---|
| `src/ai_client.py` (NEW) | `run_discussion_compaction(disc_entries, prompt_path="~/.manual_slop/prompts/compact-discussion.md")` | new |
| `src/ai_client.py` | Keep `run_discussion_compression` (the summarize path); new `run_discussion_compaction` is the rewrite path | additive |
| `src/app_controller.py` (NEW) | `_handle_compact_discussion(disc_index)` (separate from `_handle_compress_discussion`) | new |
| `src/gui_2.py` | New "Compact" button next to "Compress" | additive |
| `src/ai_client.py:prompts/` (NEW) | `compact-discussion.md` (root-first resolution, mirroring nagent's pattern) | new |
| `tests/test_compact_discussion.py` (NEW) | The 10-question self-review test suite | new |
| `docs/guide_caching_strategy.md` | (already in §11 below; compaction section) | new |
| `conductor/code_styleguides/cache_friendly_context.md` | (already in §11) | new |

**The proposed Manual Slop compaction prompt** (adapted from nagent's):

```markdown
# Compact This Discussion

You are not summarizing a chat log. You are maintaining a durable working
artifact. The discussion is a mutable data structure that exists to
support future work. Its purpose is not to preserve chronology. Its
purpose is to preserve capability.

## Core Principle
The agent is not the thing. The data is the thing. Optimize the discussion
for future transformations. Preserve information that would be expensive
to rediscover. Remove information that no longer contributes to future work.

## Data-Oriented Rules

Keep:
  - accepted decisions (User + AI + System roles)
  - user requirements
  - constraints
  - discovered invariants
  - successful experiments
  - important failed experiments
  - file-local knowledge (curation memory; FileItem.view_mode; etc.)
  - project-local knowledge (curation memory; ContextPreset; etc.)
  - discussion-local knowledge (the entry list; the role taxonomy; etc.)
  - open questions
  - TODO items
  - durable context

Remove:
  - repeated reasoning
  - repeated tool output
  - repeated file reads
  - duplicated summaries
  - obsolete hypotheses
  - intermediate exploration
  - dead conversations
  - verbose deliberation
  - chronology that no longer matters

Keep conclusions. Remove exploration. Keep decisions. Remove deliberation.
Keep state. Remove history.

## Transformation Rules

Replace many tool calls with verified outcomes.
Replace long investigations with:
  - conclusion
  - evidence
Replace long discussions with:
  - decision
  - reason
  - rejected alternatives
Merge duplicate investigations. Collapse repeated facts.
Delete obsolete information. Rewrite aggressively.
The discussion is not sacred.

## Preserve Artifact Knowledge

Preserve references to:
  - FileItem paths
  - ContextPreset names
  - Tool invocation arguments
  - RAG query results
  - Knowledge digest entries

Prefer references over duplication.

## Preserve Failure Knowledge

Keep:
  - failed experiments
  - rejected designs
  - dangerous edge cases
  - corrected assumptions

Future workers should not repeat expensive mistakes.

## Required Output Structure

# User Intent
# Current Objective
# Accepted Decisions
# Constraints
# Durable Knowledge
## Global
## Artifact Local
## Project Local
## Repository History
## Historical Coupling
# Verified Facts
# Important Failed Attempts
# Open Questions
# TODO
# Minimal Context Needed To Continue

## Explicit Instructions

Do not preserve chronology. Preserve state.
Do not preserve discussion flow. Preserve useful information.
Do not preserve intermediate tool behavior. Preserve durable artifacts.
If ten pages can become one paragraph without reducing future capability, do so.
If an investigation can be represented as a fact, store the fact.
If a discussion can be represented as a decision, store the decision.
If repeated information exists, keep the best version.

## Self Review

Before finishing, verify:
  - Can another worker continue immediately?
  - Would expensive investigation need to be repeated?
  - Are accepted decisions preserved?
  - Are constraints preserved?
  - Are important failures preserved?
  - Are artifact references preserved?
  - Has duplicated information been removed?
  - Has chronology been replaced with state?
  - Is the discussion substantially smaller?
  - Is future capability unchanged or improved?

If not, continue compacting.
```

The Manual Slop version is adapted: it adds the **project-local** knowledge dimension (parallel to the global + artifact-local) because Manual Slop has 4 memory dimensions (curation, discussion, RAG, knowledge) and the compaction needs to preserve all 4.

### 6.7 The recommended phasing (for a Candidate 13 track)

| Phase | Scope | Tasks |
|---|---|---|
| 1 | Foundation | `run_discussion_compaction` function + `compact-discussion.md` prompt + 4 unit tests |
| 2 | GUI | New "Compact" button + `_handle_compact_discussion` + the Compactor panel |
| 3 | Self-review tests | The 10-question test suite (10 tests) |
| 4 | Hook API | `POST /api/discussion/<id>/compact` endpoint |
| 5 | Live_gui | 3 live_gui tests (compaction preserves decisions, fails gracefully, compresses substantially) |
| 6 | Docs | The compact-discussion.md prompt documentation + the caching strategy doc section |

**Effort.** Small to medium. 5-6 phases, 2-3 weeks.

**Recommended priority.** **MEDIUM** (the value is real; the cost is bounded by the test-driven nature of the self-review).

---


## 7. nagent architecture (the 4 reading levels + the protocol + the state model)

nagent's architecture is small (2,524 lines for `bin/nagent`, plus 9 helper files, plus 9 executables). The architecture is *self-describing* (per the CLAUDE.md reading order) and *layered* (per the bin/helpers split). This section is the deep-dive.

### 7.1 The 4 reading levels

Per `CLAUDE.md:52-58` and the README's "Architecture" section:

| # | Level | File | Lines | What it does |
|---|---|---|---|---|
| 1 | Main loop | `bin/nagent` | 2,524 | The bulk of the system; the 5-step reading path `main() → run_agent_loop() → call_llm() → parse_response() → process_tags()` |
| 2 | Library | `bin/helpers/*_lib.py` (8 files) | ~50,000 | Real logic; thin wrappers in `bin/`, real logic in `helpers/` |
| 3 | CLI front-ends | `bin/nagent-*` (9 files) | ~40,000 | Each is a thin wrapper that calls the library; each handles `--description` for tool discovery |
| 4 | Tests | `tests/test_*.py` (7 files) | ~200,000 | Executable specs; stdlib unittest only; no third-party test framework |

**The reading order** (per `CLAUDE.md:55-57`): *"Read this path first: `main() → run_agent_loop() → call_llm() → parse_response() → process_tags()`. The loop appends to a conversation file, sends the whole file to the LLM, parses structured tags, runs handlers, appends results, and repeats until a final `<nagent-response>` is emitted."*

**The library catalog** (`bin/helpers/`):

| Library | Lines | What it does |
|---|---|---|
| `nagent-cli.py` | 2,642 | `exit_on_description()` (the `--description` self-describing pattern); `collect_bin_tool_descriptions()` (iterates `bin/` and runs `--description` on each); `WaitSpinner` (animated spinner with `enabled` flag for non-TTY) |
| `nagent-llm.py` | 20,366 | Provider abstraction; `generate_text_with_usage()` is the single primitive (file in → text out) for `openai`, `anthropic`, `google`, `cursor`, `claude-code` |
| `nagent-tags.py` | 6,036 | Explicit parser for the tag protocol (replaces regex parsing); `TagNode` dataclass; `parse_tag_document`, `parse_element`, `find_block_span`, `extract_block`, `replace_first_block`, `remove_first_block`, `unwrap_whole_element` |
| `nagent-gc-lib.py` | 27,289 | The knowledge harvest library: `scan_root`, `harvest_conversation`, `merge_harvest`, `regenerate_digest`, `load_ledger`, `save_ledger`, `parse_harvest_json`, `build_harvest_prompt` |
| `nagent-file-edit-lib.py` | 5,232 | `file_id_for_path(path) -> "{st_dev}:{st_ino}"`; the per-pid registry; the per-file conversation index |
| `nagent-file-split-lib.py` | 15,427 | `SPLIT_TYPES` (12 languages); per-language `SCORE_BY_TYPE`; `index.json` writer; `source_sha256()` |
| `nagent-file-patch-lib.py` | 5,086 | `validate_index` (strict hash check); `merge_segments`; `make_unified_patch`; `apply_segment_patches` |
| `nagent-file-summarize-lib.py` | 3,884 | `SUMMARY_MAX_ATTEMPTS = 2`; `summarize_content` (per-segment LLM call with retry); `combined_summary_from_index` |
| `nagent-file-split-{py,cpp,js,ts,json,yaml,md,xml,txt,go,rs,java}` | 12 files × ~225B = ~2,700 | Thin wrappers that call `nagent_file_split_lib.py` with the right SCORE_BY_TYPE |

**Total library code:** ~95,000 lines. **Total CLI front-end code:** ~40,000 lines. **Total test code:** ~200,000 lines. **Total:** ~340,000 lines (excluding the 2,524-line main loop).

**The library / CLI split** (the architectural rule): *"Thin wrappers live in `bin/`; real logic lives in `bin/helpers/*_lib.py`."* This is the same modular-controller pattern Manual Slop uses (e.g., `src/mcp_client.py` vs `src/mcp_client_legacy.py` per the v2.3 reading of the planned `mcp_architecture_refactor`).

### 7.2 The CLI front-ends (the 9 executables)

| Executable | Lines | Args | What it does |
|---|---|---|---|
| `bin/nagent` | 2,524 | many | The main loop |
| `bin/nagent-llm-text` | 50 | `--file PATH [--provider P] [--model M] [--config PATH] [--json]` | Text → LLM (the primitive) |
| `bin/nagent-llm-upload` | 80 | `--file PATH --prompt TEXT [--provider P] [--model M] [--json]` | File + prompt → LLM (the upload variant) |
| `bin/nagent-file-edit` | 120 | `--file PATH "prompt" [--clear]` | Per-file conversation |
| `bin/nagent-file-patch` | 80 | `--index INDEX [--patch PATH] [--dry-run] [--force] [--json]` | Merge segments, validate, patch |
| `bin/nagent-file-split` | 170 | `--file PATH --output DIR --split TYPE [--summarize] [--refresh] [--target-bytes N] [--natural] [--json]` | Split a large file |
| `bin/nagent-file-summarize` | 100 | `--file PATH [--limit-word-count N] [--output DIR] [--json]` | Summarize inline or via split |
| `bin/nagent-gc` | 150 | `--root PATH [--apply] [--no-harvest] [--max-harvest-bytes N] [--json]` | Knowledge harvest |
| `bin/nagent` (CLI mode) | — | `--status, --list-models, --list-conversations, --clear, --save-conversation NAME, --load-conversation NAME, --branch-conversation NAME, --summarize, --compact, --edit-conversation PROMPT, --file-edit PATH PROMPT, --list-file-edits` | The full maintenance set |

**The `--description` self-describing pattern** (per `bin/helpers/nagent_cli.py:exit_on_description`):

```python
def exit_on_description(description: str) -> None:
    if "--description" in sys.argv:
        print(description)
        raise SystemExit(0)
```

Every executable in `bin/` starts with `exit_on_description(TOOL_DESCRIPTION)`. The main `nagent` calls `collect_bin_tool_descriptions(bin_dir)` at startup:

```python
def collect_bin_tool_descriptions(bin_dir: Path) -> str:
    """Iterates every executable in `bin/`, runs each with `--description`,
    captures stdout (10s timeout per), concatenates into one "Available tools" block."""
    ...
```

The startup prompt includes the concatenated descriptions. A new tool is just `exit_on_description(...)` + the executable bit + `bin/` membership.

### 7.3 The structured-tag protocol (the 8 tags)

**The 8 tags** (from `bin/nagent:696-706`):

| Tag | Self-closing? | Body semantics | Return wrapper |
|---|---|---|---|
| `<nagent-response>{text}</nagent-response>` | no | Human response; ends the turn | `n/a` (turn ends) |
| `<nagent-read path="{file}"/>` | yes | Read a small file (≤ 64KB) inline | `<nagent-read-result path="..." bytes_read="..." content="..."/>` |
| `<nagent-file-read path="{file}"/>` | yes | Read a file of any size; large files are split automatically | `<nagent-file-read-result path="..." segments="..." index="..."/>` (or cascades to `nagent-file-split` + `nagent-file-summarize`) |
| `<nagent-file-patch index="{index}"/>` | yes | Merge edited split segments back into the source file | `<nagent-file-patch-result index="..." patch="..." applied="..."/>` |
| `<nagent-write path="{file}">{content}</nagent-write>` | no | Write to an allowed path | `<nagent-write-result path="..." bytes_written="..."/>` |
| `<nagent-shell>{commands}</nagent-shell>` | no | Run shell commands (with user permissions) | `<nagent-shell-result exit_code="..." stdout="..." stderr="..."/>` |
| `<nagent-next>{prompt}</nagent-next>` | no | Append a prompt to yourself and continue reasoning | `n/a` (loop continues) |
| `<nagent-conversation>{prompt}</nagent-conversation>` | no | Spawn a sub-conversation (default: fresh, isolated) | `<nagent-conversation-result conversation="..." tokens_in="..." tokens_out="..." exit_code="..."/>` |
| `<nagent-conversation conversation-file="name">{prompt}</nagent-conversation>` | no | Continue a named worker | (same wrapper) |
| `<nagent-conversation conversation-name="saved">{prompt}</nagent-conversation>` | no | Resume a saved conversation | (same wrapper) |

**The protocol rules** (from `bin/nagent:708-713`):

```
1. Tag bodies are raw text. Do not escape characters. Never emit a literal
   close tag (e.g. </nagent-shell>) inside a body; the first matching
   close tag ends the body.
2. Emit nothing outside tags. The parser rejects prose, unknown tags,
   and unknown attributes; an invalid turn comes back with a correction
   request.
3. After your action tags run, each result is appended to this
   conversation as a <nagent-*-result> block and you are called again.
   Never fabricate results; act on the appended ones.
4. Multiple tags in one turn run in order.
5. A result with an error status is data: change your approach instead
   of retrying the identical action.
```

**The explicit parser** (per `bin/helpers/nagent_tags.py`):

```python
class TagNode:
    name: str
    attrs: dict[str, str]
    content: str
    self_closing: bool
    start: int
    end: int

def parse_element(text: str, pos: int = 0) -> TagNode:
    """Parse one element starting exactly at text[pos]."""
    ...

def parse_tag_document(text: str) -> list[TagNode]:
    """Parse a whole document: elements separated by nothing but whitespace."""
    ...

def find_block_span(text: str, name: str) -> tuple[int, int] | None:
    """Span of the first literal <name>...</name> block, tags included."""
    ...

def extract_block(text: str, name: str) -> str | None:
    """The first <name>...</name> block including its tags, or None."""
    ...

def replace_first_block(text: str, name: str, replacement: str) -> str:
    """Replace the first <name>...</name> block verbatim (no escape semantics)."""
    ...

def remove_first_block(text: str, name: str) -> str:
    return replace_first_block(text, name, "")

def unwrap_whole_element(text: str, name: str) -> str | None:
    """If the entire text is one <name>...</name> element, return its content."""
    ...
```

**The error type:**

```python
class TagParseError(ValueError):
    def __init__(self, message: str, offset: int) -> None:
        super().__init__(message)
        self.message = message
        self.offset = offset
```

**The protocol contract.** The protocol is XML-ish but NOT XML. The differences:
- Tag bodies are raw text (no entity escaping)
- Elements do not nest
- The first literal close tag ends a body
- A real XML parser would reject valid protocol output

**The `MAX_FORMAT_RETRIES` retry budget.** Per the README, malformed output triggers up to 3 correction turns appended to the conversation. The LLM sees its previous bad output + a `<system>` correction message.

### 7.4 The durable state model (`~/.nagent/`)

| Path | Format | What it stores | Lifetime |
|---|---|---|---|
| `conversations/` | text files (per conversation) | The current + archived + delegated + per-file conversations | Indefinite (until `nagent-gc --apply` reclaims) |
| `conversations/file-index-{pid}.json` | JSON `{by_file_id: {file_id: {path, conversation}}}` | Maps stable file ids (device:inode) to per-file conversations | Per pid (host+shell) |
| `conversations/index-saved-conversations-{pid}.json` | JSON `{conversations: [{path, name, summary}]}` | Saved-conversations index | Per pid |
| `splits/<slug>-<uuid>/index.json` | JSON `{source_path, source_sha256, source_size_bytes, source_line_count, split_type, target_bytes, natural, created_at, segment_count, segments[]}` | Per-split metadata | Per split |
| `splits/<slug>-<uuid>/<slug>-000N.<ext>` | text files | Per-segment content | Per split |
| `splits/<slug>-<uuid>/<slug>.patch` | unified diff | The patch artifact | Per split (if patched) |
| `knowledge/<category>.md` | text files (append-only) | Per-category harvested items with provenance | Indefinite (user-editable) |
| `knowledge/tasks.md` | text (## Open / ## Done sections) | Open and done tasks with provenance | Indefinite |
| `knowledge/files/{file_id}.md` | text (per file) | Per-file knowledge notes with provenance | Indefinite (keyed by inode) |
| `knowledge/digest.md` | text (bounded 4KB) | The projected digest for initial context | Regenerated on each `nagent-gc --apply` |
| `knowledge/ledger.json` | JSON `{entries: {sha: {path, status, at, items, deleted, error}}}` | The harvest audit log | Indefinite |
| `prompts/compact-conversation.md` | text | User-editable compaction guidance | User-managed |
| `prompts/harvest-conversation.md` | text | User-editable harvest prompt | User-managed |
| `context.yaml` / `context.md` | YAML or markdown | The root context (per nagent root) | User-managed |
| `config.json` | JSON `{provider, model, ...}` | Provider + model defaults | User-managed |

**The defaults** (per the README's "Setup" section):

| Provider | Default model | Credential environment variable |
|---|---|---|
| `openai` | `gpt-5.5` | `OPENAI_API_KEY` |
| `anthropic` | `claude-sonnet-4-6` | `ANTHROPIC_API_KEY` |
| `google` | `gemini-2.5-flash` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
| `cursor` | `composer-2.5` | `CURSOR_API_KEY` |
| `claude-code` | `default` | None — uses the local Claude Code login |

**The precedence order** (per `bin/nagent`): CLI flags → env var (`NAGENT_CONFIG`) → `~/.nagent/config.json` → provider defaults.

**The file-id pattern** (per `bin/helpers/nagent_file_edit_lib.py:file_id_for_path`):

```python
def file_id_for_path(path: Path) -> str:
    """Stable file identity across renames. Returns "device:inode"."""
    stat = path.stat()
    return f"{stat.st_dev}:{stat.st_ino}"
```

Renames within the same filesystem preserve the inode, so the file_id is stable. Moves across filesystems do NOT preserve the inode; the user must re-add the file.

**The `default_conversation_name()` and `default_pid()`** (per the README's CLAUDE.md mention):

- `default_pid() = BASHPID or os.getppid()` — the host + shell identifier
- `default_conversation_name()` — derived from the user, host, and pid

Multiple nagent instances on the same host (e.g., two terminal tabs) get different conversations, but the *file-index* is per-pid so each instance has its own per-file conversation index.

### 7.5 The write boundaries

**The validation function** (per `bin/nagent`'s `validate_write_path`):

| Mode | Allowed paths | Disallowed |
|---|---|---|
| Main conversation (any tag) | `/tmp`, `/var/tmp`, `$TMPDIR` | anywhere else |
| Per-file edit (`nagent-file-edit` or `nagent --file-edit`) | The target file (by path or file id), or one of its split segments | anywhere else |

**Rejected writes** append `<nagent-write-result status="error">` to the conversation. The error is *data* (not an exception), so the LLM can see it and respond.

**The CLI surface** for invalid writes:

```
<nagent-write path="/etc/passwd">new content</nagent-write>
<nagent-write-result path="/etc/passwd" status="error">write validation failed: path not in allowlist</nagent-write-result>
```

**The pattern for the LLM.** The LLM sees the error in the conversation and (hopefully) tries a different path. `MAX_FORMAT_RETRIES = 3` for protocol failures; there is no retry for write-validation failures (the LLM is responsible for choosing a valid path).

**The Manual Slop equivalent.** `mcp_client._is_allowed` (the 3-layer security: allowlist + path validation + resolution gate) is dramatically stricter than nagent's tmpdir check. See §3.5 (the "PARITY (STRONGER on Manual Slop's side)" verdict) for the comparison.

### 7.6 The large-file pipeline

**The threshold** (per `bin/nagent`): inline reads at 64KB cascade to `nagent-file-split`. The target split size is 32KB.

**The pipeline** (per the README's "Large files" section):

```
[Q:read < 64KB?]
   │
   ├── yes ──► [I:read inline]
   │
   └── no ──► [I:nagent-file-split --file X --output /tmp/split --natural]
                │
                ▼
            [I:write index.json + segment files]
                │
                ▼
            [I:nagent-file-summarize (optional, per-segment)]
                │
                ▼
            [I:edits target segments]
                │
                ▼
            [I:nagent-file-patch --index /tmp/split/index.json]
                │
                ▼
            [I:validate source hash]
            [I:merge segments]
            [I:write patch artifact]
            [I:apply if --apply]
```

**The 12 supported languages** (per `bin/helpers/nagent-file-split-*`):

| Language | Extension | SCORE_BY_TYPE strategy |
|---|---|---|
| `txt` | `.txt` | blank lines |
| `md` | `.md` | headings (`# `, `## `, etc.) |
| `cpp` | `.cpp, .cc, .cxx, .hpp, .h, .hh` | brace depth (closing `}` at depth 0) |
| `py` | `.py` | blank lines followed by `def`/`class`/`async def` |
| `xml` | `.xml` | element depth (closing `</tag>` at depth 0) |
| `js` | `.js` | brace depth |
| `ts` | `.ts` | brace depth |
| `json` | `.json` | brace/bracket depth (closing `}`/`]` at depth 0) |
| `yaml` | `.yaml, .yml` | document markers (`---`) |
| `go` | `.go` | brace depth |
| `rs` | `.rs` | brace depth |
| `java` | `.java` | brace depth |

**The perf fix** (per the `67a3ea5` commit message): "O(n^2) splitter scoring (13.6s -> 0.008s on a 100KB cpp file)." The O(n²) → O(n) fix is in `SCORE_BY_TYPE`; the per-line scoring now uses a single pass with cached depths.

**The hash validation** (per `nagent_file_patch_lib.py:validate_index`):

```python
def validate_index(index, *, require_hash_match=True):
    """Strict hash check; rejects if source changed (unless --force)."""
    ...
```

`source_sha256()` is computed at split time and stored in `index.json`. The patch op reads the current source, recomputes the hash, and compares. If mismatch: reject (unless `--force`).

**The cascade to summarize** (per `nagent_file_summarize_lib.py:summary_cascade`):

Files > 64KB cascade to `nagent-file-summarize`. The summarizer per-segment LLM call retries up to `SUMMARY_MAX_ATTEMPTS = 2` if the LLM overshoots the `--limit-word-count`.

**The Manual Slop equivalent.** `aggregate.py:build_file_items` + `py_get_skeleton` (tree-sitter) + `ts_c_*_get_skeleton` (tree-sitter) + `outline_tool.py`. Different mechanism, same insight. See §2.12 for the detailed comparison.

### 7.7 The architecture as a whole (one diagram)

```
┌────────────────────────────────────────────────────────────────────┐
│ External (Gemini CLI, OpenCode, Claude Code)                        │
│  - reads CLAUDE.md (the agent-facing rules)                         │
│  - reads the @import'd canonical DOD file                          │
└────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────────┐
│ bin/nagent (the main loop, 2,524 lines)                             │
│  - main() → run_agent_loop() → call_llm() → parse_response() →      │
│    process_tags()                                                   │
│  - reads ~/.nagent/conversations/<name> (the durable state)        │
│  - appends, sends, parses, acts, repeats                            │
│  - MAX_FORMAT_RETRIES = 3                                           │
│  - caches stable prefix via --cache-prefix-chars                     │
└────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────────┐
│ bin/helpers/ (the library)                                          │
│  - nagent_cli.py:  --description discovery, WaitSpinner              │
│  - nagent_llm.py:  5+1 providers, cache_prefix_blocks               │
│  - nagent_tags.py: explicit tag parser                              │
│  - nagent_gc_lib.py: knowledge harvest library                       │
│  - nagent_file_edit_lib.py: per-file conversations                   │
│  - nagent_file_split_lib.py: 12-language splitter                    │
│  - nagent_file_patch_lib.py: strict hash validation                  │
│  - nagent_file_summarize_lib.py: per-segment summarize              │
└────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────────┐
│ bin/nagent-* (the CLI front-ends)                                   │
│  - 9 thin wrappers; each handles --description                      │
│  - nagent, nagent-llm-text, nagent-llm-upload                        │
│  - nagent-file-edit, nagent-file-split, nagent-file-patch            │
│  - nagent-file-summarize, nagent-gc                                │
└────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────────┐
│ ~/.nagent/ (the durable state)                                      │
│  - conversations/  (text files, the working state)                   │
│  - file-index-{pid}.json  (per-pid, per-file conversation index)    │
│  - index-saved-conversations-{pid}.json  (saved convo index)        │
│  - splits/<slug>-<uuid>/  (large-file split metadata + segments)     │
│  - knowledge/  (facts, decisions, questions, playbooks, tasks,       │
│                files, digest, ledger)                                │
│  - prompts/  (compact-conversation.md, harvest-conversation.md)     │
│  - context.yaml  (the root context)                                 │
│  - config.json  (provider + model defaults)                         │
└────────────────────────────────────────────────────────────────────┘
```

### 7.8 The conventions for changes (the engineering discipline)

Per `CLAUDE.md:130-139`:

> - Prefer adding a self-describing `bin/` executable over wiring a new code path into the loop, unless it genuinely belongs in the loop.
> - Keep provider-specific code inside `nagent_llm.py`.
> - Tests in `tests/` double as executable specs (parser, conversation lifecycle, retries, token accounting, file ids, split/patch, providers, tool descriptions). Add or update the matching test for any behavioral change.
> - `prompts/` holds reusable prompt documents (e.g. README-generation, conversation-compaction) used by the workflow, not application source.

**The 4 conventions** (the engineering discipline):

1. **Self-describing tools over wired paths.** A new tool is `bin/<tool>` + `exit_on_description()`; no edit to the dispatch.
2. **Provider-specific code is isolated.** All 5+1 providers live in `nagent_llm.py`; nowhere else.
3. **Tests are executable specs.** Every behavior change has a matching test; the test is the documentation.
4. **Prompts are data, not code.** User-editable prompts live in `prompts/`, not in the source tree.

**The 4 conventions map cleanly to Manual Slop:**

| nagent | Manual Slop |
|---|---|
| Self-describing tools | `mcp_architecture_refactor_20260606` (planned; sub-MCPs as self-describing modules) |
| Provider-specific code isolated | `_send_<provider>` in `src/ai_client.py`; the `qwen_llama_grok_followup_20260611` `send_openai_compatible()` helper enforces this |
| Tests as executable specs | `docs/guide_testing.md` codifies this; the 251 test files are the corpus |
| Prompts as data | `prompts/harvest-conversation.md`; (in Manual Slop) the editable system prompt preset pattern |

### 7.9 The architecture summary

nagent's architecture is a *data-oriented pipeline*:
- The conversation is the data
- The state is on disk
- The LLM is the transformation
- The tags are the protocol
- The handlers are the dispatch
- The tools are the executables
- The state model is the file system

The architecture is *self-describing* (the `--description` pattern), *self-caching* (the stable-to-volatile ordering), *self-fixing* (the `MAX_FORMAT_RETRIES` correction turns), and *self-pruning* (the `nagent-gc` harvest + reclaim). The architecture does *not* have a central registry, a central config, a central tool inventory, or a central state. It has files. The data is the thing.

---

## 8. The vocabulary patterns (the 8 tags + the per-tag guidance)

The tag protocol is nagent's *user surface*. The LLM emits the tags; the parser dispatches them; the handlers run them; the results come back. The vocabulary is the API.

### 8.1 The 8 tags (recap with examples)

| Tag | Self-closing? | Example (from the README) | Return wrapper |
|---|---|---|---|
| `<nagent-response>` | no | `<nagent-response>Done.</nagent-response>` | (turn ends) |
| `<nagent-read>` | yes | `<nagent-read path="src/foo.py"/>` | `<nagent-read-result path="..." bytes_read="..." content="..."/>` |
| `<nagent-file-read>` | yes | `<nagent-file-read path="src/big.cpp"/>` | cascades to split+summarize if > 64KB |
| `<nagent-file-patch>` | yes | `<nagent-file-patch index="/tmp/split/index.json"/>` | `<nagent-file-patch-result index="..." applied="..."/>` |
| `<nagent-write>` | no | `<nagent-write path="/tmp/x" bytes="42">new content</nagent-write>` | `<nagent-write-result path="..." bytes_written="..."/>` |
| `<nagent-shell>` | no | `<nagent-shell>python3 -m unittest discover -s tests -v</nagent-shell>` | `<nagent-shell-result exit_code="0" stdout="..." stderr="..."/>` |
| `<nagent-next>` | no | `<nagent-next>Try a different approach.</nagent-next>` | (loop continues) |
| `<nagent-conversation>` | no | `<nagent-conversation conversation-file="test-runner">run all tests</nagent-conversation>` | `<nagent-conversation-result conversation="..." tokens_in="..." tokens_out="..." exit_code="..."/>` |

### 8.2 The per-tag guidance (inline in the protocol)

The protocol is *defined inside the prompt* (per the README's §2 "Teach the Model an Output Format"):

> The tag list carries its usage guidance inline and lives inside ` `, so refreshed context always carries the current protocol with it.

**This means:** when the protocol is updated, the prompt is updated; when the prompt is re-rendered, the new protocol goes into the initial context. The protocol is *data*, not code.

### 8.3 The handler dispatch (in `process_tags`)

The dispatch is by tag name (per `bin/nagent`'s `process_tags` function). Each tag has:
- A handler function (in the main loop or in a library)
- A return wrapper (the `<nagent-*-result>` block)
- An error envelope (on failure)

**The dispatch shape:**

```python
def process_tags(parsed_tags):
    for tag in parsed_tags:
        try:
            if tag.name == "nagent-read":
                handle_nagent_read(tag)
            elif tag.name == "nagent-file-read":
                handle_nagent_file_read(tag)
            elif tag.name == "nagent-file-patch":
                handle_nagent_file_patch(tag)
            elif tag.name == "nagent-write":
                handle_nagent_write(tag)
            elif tag.name == "nagent-shell":
                handle_nagent_shell(tag)
            elif tag.name == "nagent-next":
                handle_nagent_next(tag)
            elif tag.name == "nagent-conversation":
                handle_nagent_conversation(tag)
            elif tag.name == "nagent-response":
                handle_nagent_response(tag)
                return  # turn ends
            else:
                append_error_to_conversation(f"unknown tag: {tag.name}")
        except Exception as exc:
            append_error_to_conversation(f"handler error in {tag.name}: {exc}")
```

**The errors-as-data pattern.** Exceptions in handlers are caught and turned into error envelopes. The LLM sees the error and can respond (change approach, not retry the identical action).

### 8.4 The parse-then-dispatch split

**The separation.** `parse_response` (uses `nagent_tags.py:parse_tag_document`) produces a list of `TagNode`s. `process_tags` (the main loop) dispatches them. The two functions are *decoupled*:
- The parser is *strict* (rejects unknown tags, malformed attributes, unterminated bodies)
- The dispatcher is *tolerant* (errors are data; the LLM sees them and responds)

**The pattern generalized.** The strict-parse-then-tolerant-dispatch is a common data-oriented pattern: validate at the boundary, handle errors as data inside. The same pattern is in Manual Slop's `data_oriented_error_handling_20260606` (`Result[T, ErrorInfo]` envelope).

### 8.5 The per-tag result wrappers

| Tag | Result wrapper | Error envelope |
|---|---|---|
| `<nagent-read>` | `<nagent-read-result path="..." bytes_read="N" content="..."/>` | `<nagent-read-result path="..." status="error">read failed: ...</nagent-read-result>` |
| `<nagent-file-read>` | `<nagent-file-read-result path="..." segments="N" index="..."/>` (or cascade result) | `<nagent-file-read-result path="..." status="error">split/summarize failed: ...</nagent-file-read-result>` |
| `<nagent-file-patch>` | `<nagent-file-patch-result index="..." applied="true" patch="..."/>` | `<nagent-file-patch-result index="..." status="error" reason="hash_mismatch" required="..." actual="..."/>` |
| `<nagent-write>` | `<nagent-write-result path="..." bytes_written="N"/>` | `<nagent-write-result path="..." status="error">write failed: ...</nagent-write-result>` |
| `<nagent-shell>` | `<nagent-shell-result exit_code="N" stdout="..." stderr="..."/>` | (errors come back as non-zero exit_code) |
| `<nagent-conversation>` | `<nagent-conversation-result conversation="name" tokens_in="N" tokens_out="M" exit_code="0"/>` | `<nagent-conversation-result conversation="name" status="error" reason="..."/>` |

**The shape is uniform.** Every result wrapper has the same structure: a positive case with token counts (or sizes, or counts) and an error case with `status="error"` and a reason. The LLM can parse any result with the same pattern.

### 8.6 The cross-cutting contract

**The 5 protocol rules** (from `bin/nagent:708-713`, repeated for emphasis):

1. **Raw bodies.** No entity escaping. The first close tag wins.
2. **Nothing outside tags.** Prose is rejected; unknown tags are rejected; unknown attributes are rejected.
3. **The loop contract.** Results come back as `<nagent-*-result>` blocks; the LLM is called again; never fabricate results.
4. **Multiple tags in one turn.** Run in order.
5. **Errors are data.** Change approach; don't retry identically.

**The contract is a 5-point list of failure modes.** Each rule names a specific failure mode that the protocol prevents. This is *explicit error design*; the same pattern is in the `data_oriented_error_handling_20260606` styleguide.

### 8.7 The tag protocol as a 4-tier vocabulary (for Manual Slop's optional per-MCP DSL)

**The tier structure** (adapted from nagent):

| Tier | Category | Tags | Example |
|---|---|---|---|
| 1 | Math | (none in nagent; would be `+ - * / ^ sum product` in a math DSL) | `2 3 +` (postfix add) |
| 2 | Data | `<nagent-read>`, `<nagent-file-read>`, `<nagent-file-patch>`, `<nagent-write>` | `<nagent-read path="src/foo.py"/>` |
| 3 | Shell | `<nagent-shell>` | `<nagent-shell>ls -la</nagent-shell>` |
| 4 | Control | `<nagent-response>`, `<nagent-next>`, `<nagent-conversation>` | `<nagent-response>Done.</nagent-response>` |

**The 4-tier mapping** (to Manual Slop's planned per-MCP DSL, per the `mcp_architecture_refactor_20260606` track's intent-based DSL placeholder):

| nagent tier | Manual Slop analog | Sub-MCP |
|---|---|---|
| Tier 1 (math) | (no Application-side need; the GUI panel is the math) | (n/a) |
| Tier 2 (data) | `read_file`, `py_get_skeleton`, `set_file_slice`, `edit_file` | `mcp_file_io.py`, `mcp_python.py` |
| Tier 3 (shell) | `run_powershell` | `mcp_runtime.py` (the new sub-MCP for shell) |
| Tier 4 (control) | `_predefined_callbacks` + `_gettable_fields` (the Hook API) | (cross-cutting; not a per-MCP concern) |

**The nagent tag protocol is the 4-tier vocabulary that nagent's *Meta-Tooling* uses.** Manual Slop's planned per-MCP DSL is the parallel; the proposed styleguide `conductor/code_styleguides/agent_dsl_verbs.md` (NEW) would codify the per-MCP verb catalog.

### 8.8 The tag protocol vs Manual Slop's function-calling

| Aspect | nagent tags | Manual Slop function-calling |
|---|---|---|
| **Notation** | XML-ish self-closing tags; raw bodies | JSON Schema; typed arguments |
| **Visibility** | Plain text, inspectable in the conversation | JSON blobs in provider-specific format |
| **Per-provider portability** | Same tags work across all 5 providers | Each provider has its own schema; 5 different per-provider formats |
| **Provider capability ceiling** | Whatever the model can emit as text | Native parallel tool calls, structured outputs, JSON-mode constraints |
| **Debuggability** | "Why didn't the model read the file?" → grep the conversation for `<nagent-read>` | "Why didn't the model call read_file?" → inspect the JSON response |
| **Per-tag guidance** | Inline in the prompt (lives inside the `<nagent-conversation>` block) | In the system prompt or a separate prompt section |
| **Strict-parse-then-tolerant-dispatch** | Yes (the explicit parser) | Yes (the schema validator) |

**The verdict.** Architectural difference, not a gap. The Application wants parallel tool calls and JSON-mode constraints; the Meta-Tooling wants inspectable text. The `mcp_architecture_refactor_20260606` sub-MCP extraction is the right scope for considering a per-MCP DSL on the Meta-Tooling side.

### 8.9 The vocabulary summary (the takeaway)

**The vocabulary IS the user surface.** A model that knows the 8 tags and the 5 protocol rules can express any intent that nagent supports. There is no separate "API documentation" — the tags ARE the API.

**The same is true for Manual Slop's planned DSL.** A model that knows the per-MCP verbs (read_file, py_get_skeleton, run_powershell, etc.) and the hook API (the `_predefined_callbacks` + `_gettable_fields` registry) can express any intent that Manual Slop's Meta-Tooling supports. The vocabulary is the API.

**The corollary.** Adding a new feature in nagent = adding a new tag (or a new dispatch branch in `process_tags`). Adding a new feature in Manual Slop = adding a new sub-MCP verb (or a new hook callback). The 4-tier structure (math / data / shell / control) is the same in both.

---

## 9. File splits, patches, summaries (the large-file pipeline)

The large-file pipeline is the 4-stage process: inline read (if < 64KB) → split (if > 64KB) → edit segments → patch (validate hash, merge, write unified diff). This section is the deep-dive.

### 9.1 The 4-stage pipeline (as a single SSDL diagram)

```
[Q:read file F]
   │
   ▼
[Q:F size < 64KB?]
   │
   ├── yes ──► [I:read inline]   [T:return content]
   │
   └── no ──►
            │
            ▼
       [I:nagent-file-split --file F --output /tmp/split --natural --target-bytes 32768 --json]
            │
            ├── [I:identify natural breakpoints (per-language SCORE_BY_TYPE)]
            ├── [I:write /tmp/split/index.json with metadata]
            ├── [I:write /tmp/split/F-0001.<ext>, F-0002.<ext>, ...]
            │
            ▼
       [I:return {index, segments}]
            │
            ▼
       [loop: each segment]
            │
            ├──► [Q:LLM call needed?]
            │     │
            │     ├── no ──► [I:read segment as-is]
            │     │
            │     └── yes ──► [I:nagent-file-summarize --file F-000N.<ext> --limit-word-count N --json]
            │                    │
            │                    ▼
            │                 [I:per-segment LLM call (up to SUMMARY_MAX_ATTEMPTS=2)]
            │                 [I:append summary to index]
            │
            ▼
       [I:edits to segments (user or LLM)]
            │
            ▼
       [I:nagent-file-patch --index /tmp/split/index.json --json]
            │
            ├── [I:validate_index: source_sha256 matches]
            │     │
            │     ├── no ──► [T:error: hash_mismatch]   (unless --force)
            │     │
            │     └── yes ──►
            │                │
            │                [I:merge_segments(segments) -> str]
            │                │
            │                [I:make_unified_patch(source, original, updated)]
            │                │
            │                [I:write patch artifact: /tmp/split/F.patch]
            │                │
            │                [I:apply?]
            │                  │
            │                  ├── no ──► [I:dry-run; print diff]
            │                  │
            │                  └── yes ──► [I:write updated source to F]
            │
            ▼
       [I:return {index, patch, applied}]
```

### 9.2 The 12 supported languages (the SCORE_BY_TYPE strategy)

| Language | Extension(s) | SCORE_BY_TYPE | Natural splitter (high score) |
|---|---|---|---|
| `txt` | `.txt` | blank lines (lower score for more consecutive non-blank lines) | blank line |
| `md` | `.md` | heading markers (`# `, `## `, etc.) | heading line |
| `cpp` | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.h`, `.hh` | closing `}` at brace depth 0 | `}` at depth 0 |
| `py` | `.py` | blank line followed by `def`, `class`, `async def` | `def foo():` line |
| `xml` | `.xml` | closing `</tag>` at element depth 0 | `</tag>` at depth 0 |
| `js` | `.js` | closing `}` at brace depth 0 | `}` at depth 0 |
| `ts` | `.ts` | closing `}` at brace depth 0 | `}` at depth 0 |
| `json` | `.json` | closing `}` or `]` at depth 0 | `}` or `]` at depth 0 |
| `yaml` | `.yaml`, `.yml` | document markers (`---`) | `---` line |
| `go` | `.go` | closing `}` at brace depth 0 | `}` at depth 0 |
| `rs` | `.rs` | closing `}` at brace depth 0 | `}` at depth 0 |
| `java` | `.java` | closing `}` at brace depth 0 | `}` at depth 0 |

**The strategy.** Every language has the same goal: find *structural* boundaries (where the code/text naturally breaks). The score is high at these boundaries; the split is at the high-score positions.

**The 12 splitter files** (in `bin/helpers/`):

```
nagent-file-split-{py,cpp,js,ts,json,yaml,md,xml,txt,go,rs,java}
```

Each is ~225 bytes; a thin wrapper that:
1. Calls `exit_on_description(NAGENT_FILE_SPLIT_<LANG>_DESCRIPTION)`
2. Imports `nagent_file_split_lib.py`
3. Calls the splitter with the right `SCORE_BY_TYPE`
4. Writes the index + segments

**The 12 wrapper code shape** (example, `nagent-file-split-py`):

```python
#!/usr/bin/python3
import sys
from pathlib import Path
HELPERS_DIR = Path(__file__).resolve().parent / "helpers"
sys.path.insert(0, str(HELPERS_DIR))
from nagent_file_split_lib import split_file
from nagent_cli import exit_on_description, emit_json

NAGENT_FILE_SPLIT_PY_DESCRIPTION = """\
Split a Python file into structure-aware segments. Uses blank lines before
def/class/async def as natural splitters. Writes index.json + segment files."""

def main():
    exit_on_description(NAGENT_FILE_SPLIT_PY_DESCRIPTION)
    # ... argparse + split_file call + emit_json ...
    ...

if __name__ == "__main__":
    sys.exit(main())
```

### 9.3 The index.json format

```json
{
  "source_path": "/repo/src/big.cpp",
  "source_sha256": "a1b2c3d4...",
  "source_size_bytes": 102400,
  "source_line_count": 3200,
  "split_type": "cpp",
  "target_bytes": 32768,
  "natural": true,
  "created_at": "2026-06-12T14:23:45.123456+00:00",
  "segment_count": 4,
  "segments": [
    {
      "name": "big-0001.cpp",
      "path": "/tmp/split/big-0001.cpp",
      "byte_offset": 0,
      "byte_length": 32768,
      "line_offset": 0,
      "line_length": 1024,
      "summary": null
    },
    {
      "name": "big-0002.cpp",
      "path": "/tmp/split/big-0002.cpp",
      "byte_offset": 32768,
      "byte_length": 32768,
      "line_offset": 1024,
      "line_length": 1024,
      "summary": null
    },
    ... 2 more segments ...
  ]
}
```

The `source_sha256` is computed at split time. The patch op validates against this hash. The `segments[].summary` is populated by `nagent-file-summarize` if run after split.

### 9.4 The patch workflow (the safety property)

**The safety property** (the load-bearing design decision):

> The patch operation validates the source hasn't changed. If the source has been modified since the split, the patch is rejected (unless `--force`).

**The validation:**

```python
def validate_index(index, *, require_hash_match=True):
    source = Path(index["source_path"])
    if not source.is_file():
        raise FileNotFoundError(f"split source gone: {source}")
    recorded_hash = index.get("source_sha256")
    if recorded_hash and source_sha256(source) != recorded_hash:
        if require_hash_match:
            raise ValueError(f"split stale (source changed): {recorded_hash} != {source_sha256(source)}")
    # else: --force passed; proceed
```

**The merge:**

```python
def merge_segments(segments):
    """Concatenate the segment contents in order."""
    return "\n".join(Path(s["path"]).read_text(encoding="utf-8") for s in segments)
```

**The diff:**

```python
def make_unified_patch(source, original, updated):
    return "\n".join(
        difflib.unified_diff(
            original.splitlines(keepends=True),
            updated.splitlines(keepends=True),
            fromfile=source,
            tofile=source,
        )
    )
```

**The apply:**

```python
def apply_segment_patches(source, segments, patch, *, require_hash_match=True):
    validate_index(index, require_hash_match=require_hash_match)
    original = merge_segments(segments)
    updated = apply_patch_to_source(source, original, patch)
    Path(source).write_text(updated, encoding="utf-8")
```

### 9.5 The cascade to summarize

**The 64KB threshold.** Files > 64KB cascade to `nagent-file-summarize`. The summarizer:

1. Calls the LLM per-segment with the prompt: "Summarize the following code/text in ≤ N words."
2. Retries up to `SUMMARY_MAX_ATTEMPTS = 2` if the LLM overshoots the `--limit-word-count`.
3. Writes the per-segment summary to `index.json:segments[].summary`.
4. Optionally writes a combined summary (`combined_summary_from_index`) to a separate file.

**The summary prompt shape** (per `nagent_file_summarize_lib.py`, inferred):

```
You are summarizing a code segment for an LLM that will read it later.
Produce a summary in ≤ {limit_word_count} words. Use plain markdown.
Focus on:
  - the segment's purpose
  - its public surface (functions, classes, exports)
  - its dependencies on other segments
  - non-obvious invariants
```

**The retry pattern:**

```python
def summarize_content(path, provider, model, config_path, *, limit_word_count):
    template_path = Path(__file__).resolve().parent.parent / "prompts" / "summarize-content.md"
    template = template_path.read_text(encoding="utf-8").strip()
    last_error = None
    for attempt in range(SUMMARY_MAX_ATTEMPTS):
        prompt = build_summarize_prompt(template, path.read_text(encoding="utf-8"), limit_word_count, retry=attempt > 0)
        response = generate_text(prompt, provider, model)
        word_count = len(response.split())
        if word_count <= limit_word_count:
            return response
        last_error = f"word count {word_count} > limit {limit_word_count}"
    raise RuntimeError(f"summarize output overshot word count after {SUMMARY_MAX_ATTEMPTS} attempts: {last_error}")
```

### 9.6 The O(n²) → O(n) perf fix (per the `67a3ea5` commit)

**The bug.** `nagent_file_split_lib.py:SCORE_BY_TYPE` was O(n²) for some languages (the per-line scoring re-computed the depth from scratch for every candidate position). On a 100KB cpp file, this took 13.6s.

**The fix.** A single pass with cached depths. The `current_depth` is maintained as a counter; each line's score is computed in O(1) given the current depth. Total: O(n) per language.

**The benchmark** (per the commit message): 100KB cpp file went from 13.6s to 0.008s. **1700x speedup.**

**The Manual Slop equivalent.** Manual Slop's `py_get_skeleton` and `ts_c_*_get_skeleton` are tree-sitter-based, which has its own O(n) cost (the parser is linear in input size). The natural-splitter approach is *less accurate* than tree-sitter but *faster* (no parser overhead). For Manual Slop, the choice is:
- Use tree-sitter (accurate, slower) for small files (≤ 64KB)
- Use natural splitters (fast, less accurate) for very large files (> 64KB)

The cascade threshold could be the same as nagent's (64KB). Manual Slop could adopt nagent's `nagent_file_split_lib.py:SCORE_BY_TYPE` design when files exceed the threshold.

### 9.7 The cross-language file index (the manual_slop analog)

| nagent | Manual Slop |
|---|---|
| 12 supported languages (txt, md, cpp, py, xml, js, ts, json, yaml, go, rs, java) | Python + C/C++ (via tree-sitter) + Markdown + plain text |
| Per-language `SCORE_BY_TYPE` (regex + line counts + brace/JSON/XML depth) | Tree-sitter parsing (accurate AST) |
| 32KB target split size | N/A (tree-sitter handles files up to ~1MB without splitting) |
| `source_sha256()` validation | mtime-based (per `_reread_file_items`) |
| O(n) after the perf fix | O(n) tree-sitter; O(n²) only on the natural-splitter fallback |

**The verdict.** Manual Slop's tree-sitter-based approach is *more accurate* for the languages it supports. nagent's natural-splitter approach is *more languages* (12 vs 2-3). For Manual Slop, the question is: do we need to support more languages? The answer depends on the user's actual codebase (most are Python + C/C++ + Markdown). The `mcp_architecture_refactor_20260606` sub-MCP extraction could add per-language sub-MCPs (e.g., `mcp_go.py`, `mcp_rust.py`) if needed.

### 9.8 The Manual Slop recommendation

**Don't add the natural-splitter fallback yet.** Manual Slop's tree-sitter covers 95% of real workloads. The natural-splitter is a *fallback* for when tree-sitter is too slow or the language isn't supported. Adopt it only if a 200KB+ file scenario actually surfaces.

**Candidate 9 (DEFER).** This is the explicit "DEFER until needed" candidate. No implementation; document the nagent pattern as a reference for future use.

### 9.9 The file ops summary

**The 4-stage pipeline** (inline read → split → edit → patch) is the data-oriented way to handle large files. The state at each stage is a file (not an in-memory object). The validation at each boundary is a sha256 hash. The errors are data (the `validate_index` returns the expected and actual hashes; the LLM sees them and responds).

**The cross-cutting pattern:** the conversation is the working state; the split is a side artifact; the patch is a side artifact; the source is the original. None of these are in-memory object graphs. All are files.

---


## 10. Future-track candidates (the 16-candidate catalog)

The full candidate list, in priority order, with full specifications. This section is the planning artifact for the next-turn work.

### 10.1 The 16 candidates (compact summary)

| # | Symbol-like ID | Name | Domain | Pri | Effort | Shape | Depends on |
|---|---|---|---|---|---|---|---|
| 1 | `SubConversationRunner` | 1:1 sub-convos | App + MT | HIGH | Med | `=>` | none |
| 2 | `RAGPreStager` | RAG pre-staging via sub-convo | App | MED | Sm | `o->` | 1 |
| 3 | `LLMClient` (stateless) | Stateless LLMClient class | App | MED | Lg | `[I]` | none |
| 4 | `IntentDSL` | Intent DSL for Meta-Tooling | MT | LOW | research | `[I]` | none |
| 5 | `SelfDescribingTools` | Self-describing MCP tools | BOTH | LOW (subsumed) | Med | `[I]` | `mcp_architecture_refactor` |
| 6 | `GitHistory` | `src/git_history.py` (nagent §7) | App | MED | Med | `[I]` | none |
| 7 | `PerFileConversation` | Per-file conversation log | App | LOW | Sm | `[I]` | 3 |
| **8** | `KnowledgeMemory` | **Knowledge memory (3rd dimension)** | **App** | **HIGH** | **Lg** | **`o->`** | **`data_oriented_error_handling`** |
| **9** | `CacheOrdering` | **Stable-to-volatile cache ordering** | **App** | **MED** | **Sm** | **`->M->`** | **none** |
| **10** | `CacheTTL` | **Cache TTL GUI controls** | **App** | **MED** | **Med** | **`=>`** | **9** |
| **11** | `Compaction` | **Conversation compaction** | **App** | **MED** | **Sm** | **`->B->`** | **none** |
| **12** | `ProjectContext` | Project context file | App | LOW | Sm | `[I]` | none |
| **13** | `GracefulSave` | Save-with-graceful-summary-failure | App | TBD | Sm | `->B->` | none |
| **14** | `AGENTSImport` | **AGENTS.md `@import` + canonical DOD** | **BOTH** | **HIGH** | **Sm** | **`[I]`** | **none** |
| 15 | `RawTranscript` | Raw-transcript persistence per Take | App | LOW | Sm | `[I]` | 3 |
| 16 | `CoeditedFiles` | `py_/ts_c_coedited_files` tools | App | LOW | Sm | `[I]` | 6 |

**The 5 HIGH-priority candidates** (the bold rows): 1, 8, 14. (Plus 11 is a de-facto HIGH because the user explicitly asked for compaction + cache TTL in the same turn that surfaced v2.3.)

**The 5 MED-priority candidates** (the bold rows): 9, 10, 11. (Plus 2, 3, 6 are existing MEDs.)

**The net effect.** 5 of the 16 candidates are HIGH (2 explicit + 1 from v2.3 reframing + 1 from v2.3 user-flagged + 1 from v2.3 sub-candidate). 6 are MED. 4 are LOW. 1 is TBD. 1 is DEFER (Candidate 9 from v1, now folded into Candidate 12).

### 10.2 Candidate 1: `SubConversationRunner` (HIGH)

**User signal:** **EXPLICIT WANT** ("I probably want to add that for just 1:1 discussions where I use a sub-agent manually for specific points").

**What it would do.** A `SubConversationRunner` class that the App can call during a 1:1 discussion:
- `async runner.spawn(prompt, *, allowed_tools=None, system_prompt=None, timeout_s=120) -> SubConversationResult`
- Reuses MMA's `mma_exec.py` as the subprocess template
- Returns a concise artifact (the sub-agent's `<nagent-response>` content) + token usage + exit code
- The App inserts the result into the active discussion as a "User" role entry
- Cleanup: sub-conversation folder is auto-archived after 7 days

**The Manual Slop shape:**

```python
# In src/sub_conversation.py (NEW)
@dataclass
class SubConversationResult:
    artifact: str                       # the sub-agent's response
    tokens_in: int
    tokens_out: int
    exit_code: int
    errors: list[ErrorInfo]            # from the data_oriented_error_handling convention

class SubConversationRunner:
    async def spawn(self, prompt: str, *, allowed_tools: list[str] = None, ...) -> SubConversationResult:
        # Reuses mma_exec.py as the subprocess template
        # Returns the child's <nagent-response> content + token usage
        ...
```

**Where it lives.** Application.

**Depends on.** None directly. Could leverage MMA's `mma_exec.py` as a starting template.

**Effort.** Medium. 2-3 phases: (1) extract reusable subprocess skeleton from MMA, (2) add 1:1-specific context injection, (3) add GUI controls.

**Recommended priority.** **HIGH** — user-flagged.

### 10.3 Candidate 8: `KnowledgeMemory` (HIGH — the v2.3 reframing)

**What it would do.** A new `src/knowledge_store.py` + GUI panel:
- `KnowledgeStore` class with `add_bullet(category, text, provenance)`, `get_digest(budget_chars=4096)`, `regenerate_digest()`, `delete_digest()` (turn-off switch)
- `KnowledgeHarvester` class with `harvest_conversation(discussion) -> Result[list[KnowledgeBullet], ErrorInfo]`
- A new `src/knowledge_harvest_cli.py` (CLI wrapper, mirror of `bin/nagent-gc`)
- A bounded `{knowledge}` block injected into `aggregate.py:run` initial context (the *stable* position)
- A "Knowledge" panel in the GUI (parallel to Logs Management)
- Per-file knowledge notes in `~/.manual_slop/knowledge/files/{file_id}.md` (parallel to `FileItem.notes`)

**The 4 memory dimensions** (the framing):

| Dim | Where | SSDL | Status |
|---|---|---|---|
| Curation | `FileItem` + `ContextPreset` + Fuzzy Anchors | `[Q]` | existing, strong |
| Discussion | `disc_entries` + branching + UISnapshot | `o->` | existing, strong |
| RAG | `src/rag_engine.py` (ChromaDB) | `[Q]` | opt-in |
| Knowledge (proposed) | `~/.manual_slop/knowledge/*.md` + per-file + digest + ledger | `o->` | NEW |

**Where it lives.** Application.

**Depends on.** `data_oriented_error_handling_20260606` (the `Result`/`ErrorInfo` pattern for the harvest LLM call's return type).

**Effort.** Large. 5-6 phases (per §4.7 above).

**Recommended priority.** **HIGH** — the most important v2.3 finding.

### 10.4 Candidate 14: `AGENTSImport` (HIGH — the v2.3 user-correction)

**What it would do.** Mirror nagent's `CLAUDE.md → context/data-oriented-design.md` `@import` pattern in Manual Slop:
1. Create `conductor/code_styleguides/data_oriented_design.md` (cloned/adapted from nagent's `context/data-oriented-design.md`)
2. Update `AGENTS.md` to add `@conductor/code_styleguides/data_oriented_design.md` at the top
3. Create `./docs/AGENTS.md` (the agent-facing mirror of `docs/Readme.md`)
4. Inject the same canonical file via `[agent].context_files` in `manual_slop.toml` (or equivalent) so the Application's RAG / context assembly picks it up

**The Manual Slop shape:**

```markdown
# AGENTS.md
This file provides guidance to AI agents (Gemini CLI, OpenCode, Claude Code)
when working with code in this repository.

## Operating rules
@conductor/code_styleguides/data_oriented_design.md
The same file is injected into every discussion via manual_slop.toml — one
source of truth for both harnesses. Edit it there; do not duplicate rules
into this file.

## What this is
**Manual Slop** is a local GUI orchestrator for LLM-driven coding sessions.
The thesis drives every design decision and should drive yours: **the data
is the thing, not the agent.** State that matters lives in inspectable,
editable files on disk — never hidden in process memory. When output is
wrong, fix the generator or its inputs (the prompt), don't patch the artifact.
`docs/Readme.md` is the canonical teaching document; read it before making
non-trivial changes.
...
```

**Where it lives.** Both (project root, `docs/`, `conductor/code_styleguides/`).

**Depends on.** None.

**Effort.** Small to medium. 1-2 phases.

**Recommended priority.** **HIGH** — the foundation for the other styleguides.

### 10.5 Candidate 9: `CacheOrdering` (MED)

**What it would do.** A refactor of `src/aggregate.py:run` and `src/ai_client.py:_send_anthropic` to enforce stable-to-volatile context ordering.

**The 12-layer model:**

| # | Layer | Stable? |
|---|---|---|
| 1 | Role instructions | yes |
| 2 | Tag protocol / function-calling schema | yes |
| 3 | Discovered tool descriptions | yes |
| 4 | System prompt preset | yes |
| 5 | Persona profile | yes |
| 6 | Project context (per Candidate 12) | yes |
| 7 | Knowledge digest (per Candidate 8) | yes |
| 8 | Discussion metadata | no (per turn) |
| 9 | Active preset (FileItem set) | no (per turn) |
| 10 | Per-file details (history, slices, notes) | no (per file) |
| 11 | Tool-call results from prior turns | no (per turn) |
| 12 | The user message | no (per turn) |

**The cache boundary** is at layer 7/8.

**The byte-comparison test** (the design contract):

```python
def test_aggregate_stable_to_volatile_ordering():
    ctrl = mock_app_controller()
    turn1 = aggregate.build_initial_context(ctrl, user_message="first")
    turn2 = aggregate.build_initial_context(ctrl, user_message="second")
    N = aggregate.stable_prefix_length(ctrl)
    assert turn1[:N] == turn2[:N]
```

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Small. 1-2 phases.

**Recommended priority.** **MEDIUM.**

### 10.6 Candidate 10: `CacheTTL` (MED)

**What it would do.** A "Caching" Operations Hub sub-panel + Hook API + state machine:
- Per-provider cache summary (Anthropic 5min, Gemini 1h, OpenAI implicit)
- Per-discussion cache state (cached/expiring/disabled)
- Cache hit rate (from `cache_creation_input_tokens + cache_read_input_tokens` fold-back)
- `[Invalidate cache]` button per discussion
- The 5 new Hook API endpoints

**The provider-specific defaults:**

| Provider | Default TTL | Configurable? |
|---|---|---|
| Anthropic | 5 min | No |
| Google (Gemini) | 1 h | Yes (via `ttl` field) |
| OpenAI | 5-10 min (provider-managed) | No |

**Where it lives.** Application.

**Depends on.** Candidate 9 (`CacheOrdering`).

**Effort.** Medium. 4-5 phases.

**Recommended priority.** **MEDIUM** — user-flagged ("I can expose more explicit controls in the future for handling discussion caching and what not.. also expose how long the caches are available for").

### 10.7 Candidate 11: `Compaction` (MED)

**What it would do.** A new `run_discussion_compaction` function (parallel to `run_discussion_compression`):
- Editable `prompts/compact-discussion.md` (root-first resolution, mirroring nagent)
- The 12-section output structure (User Intent, Current Objective, Accepted Decisions, etc.)
- The 10-question self-review contract
- A new "Compact" button next to the existing "Compress" button in the GUI
- Graceful failure (original preserved on any failure)
- The 10-test self-review suite

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Small to medium. 5-6 phases.

**Recommended priority.** **MEDIUM.**

### 10.8 Candidate 12: `ProjectContext` (LOW)

**What it would do.** A `[context_files]` section in `manual_slop.toml` (or a top-level `manual_slop_context.md` file) read by `aggregate.py:run` at discussion start.

**The Manual Slop shape:**

```toml
# In manual_slop.toml
[agent.context_files]
project = "manual_slop_context.md"     # file at project toplevel
# OR:
extra = ["docs/handbook.md", "docs/style.md"]
```

**The aggregate.py:run change:**

```python
# After the system prompt block, before the active preset
project_context_path = paths.project_dir / "manual_slop_context.md"
if project_context_path.is_file():
    project_context = project_context_path.read_text(encoding="utf-8")
    sections.append(f"## Project Context\n\n{project_context}")
```

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Small. 1 phase.

**Recommended priority.** **LOW.**

### 10.9 Candidate 13: `GracefulSave` (TBD)

**What it would do.** (PENDING VERIFICATION) — read `src/ai_client.py:run_discussion_compression` to see if it raises on LLM failure (destructive) or falls back to the original (graceful).

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Small (1 phase) IF the current behavior is "raise on failure." Trivial (just a test) IF the current behavior is "fall back to original."

**Recommended priority.** **TBD** — MEDIUM if the current behavior is destructive.

### 10.10 Candidate 2: `RAGPreStager` (MED)

**User signal:** **EXPLICIT WANT** ("Would be cool to have a sub agent maybe prepare a rag chunks before I use them in a run").

**What it would do.** A "Pre-stage RAG" command in the GUI (or in `commands.py`):
- Spawns a sub-conversation with the prompt: "Index all files in [project] for RAG. Use the index_file tool on every file in the context. Report top-K queries at the end."
- The sub-conversation runs `rag_engine.index_file()` on each tracked file
- Returns a concise summary: "Indexed N files. Top-K for 'execution clutch': [file1, file2, file3]."
- The main discussion starts with the index already warm; `RAGEngine.search()` is fast

**Where it lives.** Application.

**Depends on.** Candidate 1 (`SubConversationRunner`).

**Effort.** Small to medium. The sub-conversation runner is the heavy lift (Candidate 1). The RAG-staging prompt is ~30 lines.

**Recommended priority.** **MEDIUM** — user-flagged; cheap given Candidate 1.

### 10.11 Candidate 3: `LLMClient` (stateless) (MED)

**What it would do.** A new `src/llm_client.py`:
- `Conversation` dataclass with `messages: list[Message]`, `metadata: dict`
- `LLMClient(provider, model, api_key=None)`
- `LLMClient.send(conversation, *, tools=None) -> Conversation`
- Backwards-compat: `ai_client.send(...)` becomes a thin wrapper

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Large. 3-5 phases.

**Recommended priority.** **MEDIUM.**

### 10.12 Candidate 6: `GitHistory` (MED)

**What it would do.** A new `src/git_history.py` mirroring nagent's `file_edit_history_and_summary_block`:
- `git_file_history(repo_root, rel_path)` — `git log --follow --max-count=50 --date=short --format=...`
- `summarize_new_file_commits(...)` — LLM call to one-line-summarize new commits (with cache for unchanged history)
- `coedited_file_rows(repo_root, rel_path, commits)` — counts files in the same commits; labels high/medium/low co-edit rate
- `format_file_history(...)` — produces a `{file-history}` block

**Where it lives.** Application.

**Depends on.** None.

**Effort.** Medium. 2-3 phases.

**Recommended priority.** **MEDIUM.**

### 10.13 Candidate 4: `IntentDSL` (LOW)

**User signal:** **EXPLICIT but DEFERRED** ("I want to add an intent based dsl to help with 'discovery' or combinatorics but no where near that ideation yet").

**What it would do.** A research spike. Document the design space; don't build.

**Where it lives.** Meta-Tooling.

**Depends on.** None.

**Effort.** Research.

**Recommended priority.** **LOW** — user-deferred.

### 10.14 Candidate 5: `SelfDescribingTools` (LOW, subsumed)

**What it would do.** Each sub-MCP emits a `--description` block on `--help`. The dispatch function introspects via `mcp_client.get_tool_schemas()`.

**Where it lives.** Application.

**Depends on.** `mcp_architecture_refactor_20260606` (in plan).

**Effort.** Medium. Subsumed.

**Recommended priority.** **LOW** (subsumed).

### 10.15 Candidate 7: `PerFileConversation` (LOW)

**What it would do.** A thin `~/.manual_slop/per_file/<file_id>.md` per file (file_id by `st_dev:st_ino`):
- Updated each time a discussion references the file
- When a new discussion opens with the file in context, the per-file log is injected as a `{per-file-history}` block

**Where it lives.** Application.

**Depends on.** Candidate 3 (`LLMClient`).

**Effort.** Small. 1-2 phases.

**Recommended priority.** **LOW** — niche.

### 10.16 Candidate 15: `RawTranscript` (LOW)

**What it would do.** Optionally, when a take is snapshotted to TOML, also persist the raw transcript to a sibling file `discussions/<take_name>/transcript.jsonl`. The GUI gets a "View Raw Transcript" button. Optional "Edit Raw Transcript" mode that re-parses and re-aggregates.

**Where it lives.** Application.

**Depends on.** Candidate 3 (`LLMClient`).

**Effort.** Small. 1 phase.

**Recommended priority.** **LOW.**

### 10.17 Candidate 16: `CoeditedFiles` (LOW)

**What it would do.** Two new MCP tools:
- `py_coedited_files(path) -> list[{path, commits_together, likelihood}]`
- `ts_c_coedited_files(path) -> list[{path, commits_together, likelihood}]`

**Where it lives.** Application.

**Depends on.** Candidate 6 (`GitHistory`).

**Effort.** Small. ~200 lines + tests.

**Recommended priority.** **LOW** — bundle with Candidate 6.

### 10.18 The 16-candidate summary table (the meta)

| Priority | Count | Candidates |
|---|---|---|
| HIGH | 3 | 1, 8, 14 (+ 11 de-facto HIGH per user flag) |
| MED | 5 | 2, 3, 6, 9, 10, 11 (6 if 11 de-facto HIGH is counted) |
| LOW | 6 | 4, 5, 7, 12, 15, 16 |
| TBD | 1 | 13 |
| DEFER | 1 | 9 (v1) (folded into 12) |

**The cumulative effort** (rough estimate):

| Priority | Phases (avg) | Effort (weeks) |
|---|---|---|
| HIGH (3-4 candidates) | 5-6 phases each | 4-6 months (sequential) or 2-3 months (parallel with 2 workers) |
| MED (5-6 candidates) | 2-3 phases each | 2-3 months (sequential) or 1-1.5 months (parallel) |
| LOW (6 candidates) | 1-2 phases each | 1-2 months |
| TBD (1) | 1 phase (verification) | 1 day |

**The recommended sequencing** (per the Phase 6+ sprint's "now unblocked" status):

1. **Candidate 14** (`AGENTSImport`) — the foundation. Unblocks all the styleguides.
2. **Candidate 8** (`KnowledgeMemory`) — the most important v2.3 finding. HIGH priority.
3. **Candidate 11** (`Compaction`) — user-flagged. MEDIUM but de-facto HIGH.
4. **Candidate 9 + 10** (`CacheOrdering` + `CacheTTL`) — user-flagged. MEDIUM.
5. **Candidate 1** (`SubConversationRunner`) — user-flagged. HIGH. Depends on MMA (already shipped).
6. **Candidate 2** (`RAGPreStager`) — user-flagged. Cheap given Candidate 1.
7. **Candidates 3, 6, 13** — the remaining MEDIUM/TBDs.
8. **Candidates 4, 5, 7, 12, 15, 16** — the LOW-priority backlog.

---

## 11. Proposed new artifacts (the next-turn scope)

The 15+ new files proposed for the next turn, in the user's preferred data format. All new files (no overrides of v1 artifacts or human Readme files).

### 11.1 The 1 canonical DOD file

| Field | Value |
|---|---|
| File path | `conductor/code_styleguides/data_oriented_design.md` |
| Type | NEW styleguide |
| Source | Cloned/adapted from nagent's `context/data-oriented-design.md` (13,084 bytes) |
| Purpose | The canonical DOD reference for Manual Slop; imported by `AGENTS.md` and injected via `manual_slop.toml` |
| Sections | Tier 0/1/2; 3 defaults to reject; 8 core defaults; "get the real data"; 7-question simplification pass; 10-question self-check; enforceable deliverables |
| Effort | Small (1-2 days) |
| Priority | HIGH (Candidate 14's foundation) |

### 11.2 The 1 `AGENTS.md` update

| Field | Value |
|---|---|
| File path | `AGENTS.md` (existing; update) |
| Type | UPDATE |
| Change | Add `@conductor/code_styleguides/data_oriented_design.md` at the top; add a "what this is" section mirroring nagent's `CLAUDE.md` |
| Purpose | The agent-facing rules file for the project root |
| Effort | Small (1-2 hours) |
| Priority | HIGH (Candidate 14) |

### 11.3 The 1 new `./docs/AGENTS.md`

| Field | Value |
|---|---|
| File path | `./docs/AGENTS.md` (NEW) |
| Type | NEW |
| Purpose | The agent-facing mirror of `docs/Readme.md` (which stays human-facing, unchanged) |
| Content | Which `docs/guide_*.md` is for which MMA tier; the 4 memory dimensions; the caching strategy; the styleguide index |
| Effort | Small (1-2 hours) |
| Priority | HIGH (Candidate 14) |

### 11.4 The 5 new styleguides

| # | File path | Purpose | Source for content |
|---|---|---|---|
| 1 | `conductor/code_styleguides/agent_memory_dimensions.md` | Codify the 4 memory dimensions; rules for when to use each | v2.3 §3.1 + v2.3 §10.3 |
| 2 | `conductor/code_styleguides/rag_integration_discipline.md` | Codify the conservative-RAG rule; opt-in default; provenance; no mutation; feature-gated | v2.3 §2.8 (the RAG row in the verdict table) |
| 3 | `conductor/code_styleguides/cache_friendly_context.md` | Codify stable-to-volatile ordering; the cache TTL GUI contract; the byte-comparison test | v2.3 §5.4 |
| 4 | `conductor/code_styleguides/knowledge_artifacts.md` | Codify the knowledge harvest pattern; category files; provenance; sha256 ledger; digest regeneration; "delete to turn off" | v2.3 §3.1 + §4 |
| 5 | `conductor/code_styleguides/feature_flags.md` | Codify "delete to turn off" (file presence) + "config.toml flag" (config); when to use each | v2.3 §3.10 |

### 11.5 The 3 new project docs

| # | File path | Purpose | Cross-ref |
|---|---|---|---|
| 1 | `docs/guide_knowledge_curation.md` | The knowledge memory guide; how to use the harvest, the digest, the per-file notes | v2.3 §3.1 + §4 |
| 2 | `docs/guide_caching_strategy.md` | Caching across providers; the stable-to-volatile ordering; the cache TTL GUI | v2.3 §3.2 + §3.3 + §5 |
| 3 | `docs/guide_agent_memory_dimensions.md` | Cross-cutting: the 4 memory dimensions; how each is used in features | v2.3 §10.3 |

### 11.6 The 4 workflow doc updates

| # | File path | Change | Reason |
|---|---|---|---|
| 1 | `conductor/workflow.md` | Add TDD protocol for the new patterns (harvest, cache, compaction, knowledge); the byte-comparison test for caching; the 10-question self-review for compaction | The workflow should reflect the v2.3 patterns |
| 2 | `conductor/product-guidelines.md` | Add the memory dimensions section; the "delete to turn off" pattern | The product guidelines should reflect the v2.3 framing |
| 3 | `docs/guide_mma.md` | Use the "delegation as context management" framing in the Token Firewalling section | Per v2.3 §3.12 |
| 4 | `docs/guide_ai_client.md` | Add the cache TTL section; the per-discussion caching decision; the cache health panel | Per v2.3 §3.3 + §5.3 |

### 11.7 The format commitment (for all 14 new files)

| Property | Value |
|---|---|
| Format | The 7-column "Symbol, Name, Signature, Semantics, Example, Source, Shape" table where applicable |
| No JSON | JSON code blocks become tables or line-based arrays |
| SSDL | Use `[I]`, `->`, `o->`, `=>`, `->M->`, `->B->`, `[B]`, `[M]`, `[N]`, `[Q]`, `[S]`, `[T]`, `───` |
| Forth/array notation | `a b +` for postfix math; `name := value` for assignment; `if cond { body }` for control flow |
| Code blocks | With `───` data flow lines and `+--+` boxes for visual structure |
| File:line citations | Both nagent source (`bin/nagent:606-745`) and Manual Slop source (`src/aggregate.py:run`) |
| ASCII sketches | Per the `docs/reports/ascii_sketch_ux_workflow_20260608.md` convention: `[+/-]`, `[Role: AI v]`, `|text|`, `<click to expand>`, `in:N out:N cache:N`, `@YYYY-MM-DDTHH:MM:SS` |

### 11.8 The 14 artifacts summary (the meta-table)

| Type | Count | Files |
|---|---|---|
| Canonical DOD file | 1 | `conductor/code_styleguides/data_oriented_design.md` |
| AGENTS.md update | 1 | `AGENTS.md` |
| New agent-facing doc | 1 | `./docs/AGENTS.md` |
| New styleguides | 5 | (per §11.4) |
| New project docs | 3 | (per §11.5) |
| Workflow doc updates | 4 | (per §11.6) |
| **Total new/touched files** | **15** | |

### 11.9 The preserved files (do NOT touch)

| File | Why preserved |
|---|---|
| `Readme.md` (project root) | Human-facing, per user instruction |
| `docs/Readme.md` (docs index) | Human-facing, per user instruction |
| `conductor/tracks/nagent_review_20260608/report.md` | v1 review artifact |
| `conductor/tracks/nagent_review_20260608/comparison_table.md` | v1 review artifact |
| `conductor/tracks/nagent_review_20260608/decisions.md` | v1 review artifact |
| `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md` | v1 review artifact |
| `conductor/tracks/nagent_review_20260608/spec.md` | v1 track spec |
| `conductor/tracks/nagent_review_20260608/nagent_review_v2_20260612.md` | v2 draft (preserved per user instruction) |
| `conductor/tracks/nagent_review_20260608/nagent_review_v2_1_20260612.md` | v2.1 user-revised (preserved per user instruction) |
| `conductor/tracks/nagent_review_20260608/nagent_review_v2_2_20260612.md` | v2.2 focused delta (preserved) |
| `conductor/tracks/nagent_review_20260608/metadata.json` | (updated; the previous v2.2 block is preserved) |
| `conductor/tracks/nagent_review_20260608/state.toml` | (updated; the previous v2.2 tasks are preserved) |

---

## 12. Recommended next steps (the action plan)

The user (the product owner) said: "After we'll look into updating upcoming tracks and documentation related to it, along with the agent workflow docs." This section is the recommended sequence.

### 12.1 The next turn's recommended sequence

| Step | Scope | Effort | Output |
|---|---|---|---|
| 1 | User review of v2.3 | — | Feedback + chosen new artifacts |
| 2 | Canonical DOD file (Candidate 14's foundation) | 1-2 days | `conductor/code_styleguides/data_oriented_design.md` |
| 3 | `AGENTS.md` update | 1-2 hours | `AGENTS.md` with `@import` line + "what this is" section |
| 4 | `./docs/AGENTS.md` | 1-2 hours | Agent-facing mirror of `docs/Readme.md` |
| 5 | 5 new styleguides | 2-3 days | All in §11.4 (5 files) |
| 6 | 3 new project docs | 2-3 days | All in §11.5 (3 files) |
| 7 | 4 workflow doc updates | 1-2 days | All in §11.6 (4 files) |
| 8 | Candidate 13 verification (TBD) | 1 day | `src/ai_client.py:run_discussion_compression` source read |

**Total.** 2-3 weeks. The foundation (steps 2-4) takes 1-2 days; the styleguides take 2-3 days; the project docs take 2-3 days; the workflow updates take 1-2 days.

### 12.2 The follow-on turn's recommended sequence (the high-priority candidates)

| Step | Scope | Effort | Output |
|---|---|---|---|
| 9 | Candidate 14 full implementation (canonical DOD + AGENTS.md + docs/AGENTS.md + injection) | already in step 2-4 | (deployed) |
| 10 | Candidate 8 (`KnowledgeMemory`) Phase 1 (Foundation) | 1 week | `KnowledgeStore` + digest regeneration + 5 unit tests; `paths.py` helper; `FileItem.notes` |
| 11 | Candidate 8 Phase 2 (Harvester) | 1-2 weeks | `KnowledgeHarvester` + harvest prompt + CLI + 8 unit tests; ledger |
| 12 | Candidate 8 Phase 3 (Injection) | 1 week | `aggregate.py:run` `{knowledge}` + `{file-knowledge}` blocks |
| 13 | Candidate 11 (`Compaction`) | 1-2 weeks | `run_discussion_compaction` + prompt + GUI button + 10 self-review tests |
| 14 | Candidate 9 + 10 (`CacheOrdering` + `CacheTTL`) | 2-3 weeks | `aggregate.py` re-ordering + Anthropic cache control + Gemini explicit + GUI panel + 5 live_gui tests |
| 15 | Candidate 1 (`SubConversationRunner`) | 1-2 weeks | `SubConversationRunner` class + GUI controls + 3 live_gui tests |
| 16 | Candidate 2 (`RAGPreStager`) | 1 week | RAG staging sub-conversation + the prompt + 2 live_gui tests |

**Total.** 3-4 months (sequential) or 2 months (parallel with 2 workers).

### 12.3 The longer-term roadmap (the next 6-12 months)

| Phase | Scope | Output |
|---|---|---|
| 17 | All remaining LOW-priority candidates (5, 7, 12, 15, 16) | 1-2 months |
| 18 | Candidate 3 (`LLMClient` stateless) — the big refactor | 2-3 months |
| 19 | Candidate 4 (`IntentDSL`) — the research spike | 1-2 months |
| 20 | Track archive + conductor integration | 1-2 weeks |

### 12.4 The format commitment (recap)

All new artifacts in the next turn follow:
- 7-column "Symbol, Name, Signature, Semantics, Example, Source, Shape" table format
- No JSON code blocks
- SSDL shape tags
- Forth/array notation in code examples
- File:line citations into both nagent source and Manual Slop source
- ASCII sketches for GUI panels (per the `docs/reports/ascii_sketch_ux_workflow_20260608.md` convention)

### 12.5 The open questions (for the user)

| # | Question | Why it matters |
|---|---|---|
| 1 | Confirm the format commitment (per §11.7) — yes/no? | Drives all 14 new files |
| 2 | Confirm the 5 HIGH-priority candidates (1, 8, 11, 14; +9 de-facto HIGH) | Drives the next-turn sequencing |
| 3 | Confirm the 14 new artifacts in §11 | Drives the scope of the next turn |
| 4 | Any new user flags since v2.3 was drafted? | Surfaces late changes |
| 5 | Should v2.3 itself be the final report (vs another v2.4)? | The series of revisions needs to converge |

---

## 13. References

### 13.1 nagent source (read in full for this review)

| File | Lines | What it provides |
|---|---|---|
| `bin/nagent` | 2,524 | The main loop; `build_initial_context` at 606-745; `conversation_cache_boundaries` at 970-987; `call_llm` at 990-1019; `compact_conversation` at 1975-2019; `--save-conversation` at 2147; `--branch-conversation` at 2157; `--compact` at 2178 |
| `bin/nagent-llm-text` | 50 | The LLM text wrapper |
| `bin/nagent-llm-upload` | 80 | The LLM upload wrapper |
| `bin/nagent-file-edit` | 120 | The per-file conversation CLI |
| `bin/nagent-file-patch` | 80 | The patch CLI |
| `bin/nagent-file-split` | 170 | The split CLI |
| `bin/nagent-file-summarize` | 100 | The summarize CLI |
| `bin/nagent-gc` | 150 | **The knowledge harvest CLI (NEW)** |
| `bin/helpers/nagent-cli.py` | 2,642 | `exit_on_description`; `collect_bin_tool_descriptions`; `WaitSpinner` |
| `bin/helpers/nagent-llm.py` | 20,366 | **5+1 providers; `cache_prefix_blocks`; `_result_with_usage` fold-back (NEW)** |
| `bin/helpers/nagent-tags.py` | 6,036 | **The explicit tag parser (NEW, replaces regex)** |
| `bin/helpers/nagent-gc-lib.py` | 27,289 | **The knowledge harvest library (NEW)** |
| `bin/helpers/nagent-file-edit-lib.py` | 5,232 | `file_id_for_path`; the per-pid registry |
| `bin/helpers/nagent-file-split-lib.py` | 15,427 | 12-language splitter; `SCORE_BY_TYPE`; O(n²) → O(n) fix |
| `bin/helpers/nagent-file-patch-lib.py` | 5,086 | `validate_index`; `merge_segments`; `make_unified_patch` |
| `bin/helpers/nagent-file-summarize-lib.py` | 3,884 | `SUMMARY_MAX_ATTEMPTS=2`; per-segment LLM call |
| `bin/helpers/nagent-file-split-{12 langs}` | 12 × ~225B = ~2,700 | Per-language thin wrappers |
| `prompts/compact-conversation.md` | 3,237 | **The compaction prompt (NEW, editable, root-first)** |
| `prompts/harvest-conversation.md` | 1,674 | **The harvest prompt (NEW, editable, root-first)** |
| `context/data-oriented-design.md` | 13,084 | **The canonical DOD reference (NEW)** |
| `context.yaml` | 34 | The root context pointer (`paths: [context/data-oriented-design.md]`) |
| `CLAUDE.md` | 5,832 | **The agent-facing rules file with `@import` pattern (NEW)** |
| `requirements.txt` | 94 | Dependencies (`claude-agent-sdk` + standard SDKs) |
| `config.example.json` | 49 | The config template |
| `tests/test-nagent.py` | 106,128 | The main test file |
| `tests/test-nagent-file-edit.py` | 28,393 | The file-edit tests |
| `tests/test-nagent-file-split.py` | 11,525 | The split tests |
| `tests/test-nagent-file-patch.py` | 8,001 | The patch tests |
| `tests/test-nagent-file-summarize.py` | 9,106 | The summarize tests |
| `tests/test-nagent-gc.py` | 27,306 | **The GC tests (NEW)** |
| `tests/test-nagent-tags.py` | 5,902 | **The tag parser tests (NEW)** |

### 13.2 Manual Slop source (read selectively for this review)

| File | Lines | What it provides |
|---|---|---|
| `src/aggregate.py` | 518 | The context composition pipeline; `run` is the consumer entry point |
| `src/ai_client.py` | 2,883 | The multi-provider LLM client; `_add_history_cache_breakpoint`; `_send_<provider>`; `run_discussion_compression`; `run_subagent_summarization` |
| `src/rag_engine.py` | 384 | The RAG engine; ChromaDB-backed; `RAGEngine.search()` |
| `src/models.py` | (large) | `FileItem` schema at line ~510; `ContextPreset` at line ~909 |
| `src/mcp_client.py` | (large) | The 45 MCP tools; the 3-layer security model |
| `src/app_controller.py` | (large) | The headless controller; `app_state`; `_handle_compress_discussion` at line ~3357 |
| `src/gui_2.py` | (large) | The ImGui GUI; `render_discussion_entry` at line ~3770 (per-entry A1-A7); `render_discussion_entry_controls` at line ~4239 (B1-B11); Compress button at line ~4252 |
| `src/context_presets.py` | (small) | The `ContextPresetManager` |
| `src/history.py` | (small) | `HistoryManager` + `UISnapshot` |
| `src/paths.py` | (small) | The path resolution module |
| `src/commands.py` | (large) | The 33 Command Palette commands |
| `src/command_palette.py` | (large) | The Command Palette UI |

### 13.3 Manual Slop docs (read for this review)

| File | What it provides |
|---|---|
| `docs/Readme.md` | The docs index (preserved, human-facing) |
| `docs/guide_architecture.md` | Threading model; cross-thread state sync |
| `docs/guide_ai_client.md` | The multi-provider LLM client |
| `docs/guide_mma.md` | The 4-tier MMA orchestration |
| `docs/guide_tools.md` | The MCP tool inventory + Hook API |
| `docs/guide_mcp_client.md` | The 45 tools + 3-layer security |
| `docs/guide_app_controller.md` | The headless controller |
| `docs/guide_context_curation.md` | The Granular AST Control + Fuzzy Anchors |
| `docs/guide_personas.md` | The unified agent profile model |
| `docs/guide_rag.md` | The RAG subsystem |
| `docs/guide_gui_2.md` | The ImGui application |
| `docs/guide_meta_boundary.md` | The Application vs Meta-Tooling split (load-bearing) |
| `docs/guide_testing.md` | The test suite architecture (251 test files) |
| `docs/guide_command_palette.md` | The 33 commands + "Everything" mode |
| `docs/reports/computational_shapes_ssdl_digest_20260608.md` | The 6 SSDL primitives + 7 modifiers (style reference) |
| `docs/reports/ascii_sketch_ux_workflow_20260608.md` | The 10 ASCII sketch conventions (style reference) |
| `docs/reports/proposed_new_tracks_20260608.md` | The 4-tier proposal format (style reference) |

### 13.4 External references

| Reference | URL | What it provides |
|---|---|---|
| nagent repo | `https://github.com/macton/nagent` | The full source at commit `eb6be32a` |
| nagent README | `https://github.com/macton/nagent/blob/main/README.md` | The 7-Part + 14-section teaching-arc structure |
| nagent CLAUDE.md | `https://raw.githubusercontent.com/macton/nagent/main/CLAUDE.md` | The agent-facing rules file (5,832 bytes) |
| nagent context/data-oriented-design.md | `https://raw.githubusercontent.com/macton/nagent/main/context/data-oriented-design.md` | The canonical DOD reference (13,084 bytes) |
| nagent prompts/compact-conversation.md | `https://raw.githubusercontent.com/macton/nagent/main/prompts/compact-conversation.md` | The compaction prompt (3,237 bytes) |
| nagent prompts/harvest-conversation.md | `https://raw.githubusercontent.com/macton/nagent/main/prompts/harvest-conversation.md` | The harvest prompt (1,674 bytes) |
| nagent nagent-gc source | `https://raw.githubusercontent.com/macton/nagent/main/bin/nagent-gc` | The GC CLI wrapper |
| nagent nagent_gc_lib source | `https://raw.githubusercontent.com/macton/nagent/main/bin/helpers/nagent_gc_lib.py` | The knowledge harvest library |
| nagent nagent_tags source | `https://raw.githubusercontent.com/macton/nagent/main/bin/helpers/nagent_tags.py` | The explicit tag parser |
| nagent nagent_llm source | `https://raw.githubusercontent.com/macton/nagent/main/bin/helpers/nagent_llm.py` | The provider abstraction + cache_prefix_blocks |
| nagent bin/nagent source | `https://raw.githubusercontent.com/macton/nagent/main/bin/nagent` | The main loop |
| nagent 8-commit log | `https://api.github.com/repos/macton/nagent/commits?per_page=8` | The 8 new commits (2026-06-08 → 2026-06-12) |
| nagent 33-file tree | `https://api.github.com/repos/macton/nagent/git/trees/main?recursive=1` | The 33 files (incl. the new ones) |

### 13.5 The preservation list (recap)

Per the user's repeated instructions:
- `Readme.md` and `docs/Readme.md` stay human-facing; the new agent-facing files are separate (`AGENTS.md` + `./docs/AGENTS.md`)
- The v1 review artifacts are preserved (`report.md`, `comparison_table.md`, `decisions.md`, `nagent_takeaways_20260608.md`)
- The v2, v2.1, and v2.2 reviews are preserved
- The v2.3 is the current comprehensive review (this file)
- The proposed new artifacts are NEW files; none override the existing ones

### 13.6 The cross-references to other tracks

| Track | Relationship to v2.3 |
|---|---|
| `data_oriented_error_handling_20260606` | Foundational: the `Result[T, ErrorInfo]` envelope is the shape the harvest + compaction LLM calls return |
| `mcp_architecture_refactor_20260606` | The sub-MCP extraction is the right scope for nagent's `--description` self-describing pattern (Candidate 5) |
| `qwen_llama_grok_integration_20260606` | The `send_openai_compatible()` helper is the right shape for the claude-code provider integration (Candidate 5 / not a new track) |
| `qwen_llama_grok_followup_20260611` | The follow-up; includes `Result` migration in the public API |
| `public_api_migration_20260606` (planned) | The deprecated `ai_client.send()` removal; the foundation for Candidate 3 (`LLMClient` stateless) |
| `startup_speedup_20260606` | The main-thread-purity invariant; relevant to the GUI panel design for Candidates 8, 10, 11 |
| `test_infrastructure_hardening_20260609` | The test infra; the foundation for the new live_gui tests |
| `intent_dsl_survey_20260612` (the related Meta-Tooling-side work) | The intent-based DSL research; the inspiration for the per-MCP verb catalog (Candidate 4 territory) |
| `manual_ux_validation_20260608_PLACEHOLDER` | The ASCII-sketch UX workflow; the format reference for the GUI panels |

### 13.7 The file:line citation index (the nagent source map)

For quick reference, the nagent source citations used throughout this review:

| Citation | File:line | Used in |
|---|---|---|
| `bin/nagent:606-745` | `build_initial_context` | §2.1, §2.10, §3.2, §5.1, §7.3 |
| `bin/nagent:631-641` | `install_context` injection | §3.5, §7.4 |
| `bin/nagent:642-657` | `project_context_block` | §3.5, §7.4 |
| `bin/nagent:677-685` | `knowledge_block` injection | §3.1, §4.1 |
| `bin/nagent:687-690` | "Block order is stable-to-volatile" comment | §3.2, §5.1 |
| `bin/nagent:696-706` | The 8-tag list | §2.2, §7.3, §8.1 |
| `bin/nagent:708-713` | The 5 protocol rules | §2.2, §7.3, §8.6 |
| `bin/nagent:715-731` | The conversations-are-data block | §3.12, §8.2 |
| `bin/nagent:970-987` | `conversation_cache_boundaries` | §3.2, §5.1 |
| `bin/nagent:990-1019` | `call_llm` (the --cache-prefix-chars flow) | §3.2, §5.1 |
| `bin/nagent:1013-1014` | `command.extend(["--cache-prefix-chars", str(boundary)])` | §3.2, §5.1 |
| `bin/nagent:1975-2019` | `compact_conversation` | §3.4, §6.4 |
| `bin/nagent:1965-1972` | `compact_prompt_path` (root-first resolution) | §3.4, §6.4 |
| `bin/nagent:2147-2156` | `--save-conversation` | §3.11 |
| `bin/nagent:2157-2170` | `--branch-conversation` | §3.11 |
| `bin/nagent:2178` | `--compact` | §3.4, §6.4 |
| `bin/helpers/nagent_gc_lib.py:1-700` | The full harvest library | §3.1, §4 |
| `bin/helpers/nagent_gc_lib.py:13-15` | The 3 budget constants | §3.1, §4.5 |
| `bin/helpers/nagent_gc_lib.py:25-30` | The category files map | §3.1, §4.1 |
| `bin/helpers/nagent_gc_lib.py:80+` | `scan_root` | §3.1, §4.2 |
| `bin/helpers/nagent_gc_lib.py:130+` | `load_ledger` / `save_ledger` | §3.1, §4.1 |
| `bin/helpers/nagent_gc_lib.py:180+` | `parse_harvest_json` | §3.1, §4.3 |
| `bin/helpers/nagent_gc_lib.py:235+` | `harvest_conversation` (the retry budget) | §3.1, §4.3 |
| `bin/helpers/nagent_gc_lib.py:245+` | `merge_harvest` (the "files" branch) | §3.1, §3.9, §4.4 |
| `bin/helpers/nagent_gc_lib.py:380+` | `regenerate_digest` (the "delete to turn off") | §3.1, §3.10, §4.1 |
| `bin/helpers/nagent_llm.py:65-80` | `PROVIDERS, DEFAULT_MODELS, CREDENTIAL_ENV` | §2.1, §3.6, §7.4 |
| `bin/helpers/nagent_llm.py:195-220` | `_claude_code_generate` | §3.6 |
| `bin/helpers/nagent_llm.py:cache_prefix_blocks` | The cache_prefix_blocks function | §3.2, §5.1 |
| `bin/helpers/nagent_llm.py:_result_with_usage` | The cache token fold-back | §3.2, §5.1 |
| `bin/helpers/nagent_tags.py:1-160` | The full tag parser | §7.3, §8.4 |
| `bin/helpers/nagent_file_edit_lib.py:file_id_for_path` | The st_dev:st_ino pattern | §2.13, §7.4 |
| `bin/helpers/nagent_file_split_lib.py:SCORE_BY_TYPE` | The per-language scoring | §2.12, §9.2 |
| `bin/helpers/nagent_file_split_lib.py` (O(n) fix) | The perf fix (13.6s → 0.008s) | §1.4, §9.6 |
| `bin/helpers/nagent_file_patch_lib.py:validate_index` | The strict hash check | §2.12, §9.4 |
| `bin/helpers/nagent_file_summarize_lib.py:summarize_content` | The per-segment LLM call + retry | §2.12, §9.5 |
| `bin/nagent-gc:75-130` | The CLI surface | §3.1, §4.2 |
| `CLAUDE.md:1-150` | The agent-facing rules file (the `@import` pattern) | §3.8 |
| `context/data-oriented-design.md:1-1000+` | The canonical DOD reference | §3.7 |
| `prompts/compact-conversation.md:1-100` | The 12-section output structure | §3.4, §6.2 |
| `prompts/compact-conversation.md:90-110` | The 10-question self-review | §3.4, §6.3 |
| `prompts/harvest-conversation.md:1-30` | The strict-JSON output schema | §3.1, §4.1 |
| `prompts/harvest-conversation.md:25-35` | The 7 category rules | §3.1, §4.1 |

### 13.8 The file:line citation index (the Manual Slop source map)

For quick reference, the Manual Slop source citations used throughout this review:

| Citation | File:line | Used in |
|---|---|---|
| `src/aggregate.py:run` | The context composition entry point | §3.2, §3.5, §5.2 |
| `src/aggregate.py:build_file_items` | The FileItem builder | §2.12 |
| `src/ai_client.py:2883` | (module size) | §2.1 |
| `src/ai_client.py:send` | The main send function | §2.1, §10.11 |
| `src/ai_client.py:_send_anthropic` | The Anthropic provider | §3.2, §5.1, §5.6 |
| `src/ai_client.py:_send_gemini` | The Gemini provider (with explicit caching) | §3.3, §5.6 |
| `src/ai_client.py:_send_gemini_cli` | The Gemini CLI provider (parallels nagent's claude-code) | §3.6 |
| `src/ai_client.py:_add_history_cache_breakpoint` | The history cache breakpoint | §3.2, §5.2 |
| `src/ai_client.py:_result_with_usage` | (per-provider; the fold-back) | §3.2, §5.1 |
| `src/ai_client.py:_strip_cache_controls` | The history cache control strip | §3.2 |
| `src/ai_client.py:_build_chunked_context_blocks` | The chunked context blocks | §3.2, §5.6 |
| `src/ai_client.py:_reread_file_items` | The mtime-based diff injection | §2.7, §3.2 |
| `src/ai_client.py:_truncate_tool_output` | The tool output truncation | §2.3 |
| `src/ai_client.py:run_discussion_compression` | The existing Compress path | §3.4, §3.11, §6.6 |
| `src/ai_client.py:run_subagent_summarization` | The existing in-process summarization | §2.3 |
| `src/ai_client.py:_ANTHROPIC_CHUNK_SIZE` | The Anthropic chunk size constant | §3.2, §5.1 |
| `src/ai_client.py:_ANTHROPIC_MAX_PROMPT_TOKENS` | The Anthropic max prompt constant | §3.2, §5.1 |
| `src/ai_client.py:_GEMINI_CACHE_TTL` | The Gemini cache TTL constant | §3.3, §5.3 |
| `src/ai_client.py:PROVIDERS` | The providers constant | §2.1 |
| `src/ai_client.py:MAX_TOOL_ROUNDS` | The tool round cap | §2.3 |
| `src/ai_client.py:_CHARS_PER_TOKEN` | The token estimation constant | §2.1 |
| `src/rag_engine.py:1-384` | The RAG engine | §2.8, §3.3 |
| `src/rag_engine.py:RAGEngine.search` | The semantic search | §2.8, §3.3 |
| `src/rag_engine.py:RAGEngine.index_file` | The file indexer | §2.8, §10.10 |
| `src/rag_engine.py:_validate_collection_dim` | The dim-mismatch fix (per `16412ad5`) | §3.3 |
| `src/models.py:510-559` | The `FileItem` schema | §2.6, §3.9, §4.7 |
| `src/models.py:909-937` | The `ContextPreset` schema | §2.6 |
| `src/app_controller.py:3357` | `_handle_compress_discussion` | §3.4, §6.6 |
| `src/app_controller.py:3503` | `_branch_discussion` | §2.6 |
| `src/app_controller.py:3236` | (the discussion save flush) | §2.6 |
| `src/app_controller.py:716` | (the comms.log ring buffer) | (background reference) |
| `src/gui_2.py:3770` | `render_discussion_entry` | §2.6 |
| `src/gui_2.py:3789` | The `+/-` collapsed toggle | §2.6 |
| `src/gui_2.py:3793-3796` | The role combo | §2.6 |
| `src/gui_2.py:3799` | The `[Edit]/[Read]` toggle | §2.6 |
| `src/gui_2.py:3813` | The `Ins` button | §2.6 |
| `src/gui_2.py:3815-3816` | The `Del` button | §2.6 |
| `src/gui_2.py:3821` | The `Branch` button | §2.6 |
| `src/gui_2.py:3841` | The `imgui.input_text_multiline` | §2.6 |
| `src/gui_2.py:3855` | `render_discussion_entry_read_mode` | §2.6 |
| `src/gui_2.py:4239-4260` | `render_discussion_entry_controls` (B1-B11) | §2.6 |
| `src/gui_2.py:4252` | The Compress button | §3.4, §6.6 |
| `src/gui_2.py:5163+` | The MMA spawn-approval modal | (background reference) |
| `src/commands.py` | The 33 commands | §3.4 (background) |
| `src/command_palette.py` | The Command Palette | §3.4 (background) |
| `src/context_presets.py` | The `ContextPresetManager` | §2.6, §3.1 |
| `src/history.py:8-63` | `UISnapshot` | §2.6 |
| `src/history.py:71` | `HistoryManager(max_capacity=100)` | §2.6 |
| `src/paths.py` | The path resolution module | §3.5, §3.8 |
| `src/multi_agent_conductor.py:_spawn_worker` | The MMA worker spawn | §2.5, §3.12 |
| `src/multi_agent_conductor.py:run_worker_lifecycle` | The worker lifecycle | §2.5, §3.12 |
| `src/multi_agent_conductor.py:ConductorEngine.run` | The MMA engine | §2.5, §3.12 |
| `src/mcp_client.py:dispatch` | The 45-tool dispatch | §2.4, §3.8 |
| `src/mcp_client.py:_is_allowed` | The 3-layer security | §2.10, §7.5 |
| `src/mcp_client.py:_resolve_and_check` | The path validation + resolution gate | §2.10, §7.5 |
| `src/mcp_client.py:get_tool_schemas` | The tool capability declaration | §2.4 |

### 13.9 The state of the world (this commit)

| File | Status |
|---|---|
| `nagent_review_v2_20260612.md` | v2 draft, preserved |
| `nagent_review_v2_1_20260612.md` | v2.1 user-revised, preserved |
| `nagent_review_v2_2_20260612.md` | v2.2 focused delta, preserved |
| `nagent_review_v2_3_20260612.md` | **THIS FILE** — the full rewrite, the comprehensive review |
| `report.md` + `comparison_table.md` + `decisions.md` + `nagent_takeaways_20260608.md` | v1 review artifacts, preserved |
| `spec.md` | The v1 track spec, preserved |
| `metadata.json` | (updated; the v2.2 block is preserved; the v2.3 block is added) |
| `state.toml` | (updated; the v2.2 tasks are preserved; the v2.3 tasks are added) |
| `Readme.md` (project root) | Human-facing, preserved |
| `docs/Readme.md` | Human-facing, preserved |
| The 14 new artifacts (per §11) | **NOT YET CREATED** — proposed for the next turn |

### 13.10 The end (the meta-summary)

This v2.3 is the most comprehensive review of the latest nagent corpus. It:
- Covers all 14 patterns in the README in depth (with file:line citations)
- Deep-dives the 12 new additions (knowledge harvest, cache strategy, compaction, project context, claude-code provider, shared DOD, CLAUDE.md pattern, per-file notes, "delete to turn off", save-with-graceful-failure, delegation reframing)
- Deep-dives the 3 major new patterns (harvest pipeline, cache strategy, compaction)
- Covers the architecture (4 reading levels, tag protocol, durable state model, write boundaries, large-file pipeline)
- Covers the vocabulary (8 tags + per-tag guidance + the 4-tier structure)
- Covers the file-ops (split / patch / summarize)
- Lists the 16 future-track candidates in priority order
- Proposes 14 new artifacts for the next turn
- Commits to a format (7-column tables, no JSON, SSDL tags, forth/array notation)
- Preserves all prior v1, v2, v2.1, v2.2 reviews and the human Readme files
- Recommends a sequence for the next turn (canonical DOD → AGENTS.md updates → styleguides → project docs → workflow updates)

The next turn's work is: confirm the format commitment, confirm the 5 HIGH-priority candidates, confirm the 14 new artifacts, then execute.

End of v2.3 report.

