Private
Public Access
0
0

feat(directives): scavenge from nagent_review_20260608/: 5 directives

This commit is contained in:
2026-07-04 00:13:24 -04:00
parent bea5d6b151
commit ebca201d39
10 changed files with 174 additions and 0 deletions
@@ -0,0 +1,9 @@
# decompose_or_isolate_never_offload
## v1
**Why this iteration:** Lifted from `conductor/tracks/nagent_review_20260608/nagent_takeaways_v3_20260619.md:71-72` (Candidate 22 — "Tier 3 worker contract 'decompose or isolate, never offload'"). The two reasons delegation is worth its cost are named explicitly: decomposition (genuinely complex, with parts) and context isolation (noisy step, small result). The recursion bug fixed by this naming is in the same source.
**Source:** `conductor/tracks/nagent_review_20260608/nagent_takeaways_v3_20260619.md:71-72`
---
**Lifted:** 2026-07-02 (scavenge pass — directive library expansion from nagent_review track)
@@ -0,0 +1,27 @@
# Delegation is justified by decomposition or context isolation — never as a single-action offload
## The two-reason rule
A Tier 2 agent (or any orchestrator) may delegate a task to a sub-agent only when one (or both) of the following is true:
1. **Decomposition.** The task is genuinely complex, with multiple parts. The sub-agent handles one part; the parent composes the answers.
2. **Context isolation.** The step is noisy (large file reads, long shell output, retries) but the *result* is small. The sub-agent absorbs the noise; the parent gets a distilled artifact.
Delegation is NOT justified for:
- A single small action whose result is no smaller than doing it directly.
- A trivial computation that the parent could do inline.
- "It feels cleaner" without one of the two reasons above.
Per `conductor/tracks/nagent_review_20260608/nagent_takeaways_v3_20260619.md:71-72`: the v3 delegation rewrite "fixes a recursion bug (file-edit agent → worker → nagent-file-edit → file-edit agent → ... hangs the tree) by naming the two reasons delegation is worth its cost: decomposition ... and context isolation. 'Don't offload a single small action whose result is no smaller than doing it yourself.'"
## Why
Delegation has costs: serialization, IPC, model invocation latency, the prompt-boundary summary. If the delegated task is one trivial operation, the parent pays all those costs to get back what it could have computed in one line. Worse, delegation creates a recursion bug: the parent → worker → sub-worker → sub-sub-worker → ... loop that hangs the tree.
## What this means in practice
- The Tier 3 worker system prompt carries this rule as an explicit contract.
- A test (`tests/test_<delegation>.py`) asserts the worker contract prefix is present in the initial context.
- A test asserts the recursion case is caught (a worker that tries to delegate to itself is rejected).
- "Reasonable" judgment calls are explicit: when in doubt, do it inline; when the noise is large, delegate.
@@ -0,0 +1,9 @@
# file_id_stable_across_rename
## v1
**Why this iteration:** Lifted from `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:128-130` (Pattern 4 — "File-identity over file-path — a stable st_dev:st_ino is rename-safe"). The nagent primitive is `file_id_for_path(path) -> "{st_dev}:{st_ino}"`; the same primitive applies to Manual Slop's `FileItem` and any per-file durable cache.
**Source:** `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:128-130`
---
**Lifted:** 2026-07-02 (scavenge pass — directive library expansion from nagent_review track)
@@ -0,0 +1,26 @@
# Durable per-file memory is keyed by file_id (st_dev:st_ino), not by path — renames preserve identity
## The identity primitive
When memory is attached to a specific file on disk (a per-file conversation, a curation snapshot, a summary cache), the lookup key is the file's inode pair `{st_dev}:{st_ino}`, not the path string.
Per `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:128-130`: "nagent_file_edit_lib.py:file_id_for_path(path) -> '{st_dev}:{st_ino}'. The per-file conversation index keys by inode, not by path. Rename the file in place (same inode) → same conversation."
## Why
Path-keyed memory is fragile. A user renames `src/foo.py` to `src/bar.py`, and the curation snapshot, the per-file conversation log, and the summary cache all silently orphan. The data is still on disk; the *index* lost its key.
Inode-keyed memory survives:
- Rename in place (`mv src/foo.py src/bar.py`) — same inode.
- Move across directories (`mv src/foo.py lib/foo.py`) — same inode on the same filesystem.
- Editor refactor that preserves the file content.
It does NOT survive:
- `git checkout` to a different commit (inode may change on checkout depending on filesystem).
- `rm` + recreate — new inode, new identity.
## What this means in practice
- Any `FileItem`, `ContextPreset`, or per-file cache field that tracks durable state has a `file_id: str = "{st_dev}:{st_ino}"` field populated at load time.
- Lookups prefer `file_id`; fallback to fuzzy match on basename + directory tree.
- The schema migration: existing path-only files get a `file_id` added on first read; the path field stays for human-readable display.
@@ -0,0 +1,9 @@
# parse_failure_visible_to_conversation
## v1
**Why this iteration:** Lifted from `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:171-178` (Pattern 6 — "Visible retry on protocol failure — turn errors into conversation data"). The nagent pattern: bad output + `<system>` correction appended to the conversation; `MAX_FORMAT_RETRIES = 3`; the conversation is greppable for every retry.
**Source:** `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:171-178`
---
**Lifted:** 2026-07-02 (scavenge pass — directive library expansion from nagent_review track)
@@ -0,0 +1,21 @@
# Parse failures become conversation data — the bad output and the system correction are appended so the next call sees them
## The visibility contract
When a model's output fails to parse (malformed tool call, broken tag, missing closing delimiter), the failure is NOT silently retried inside the loop. The loop:
1. Appends the bad output to the conversation as a quoted block, with a system-level correction message ("Invalid format: {parse_error}. Respond only with valid tags.").
2. Lets the next call see its own previous failure + the correction.
3. Increments a visible counter so the user (and Tier 2 code review) can spot persistent parse failures.
Per `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:171-178`: the nagent pattern is `MAX_FORMAT_RETRIES = 3` + the bad output + the `<system>` correction are appended to the conversation. "The user can grep the conversation for `<system>` to find every retry."
## Why
Silent retries hide the model's actual failure mode. If the model keeps emitting bad XML because it misunderstood the schema, a silent retry loop burns context without the user (or Tier 2) ever seeing *what* the model is getting wrong. Surfacing the failure in the conversation makes the loop debuggable and gives the user a chance to fix the prompt.
## What this means in practice
- The diagnostic panel shows the last N parse failures: the model output + the error message + the timestamp.
- A persistent parse failure (> 3 retries) escalates: the conversation pauses with a "fix your prompt or accept this garbage" dialog.
- The conversation file is greppable for parse failures (`grep "<system>Invalid"`); the JSON-L comms log is not sufficient on its own.
@@ -0,0 +1,9 @@
# state_visible_at_the_right_layer
## v1
**Why this iteration:** Lifted from `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:33-38` (Pattern 1 — "State visibility — files for the things that matter, processes for the things that don't"). The nagent framing: state that *survives* is in `~/.nagent/`; state that doesn't survive is in the running process. The boundary is sharp.
**Source:** `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:33-38`
---
**Lifted:** 2026-07-02 (scavenge pass — directive library expansion from nagent_review track)
@@ -0,0 +1,31 @@
# Durable state lives in files the user can inspect — transient process state is acceptable only if it has an inspection surface
## The visibility boundary
The boundary is sharp: anything the user might want to inspect, diff, copy, or back up lives in a file. Anything else (an in-flight LLM call result, a parse state, a one-shot retry counter) stays in process memory.
Per `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:33-38`: "The state that *doesn't survive* is the running process: LLM call result, current turn, parse state. The boundary is sharp: anything the user might want to inspect, diff, copy, or back up is a file."
## What goes in files (durable)
- Project config: `manual_slop.toml`, project presets, personas
- Track state: `conductor/tracks/<id>/{spec,plan,state.toml,metadata.json}`
- Discussion history: `disc_entries` flushed to project TOML
- Logs: `comms.log` JSON-L, session artifacts
- Knowledge harvest: `~/.manual_slop/knowledge/*.md` (per the knowledge harvest pattern)
## What stays in process (transient, but inspectable)
- Provider history lists (`_anthropic_history`, etc.) — acceptable *if* a "Live State Inspector" GUI panel exposes them.
- Current `disc_entries` *before* the user flushes the discussion — the user's view is the source of truth; the JSON-L is the durable form.
- Cache state, token budgets, ephemeral worker scratch — short-lived; not a durability concern.
## Why
The user must be able to `cat`, `git diff`, `grep`, and `cp` the state that matters. Process globals make this impossible. The trade-off (latency for disk writes) is acceptable because Manual Slop is a single-developer expert tool, not a high-throughput service.
## What this means in practice
- New stateful subsystems land with a file-backed surface from the start; "we'll add disk persistence later" is rejected.
- Process globals that survive across turns must be exposed via a GUI inspector panel (the "Live State Inspector" pattern) or via the Hook API.
- A piece of state without a `cat`-able form is a bug, not a feature.
@@ -0,0 +1,9 @@
# subagent_returns_artifact_not_transcript
## v1
**Why this iteration:** Lifted from `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:285-296` (Pattern 10 — "Sub-agents return a concise artifact, not a full transcript"). The nagent pattern is the `<nagent-conversation-result>` tag with body + exit + tokens only; Manual Slop's `SubConversationResult` type is specified as `artifact: str + tokens_in + tokens_out + exit_code + errors: list[str]`.
**Source:** `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:285-296`
---
**Lifted:** 2026-07-02 (scavenge pass — directive library expansion from nagent_review track)
@@ -0,0 +1,24 @@
# Sub-agents return a concise artifact, never their full conversation log
## The artifact contract
When a sub-agent (a child invocation of a Tier 3 worker, a sub-conversation, or a Tier 4 QA call) finishes, the parent's context must receive only:
- The distilled answer (a string artifact — the response content)
- Token usage (in + out)
- Exit code / success status
- Errors (a short list of error strings, if any)
The sub-agent's intermediate reads, retries, tool calls, scratch reasoning, and dead-end attempts do NOT propagate to the parent's context. They are persisted to a separate log file (e.g., `~/.manual_slop/sub_conversations/<uuid>.jsonl`) for debugging but are invisible to the parent's next turn.
Per `conductor/tracks/nagent_review_20260608/nagent_takeaways_20260608.md:285-296`: the child's `<nagent-conversation-result>` contains "only the child's `<nagent-response>` body + exit code + stderr. The parent's conversation is *not* polluted with the child's intermediate reads, shell calls, or retries."
## Why
If every sub-agent's full conversation streamed into the parent, the parent's context would explode after 2-3 invocations. Worse, the parent would start reasoning about the *sub-agent's reasoning* rather than the *answer*, doubling the cognitive load and introducing cascading hallucinations.
## What this means in practice
- The `SubConversationResult` (or equivalent) type is `artifact: str + tokens_in: int + tokens_out: int + exit_code: int + errors: list[str]` — never a transcript.
- Parent's `disc_entries` gets exactly one new entry per sub-agent call: the artifact, as a User or System role.
- The sub-agent's full transcript is on disk; the parent does not see it.