Skip to content

Commit 5b11d8a

Browse files
authored
Merge pull request #14 from luohaha/fix/suggestions-mailbox
fix(orchestrator): per-attempt human notes via suggestions/NNN.md, drop the byte cursor
2 parents 7c2e40b + cad9aeb commit 5b11d8a

7 files changed

Lines changed: 63 additions & 77 deletions

File tree

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ the same session indefinitely.)
165165

166166
You stay in control throughout: read what each attempt did via
167167
`/goal-run` or `goaloop status`; **edit `goal.md`** for a permanent
168-
change or **append to `suggestions.md`** for a transient per-attempt
169-
note — the next attempt picks it up. There's no live conversation into a
170-
running Runner.
168+
change or **drop a `suggestions/NNN.md`** note for a transient per-attempt
169+
nudge — the Runner of that attempt reads it. There's no live conversation into
170+
a running Runner.
171171

172172
### Configuration & modes
173173

@@ -182,10 +182,12 @@ and waits for your approval before the next one; release it with
182182
`goaloop continue <name>`. (`pass`/`blocked`/`error` are terminal, and
183183
`in_progress` resumes automatically — only `advanced` waits.)
184184

185-
`suggestions.md` is an optional async channel: append a one-off note and
186-
the next fresh attempt sees the text added since it was last read, once.
187-
Use `goal.md` for permanent/structural changes, `suggestions.md` for
188-
transient nudges (e.g. left while AFK).
185+
`suggestions/` is an optional async channel — a directory parallel to
186+
`attempts/`: the Runner of attempt NNN reads `suggestions/NNN.md` at the start
187+
of that attempt, so the manager drops a note named for the round that will read
188+
it (e.g. `suggestions/006.md` while attempt 005 runs). The file stays in place
189+
afterward as part of the audit trail. Use `goal.md` for permanent/structural
190+
changes, `suggestions/` for transient nudges (e.g. left while AFK).
189191

190192
## Workspace contents
191193

@@ -194,8 +196,8 @@ After running, the workspace looks like:
194196
```
195197
<workspace>/
196198
├── goal.md
197-
├── config.yaml # optional: model / interval / mode
198-
├── suggestions.md # optional: async per-attempt notes
199+
├── config.yaml # optional: model / interval / mode / caps
200+
├── suggestions/ # optional: per-attempt human notes (NNN.md read by attempt NNN)
199201
├── memory/
200202
│ └── learnings.md # ~4KB cap; Runner curates this
201203
└── attempts/

agents/goal-runner.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ optional Initial Context), `memory/learnings.md` if present, and the last few
1515
on them rather than assumptions. Also read whatever `goal.md`'s Verification
1616
references (rubrics, scripts, baselines).
1717

18+
Also read `suggestions/NNN.md` for THIS attempt's number (your brief states it),
19+
if it exists — a transient human note left for exactly this round. Treat it as
20+
guidance to address this attempt; `goal.md` still wins for the permanent spec.
21+
Leave the file in place (it's part of the audit trail, like `attempts/NNN.md`).
22+
1823
## 2. Verify
1924
Run the Verification procedure from `goal.md` — the **only authoritative check**,
2025
not your sense that it "looks done". It yields **pass** (objective met AND no hard

docs/design.md

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -235,26 +235,26 @@ tokens.
235235
```
236236
<workspace>/
237237
├── goal.md # Objective + Hard Constraints + Verification spec
238-
├── config.yaml # Optional: model / interval / mode (see below)
239-
├── suggestions.md # Optional: async per-attempt human notes (see below)
238+
├── config.yaml # Optional: model / interval / mode / caps (see below)
239+
├── suggestions/ # Optional: per-attempt human notes (see below)
240+
│ └── 006.md # suggestions/NNN.md is read by the Runner of attempt NNN
240241
├── memory/
241242
│ └── learnings.md # Run-level curated knowledge; Runner self-maintains, ~4KB cap
242243
├── attempts/
243244
│ ├── 001.md # Per-attempt factual record, written by the Runner of that attempt
244245
│ ├── 002.md
245246
│ └── ... # Append-only: each file is written once, never modified
246247
└── .goaloop/ # Orchestrator-private state (not part of the goal record)
247-
├── state.json # Checkpoint: active session id for crash/quota resume
248+
├── state.json # Checkpoint: active session id + cumulative counters/cost for resume
248249
├── status.txt # Current one-line orchestrator status (read by /goal-run)
249-
├── attempt_complete.json # Last completed attempt's {attempt, status, cost_usd}
250-
├── suggestions.cursor # Byte offset into suggestions.md already shown
250+
├── attempt_complete.json # Last completed attempt's {attempt, status, cost_usd, total_cost_usd}
251251
├── continue.json # copilot-mode approval token (written by `goaloop continue`)
252252
├── orchestrator.log # Per-attempt log: Runner messages, tool calls, results
253253
└── pipeline.pid # PID of the running orchestrator (for status/stop)
254254
```
255255

256256
The `goal.md` + `memory/` + `attempts/` triple is the goal record GoaLoop
257-
requires; `config.yaml` and `suggestions.md` are optional human inputs, and
257+
requires; `config.yaml` and `suggestions/` are optional human inputs, and
258258
`.goaloop/` is the orchestrator's own bookkeeping and can be deleted
259259
between runs without losing the audit trail. Anything else (scripts the
260260
verification uses, source code being modified, datasets, etc.) lives wherever
@@ -443,8 +443,8 @@ The orchestrator, not the Manager, performs the per-attempt mechanics:
443443
3. **Spawn the Runner.** `claude -p <brief> --append-system-prompt
444444
goal-runner.md --output-format stream-json --session-id <uuid>
445445
--dangerously-skip-permissions`. The brief carries the workspace path,
446-
the attempt number, the read-context + terminator instructions, and —
447-
on a fresh attempt — any NEW `suggestions.md` text since the cursor.
446+
the attempt number, and the read-context + terminator instructions; the
447+
Runner itself reads `suggestions/NNN.md` for that attempt as context.
448448
4. **Parse the terminator** (`{"status": pass|advanced|in_progress|blocked}`)
449449
and branch: `pass`/`blocked` → exit; `advanced` (and `attempts/NNN.md`
450450
exists) → pace (or, in copilot mode, wait for approval), then next
@@ -531,23 +531,28 @@ on the user's confirmation** — not to carry the guidance itself.
531531
This is also why `goal.md` is mutable mid-run: it is the steering wheel, not
532532
just the starting configuration.
533533

534-
**`suggestions.md` — transient / per-attempt.**
534+
**`suggestions/` — transient / per-attempt.**
535535

536536
For a one-off note that does not belong in the goal spec (e.g. something left
537-
while AFK — "try lock granularity next"), the human appends a line to
538-
`<workspace>/suggestions.md`. On each FRESH attempt the orchestrator injects
539-
the text added since a stored cursor (`.goaloop/suggestions.cursor`) into the
540-
Runner's brief as a "Human guidance (NEW)" section, then advances the cursor
541-
— so each note is shown to exactly one attempt and not repeated. Use
542-
`goal.md` for changes that should persist; use `suggestions.md` for transient
543-
nudges.
537+
while AFK — "try lock granularity next"), the manager drops a note file into
538+
`<workspace>/suggestions/` — a directory parallel to `attempts/`. Delivery is
539+
**Runner-side, by attempt number**: the Runner of attempt NNN reads
540+
`suggestions/NNN.md` as part of its Load-context step (just like it reads
541+
`goal.md` and `attempts/`), so the manager — which knows the current attempt
542+
from status — names the note for the round that will read it (`suggestions/006.md`
543+
while 005 runs). The orchestrator never touches it; there is no injection, no
544+
consumption, no cursor. The file stays in place afterward as part of the audit
545+
trail. Because delivery is an exact number match, a note whose round the loop
546+
has already passed simply sits unread until re-pointed — an accepted, low-odds
547+
trade for the simplicity of "Runner reads its own `suggestions/NNN.md`". Use
548+
`goal.md` for changes that should persist; `suggestions/` for transient nudges.
544549

545550
**The Manager distinguishes messages for itself vs. goal edits.**
546551

547552
| User message | Manager's response |
548553
|---|---|
549554
| "How is it going?" | Answer directly by reading `.goaloop/status.txt` + latest `attempts/NNN.md` |
550-
| "Try lock granularity instead next time" | A transient nudge → append to `suggestions.md` (next attempt sees it once); a permanent change → propose editing `goal.md`'s Initial Context |
555+
| "Try lock granularity instead next time" | A transient nudge → write `suggestions/<next-attempt>.md` (that Runner reads it); a permanent change → propose editing `goal.md`'s Initial Context |
551556
| "Stop the orchestrator" | `goaloop stop <name>`; confirm it stopped |
552557
| "Change the target to P99 < 3s" | Propose editing `goal.md`'s Objective; edit on confirmation |
553558
| "Hmm" / "OK" / chit-chat | Ignore |
@@ -629,8 +634,8 @@ Human pacing during a run is achieved by:
629634
- **Reading status** via `/goal-run` (or `goaloop status`, or
630635
`tail -f .goaloop/orchestrator.log`) — the orchestrator writes `status.txt` and
631636
`attempt_complete.json` each attempt.
632-
- **Editing `goal.md`** (permanent) or **appending to `suggestions.md`**
633-
(transient, per-attempt) to steer — the next attempt's Runner picks it up
637+
- **Editing `goal.md`** (permanent) or **dropping a `suggestions/NNN.md`**
638+
(transient, per-attempt) to steer — the Runner of that attempt picks it up
634639
(see Human guidance protocol).
635640
- **`goaloop continue`** to release the next attempt in copilot mode.
636641
- **`goaloop stop`** to halt earlier than `pass`.
@@ -679,9 +684,9 @@ The following were considered and deliberately not built. Each appears here
679684
with the reason so future contributors don't reintroduce them by reflex.
680685

681686
> **Implemented after v0.1 (now included).** Two features once listed here as
682-
> excluded have since been built: `suggestions.md` (a transient per-attempt
683-
> human → agent channel, NEW-since-cursor injection — see Human guidance
684-
> protocol) and copilot mode (the `--mode copilot` human-review pause,
687+
> excluded have since been built: `suggestions/` (a transient per-attempt
688+
> human → agent channel — the Runner of attempt NNN reads `suggestions/NNN.md`;
689+
> see Human guidance protocol) and copilot mode (the `--mode copilot` human-review pause,
685690
> released with `goaloop continue` — see How to run). They are documented
686691
> above and are no longer exclusions.
687692

goaloop/orchestrator.py

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,10 @@ def __init__(
8787
self.status_path = self.state_dir / "status.txt"
8888
self.complete_path = self.state_dir / "attempt_complete.json"
8989
self.continue_path = self.state_dir / "continue.json"
90-
# Async human feedback channel (optional). The human appends notes to
91-
# suggestions.md; we inject anything past the cursor as NEW into the
92-
# next fresh attempt's brief, then advance the cursor.
93-
self.suggestions_path = self.ws / "suggestions.md"
94-
self.cursor_path = self.state_dir / "suggestions.cursor"
90+
# Human feedback is delivered Runner-side, not here: the manager writes
91+
# a note to suggestions/NNN.md and the Runner of attempt NNN reads it as
92+
# context for that round (see agents/goal-runner.md, "Load context").
93+
# The orchestrator stays out of it — no injection, no consumption.
9594

9695
self.adapter = ClaudeAdapter(
9796
cwd=str(self.ws),
@@ -189,35 +188,6 @@ def _end_error(self, attempt: int, reason: str) -> None:
189188
self._mark_complete(attempt, "error", None)
190189
self._clear_active()
191190

192-
def _suggestions_section(self) -> str:
193-
"""Build the NEW-since-cursor block from suggestions.md, then advance
194-
the cursor (these notes are now delivered into a session).
195-
196-
Returns "" when there's no suggestions.md or nothing new. Only NEW
197-
text is injected — older notes stay in the file for the human; the
198-
Runner can read it directly if it wants history. goal.md remains the
199-
channel for permanent/structural guidance; suggestions.md is for
200-
transient per-attempt notes (e.g. left while AFK).
201-
"""
202-
if not self.suggestions_path.exists():
203-
return ""
204-
content = self.suggestions_path.read_text()
205-
if not content.strip():
206-
return ""
207-
cursor = 0
208-
if self.cursor_path.exists():
209-
try:
210-
cursor = int(self.cursor_path.read_text().strip())
211-
except ValueError:
212-
cursor = 0
213-
cursor = max(0, min(cursor, len(content)))
214-
new = content[cursor:].strip()
215-
if not new:
216-
return ""
217-
self.state_dir.mkdir(parents=True, exist_ok=True)
218-
self.cursor_path.write_text(str(len(content))) # delivered — advance
219-
return f"\n## Human guidance (NEW — address this attempt)\n\n{new}\n"
220-
221191
def _wait_for_continue(self, n: int) -> None:
222192
"""Copilot mode: block until the human approves the next attempt.
223193
@@ -235,10 +205,8 @@ def _wait_for_continue(self, n: int) -> None:
235205
self.log("[orchestrator] approval received — continuing")
236206

237207
def _build_brief(self, n: int) -> str:
238-
guidance = self._suggestions_section()
239208
return f"""Workspace: {self.ws}
240209
This is attempt {n:03d}; write your attempt record to attempts/{n:03d}.md.
241-
{guidance}
242210
Run one attempt per your system-prompt workflow, then end your final message
243211
with a single line that is exactly one of these JSON objects:
244212
{{"status": "pass", "verification": "<one-line summary>"}}

skills/goal-flash/SKILL.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ rather than interviewing for it:
115115
`goaloop status <name>` or `tail -f <workspace>/.goaloop/orchestrator.log`.
116116
- **The inferred goal.md is the steering wheel and is mutable mid-run.** If
117117
the inference was off, edit `<workspace>/goal.md` (esp. Verification) — the
118-
next attempt picks it up. Drop a transient nudge in `suggestions.md`. Halt
119-
with `goaloop stop <name>`.
118+
next attempt picks it up. For a transient nudge, drop a note file the next
119+
attempt will read — `suggestions/<next-attempt>.md`, named per the convention
120+
`/goal-run` describes (check `goaloop status` for the round). Halt with
121+
`goaloop stop <name>`.
120122

121123
From here, progress relay is identical to `/goal-run` — read
122124
`.goaloop/status.txt`, `.goaloop/attempt_complete.json`, and the latest

skills/goal-init/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ After writing, tell the user:
131131
mid-run, edit `goal.md` — the next attempt picks it up.
132132
- (Optional, no action needed at init) the workspace may also hold a
133133
`config.yaml` (flat keys `model` / `interval` / `mode: auto|copilot`)
134-
to set run defaults, and a `suggestions.md` where they can drop async
135-
per-attempt notes mid-run (each note is shown to one fresh attempt);
136-
`goal.md` remains the place for permanent changes.
134+
to set run defaults, and a `suggestions/` directory where the manager can
135+
drop async per-attempt note files mid-run (the Runner of attempt NNN reads
136+
`suggestions/NNN.md`); `goal.md` remains the place for permanent changes.
137137

138138
Encourage them to read and tweak `goal.md` before kicking off — it's
139139
the load-bearing artifact of the whole framework.

skills/goal-run/SKILL.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,15 @@ durable guidance channels, by intent serving different purposes.
152152
(or a file it references, like a rubric). The next attempt's Runner
153153
reads the updated spec naturally — no relay needed. Propose the edit,
154154
make it on the user's confirmation; no restart required.
155-
- **Transient per-attempt note**: append a line to
156-
`~/.goaloop/<name>/suggestions.md`. The next fresh attempt sees the
157-
text added since it was last read, once (then it's not repeated). Use
158-
this for one-off nudges (e.g. dropped while AFK) rather than changes
159-
that should persist — those belong in `goal.md`.
155+
- **Transient per-attempt note**: the Runner of attempt NNN reads
156+
`~/.goaloop/<name>/suggestions/NNN.md` at the start of that attempt. So
157+
write the note there, targeting the round that will read it: check
158+
`goaloop status` for the current attempt and write the NEXT number — if
159+
attempt 005 is running, write `suggestions/006.md`. The file stays in place
160+
afterward as an audit trail (parallel to `attempts/NNN.md`). Use this for
161+
one-off nudges (e.g. dropped while AFK); `goal.md` is for changes that
162+
should persist. (If the loop has already moved past the round you targeted,
163+
the note just sits unread — re-point it at the new next attempt.)
160164
- **Stop the orchestrator**: `goaloop stop <name>` (sends SIGTERM; it
161165
exits after the in-flight attempt's process settles).
162166

0 commit comments

Comments
 (0)