Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
{"id":"beadle-4yg","title":"Pipeline v2: Stage 1 — Task-level design","description":"Worker: bwk. Evaluator: mdm. Read architecture doc (docs/pipeline-v2-design.md) and current code. Produce exact struct changes, function signatures, migration steps, test cases for T1-T7. Output: implementation spec detailed enough for mechanical execution. Parent: beadle-mvd","status":"closed","priority":2,"issue_type":"task","owner":"claude@punt-labs.com","created_at":"2026-04-18T07:56:16.70317152-07:00","created_by":"J F","updated_at":"2026-04-18T08:08:39.624098561-07:00","closed_at":"2026-04-18T08:08:39.624098561-07:00","close_reason":"Closed"}
{"id":"beadle-5ck","title":"feat(daemon): per-command MCP config wiring from command YAML","status":"closed","priority":1,"issue_type":"task","owner":"claude@punt-labs.com","created_at":"2026-04-14T19:44:23.320125431-07:00","created_by":"J F","updated_at":"2026-04-14T20:19:14.855038248-07:00","closed_at":"2026-04-14T20:19:14.855042528-07:00"}
{"id":"beadle-5f7","title":"SessionStart hook: auto-allow permissions + command deployment","status":"closed","priority":1,"issue_type":"task","owner":"jmf@pobox.com","created_at":"2026-03-15T12:40:13.416903-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-15T12:57:40.992793-07:00","closed_at":"2026-03-15T12:57:40.992793-07:00","close_reason":"SessionStart hook already exists and is wired in hooks.json. Permissions are auto-allowed."}
{"id":"beadle-5ma","title":"fix(release): include beadle-daemon in release binaries","description":"The release workflow (release.yml) only builds and uploads beadle-email. beadle-daemon is not included in GitHub Releases. Users must build from source. Both binaries should be cross-compiled and uploaded as release assets.","status":"open","priority":2,"issue_type":"bug","owner":"claude@punt-labs.com","created_at":"2026-04-18T15:05:22.502604109-07:00","created_by":"J F","updated_at":"2026-04-18T15:05:22.502604109-07:00"}
{"id":"beadle-5tk","title":"feat(daemon): command YAML loader with GPG signature verification","status":"closed","priority":1,"issue_type":"task","owner":"claude@punt-labs.com","created_at":"2026-04-14T19:43:52.994660168-07:00","created_by":"J F","updated_at":"2026-04-14T20:19:14.735543354-07:00","closed_at":"2026-04-14T20:19:14.735549124-07:00"}
{"id":"beadle-5xx","title":"Fix permission model bugs: remove owner override and removeContact gate","status":"closed","priority":1,"issue_type":"bug","owner":"jmf@pobox.com","created_at":"2026-03-19T21:45:39.127152-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-19T21:50:19.545956-07:00","closed_at":"2026-03-19T21:50:19.545956-07:00","close_reason":"Closed"}
{"id":"beadle-607","title":"Set license to MIT: add LICENSE file, update README","status":"closed","priority":2,"issue_type":"task","owner":"jmf@pobox.com","created_at":"2026-03-18T06:03:36.687775-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-18T07:08:41.554646-07:00","closed_at":"2026-03-18T07:08:41.554646-07:00","close_reason":"Merged in PR #34. MIT LICENSE file added, README updated."}
Expand Down Expand Up @@ -106,6 +107,7 @@
{"id":"beadle-twc","title":"Add --json, --verbose, --quiet global flags to CLI","status":"closed","priority":1,"issue_type":"task","owner":"jmf@pobox.com","created_at":"2026-03-18T06:03:37.253054-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-18T09:03:41.54664-07:00","closed_at":"2026-03-18T09:03:41.54664-07:00","close_reason":"Merged in PR #38. CLI parity (list/read/send/move/folders), global flags (--json/--verbose/--quiet), install/uninstall subcommands."}
{"id":"beadle-tx7","title":"download_attachment: extract and return attachment content by part index","description":"read_message lists attachment metadata (filename, content type, size) but cannot extract the actual content. Add a download_attachment tool that takes message_id and part index, extracts the MIME part bytes, and returns them (base64 for binary, text for text parts). Add suppress-output handler. Enables reading attached documents, saving files locally, forwarding attachments.","status":"closed","priority":1,"issue_type":"feature","owner":"jmf@pobox.com","created_at":"2026-03-13T23:53:55.404699-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-14T10:55:45.209781-07:00","closed_at":"2026-03-14T10:55:45.209781-07:00","close_reason":"Implemented in feat/download-attachment branch"}
{"id":"beadle-uce","title":"feat(daemon): Planner interface with LLM and Rule implementations","status":"closed","priority":1,"issue_type":"task","owner":"claude@punt-labs.com","created_at":"2026-04-14T19:43:58.103063784-07:00","created_by":"J F","updated_at":"2026-04-14T20:19:14.814887152-07:00","closed_at":"2026-04-14T20:19:14.814891862-07:00"}
{"id":"beadle-vjo","title":"feat(daemon): CLI runner resolves args from pipe JSON","description":"CLI runner falls back to pipe JSON fields when planner doesn't provide args. Enables multi-stage pipelines where stage 0 (Claude) produces JSON that stage 1 (CLI) consumes as command flags. Planner-provided args take precedence over pipe fields.","status":"in_progress","priority":2,"issue_type":"task","owner":"claude@punt-labs.com","created_at":"2026-04-18T15:15:15.280908795-07:00","created_by":"J F","updated_at":"2026-04-18T15:15:24.78236094-07:00"}
{"id":"beadle-vkh","title":"Fix ethos repo config contract: active → agent","status":"closed","priority":1,"issue_type":"bug","owner":"jmf@pobox.com","created_at":"2026-03-21T22:37:14.757186-07:00","created_by":"\"jmf-pobox\"","updated_at":"2026-03-21T23:03:19.439572-07:00","closed_at":"2026-03-21T23:03:19.439572-07:00","close_reason":"Closed"}
{"id":"beadle-vwm","title":"PGP verification consistency: read_message and trust model fixes","description":"PR #1 review feedback. Three related issues:\n\n1. `read_message` handler doesn't run PGP verification like `list_messages` — user sees `verified` in listing but `unverified` when reading the same message\n2. `sign.go` declares `Content-Transfer-Encoding: quoted-printable` but writes body verbatim — change to `7bit`\n3. Dead re-parse in `verify.go` L159-163 — `mail.ReadMessage` result immediately discarded\n\nFiles: `internal/mcp/tools.go`, `internal/pgp/sign.go`, `internal/pgp/verify.go`","status":"closed","priority":1,"issue_type":"bug","owner":"jmf@pobox.com","created_at":"2026-03-13T11:15:55.307398-07:00","created_by":"jmf-pobox","updated_at":"2026-03-13T11:17:01.718592-07:00","closed_at":"2026-03-13T11:17:01.718592-07:00","close_reason":"Closed"}
{"id":"beadle-vyv","title":"design: beadle as orchestrator — MCP server spawns Claude Code for inbox processing","description":"## Problem\n\nThe current two-layer polling design (DES-015) has a sync gap: the MCP server detects new mail (background goroutine) but relies on a durable CronCreate job in Claude Code for processing. These can drift — CronCreate expires after 7 days, manual intervention breaks sync, session crashes leave one layer orphaned.\n\nIn the Docker sandbox model, this gets worse: who starts Claude Code? Today Claude Code starts beadle. In a sandbox, beadle is the long-running daemon.\n\n## Solution\n\nInvert the launch. beadle spawns Claude Code with a prompt file:\n\n claude --prompt-file .beadle/startup.md\n\nWhere startup.md is:\n\n /loop 5m /inbox 5m\n\nThis single command sets both layers atomically:\n1. /loop 5m creates the durable CronCreate job that fires /inbox every 5m (processing layer)\n2. /loop immediately executes /inbox 5m on the first fire\n3. /inbox 5m calls set_poll_interval → MCP server background goroutine starts (detection layer)\n\nBoth layers in sync from the first turn. No SessionStart hooks, no CLAUDE.md instructions, no hoping the model acts. The prompt IS a user message — the model always processes it.\n\n## Sandbox startup sequence\n\n sbx run claude --prompt-file .beadle/startup.md\n\nOr beadle spawns it internally:\n\n exec.Command(\"claude\", \"--prompt-file\", promptPath)\n\nThe sync problem disappears because there is no gap between \"server starts\" and \"processing starts.\" One atomic launch.\n\n## Open Questions\n\n- Does claude --prompt-file exist today? What flags control headless/batch execution?\n- Can beadle inside a sandbox spawn claude inside the same sandbox?\n- What happens when the Claude Code session ends (context limit, crash)? Does beadle detect and re-launch?\n- Does this violate \"zero agent authority\"? The owner's signed config (poll_interval in email.json) is the authorization. The prompt file is a static, auditable artifact.\n- How does this interact with N:1 multi-session via mcp-proxy? The prompt-file session is the \"processing\" session; interactive sessions connect separately.\n\n## Relationship to Current Work\n\n- Not blocking beadle-o1w (Docker image). Ship the current two-layer design first.\n- This is a future architecture for when beadle runs as a long-lived sandbox daemon.\n- The PreToolUse sync-check hook is the interim enforcement for the current design.","status":"closed","priority":2,"issue_type":"feature","owner":"claude@punt-labs.com","created_at":"2026-04-12T08:32:53.391598859-07:00","created_by":"J F","updated_at":"2026-04-14T00:05:40.329372659-07:00","closed_at":"2026-04-14T00:05:40.329375869-07:00"}
Expand Down
2 changes: 2 additions & 0 deletions .ethos/missions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@
{"id":"m-2026-04-18-077","created_at":"2026-04-18T14:56:51Z","closed_at":"2026-04-18T15:37:18Z","status":"failed","type":"report","leader":"claude","worker":"bwk","evaluator":"mdm","write_set":[".tmp/review-pipeline-v2.md"],"success_criteria":["findings reported with severity and file:line references"],"rounds_used":1,"rounds_budgeted":1,"verdict":"fail","files_changed":[],"pipeline":"standard-2026-04-18-7c0858"}
{"id":"m-2026-04-18-106","created_at":"2026-04-18T19:42:09Z","closed_at":"2026-04-18T19:47:19Z","status":"closed","type":"implement","leader":"claude","worker":"bwk","evaluator":"mdm","write_set":["internal/mcp/"],"success_criteria":["make check passes","new code has tests"],"rounds_used":1,"rounds_budgeted":3,"verdict":"pass","files_changed":["internal/mcp/tools.go","internal/mcp/format.go","internal/mcp/smoke_test.go","internal/mcp/handler_test.go"],"pipeline":"quick-2026-04-18-cc18e3"}
{"id":"m-2026-04-18-107","created_at":"2026-04-18T19:42:09Z","closed_at":"2026-04-18T19:52:22Z","status":"closed","type":"report","leader":"claude","worker":"bwk","evaluator":"mdm","write_set":[".tmp/review-batch-move.md"],"success_criteria":["findings reported with severity and file:line references"],"rounds_used":1,"rounds_budgeted":1,"verdict":"pass","files_changed":[],"pipeline":"quick-2026-04-18-cc18e3"}
{"id":"m-2026-04-18-082","created_at":"2026-04-18T15:37:31Z","closed_at":"2026-04-18T22:15:56Z","status":"closed","type":"implement","leader":"claude","worker":"bwk","evaluator":"djb","ticket":"beadle-976","write_set":["internal/daemon/runner.go","internal/daemon/runner_test.go","internal/daemon/pipeline.go","internal/daemon/pipeline_test.go","internal/daemon/pipeline_edge_test.go","internal/daemon/mission.go"],"success_criteria":["CLIRunner sets explicit minimal Env on exec.Cmd","stderr buffers capped at 1MB","escapeYAMLValue not used for pipe values in buildStageContract","fireElse has no pipe parameter","go test -race passes"],"rounds_used":1,"rounds_budgeted":2,"verdict":"pass","files_changed":["internal/daemon/runner.go","internal/daemon/runner_test.go","internal/daemon/pipeline.go","internal/daemon/mission.go"]}
{"id":"m-2026-04-18-125","created_at":"2026-04-18T22:16:01Z","closed_at":"2026-04-18T22:19:09Z","status":"closed","type":"implement","leader":"claude","worker":"bwk","evaluator":"mdm","write_set":["internal/daemon/"],"success_criteria":["make check passes","new code has tests"],"rounds_used":1,"rounds_budgeted":3,"verdict":"pass","files_changed":["internal/daemon/runner.go","internal/daemon/runner_test.go"],"pipeline":"quick-2026-04-18-7d7a42"}
14 changes: 14 additions & 0 deletions internal/daemon/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package daemon
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -135,6 +136,13 @@ func (r *CLIRunner) Run(ctx context.Context, e *Executor, p *Pipeline, idx int,
return "", err
}

// Best-effort parse of pipe as JSON so declared args can fall back
// to fields from the previous stage's output.
var pipeFields map[string]any
if trimmed := strings.TrimSpace(pipe); len(trimmed) > 0 && trimmed[0] == '{' {
_ = json.Unmarshal([]byte(pipe), &pipeFields)
Comment thread
claude-puntlabs marked this conversation as resolved.
}

args := make([]string, len(cmd.FixedArgs))
copy(args, cmd.FixedArgs)

Expand All @@ -147,6 +155,12 @@ func (r *CLIRunner) Run(ctx context.Context, e *Executor, p *Pipeline, idx int,

for _, decl := range cmd.Args {
val, ok := call.Args[decl.Name]
// TODO(beadle-vjo): pipe-derived args bypass ValidateArgs type constraints.
// Prior stage schema validation covers this for now. Add runtime validation
// when arg types are enforced at execution time.
if !ok && pipeFields != nil {
val, ok = pipeFields[decl.Name]
}
Comment thread
claude-puntlabs marked this conversation as resolved.
Comment thread
claude-puntlabs marked this conversation as resolved.
if !ok {
continue
}
Expand Down
85 changes: 85 additions & 0 deletions internal/daemon/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,88 @@ func TestCLIRunner_CompoundPipeStdin(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "PIPE DATA", result)
}

func TestCLIRunner_ArgsFromPipe(t *testing.T) {
_, wl := setupWhitelist(t, "echo")
runner := &CLIRunner{Whitelist: wl}

cmd := &Command{
Name: "test-pipe-args",
Runner: "cli",
Mode: "process",
Binary: "echo",
FixedArgs: []string{"-n"},
Args: []CommandArg{
{Name: "title", Type: "string"},
{Name: "type", Type: "string"},
},
OutputSchema: "text",
Timeout: "5s",
}
call := CommandCall{Command: "test-pipe-args", Args: map[string]any{}}
p := testPipeline()

pipe := `{"title": "Fix auth", "type": "task"}`
result, err := runner.Run(context.Background(), &Executor{Logger: testLogger()}, p, 0, cmd, call, pipe)
require.NoError(t, err)
assert.Contains(t, result, "--title=Fix auth")
assert.Contains(t, result, "--type=task")
}

func TestCLIRunner_ArgsPlannerOverridesPipe(t *testing.T) {
_, wl := setupWhitelist(t, "echo")
runner := &CLIRunner{Whitelist: wl}

cmd := &Command{
Name: "test-override",
Runner: "cli",
Mode: "process",
Binary: "echo",
FixedArgs: []string{"-n"},
Args: []CommandArg{
{Name: "title", Type: "string"},
},
OutputSchema: "text",
Timeout: "5s",
}
call := CommandCall{
Command: "test-override",
Args: map[string]any{"title": "Override"},
}
p := testPipeline()

pipe := `{"title": "From pipe"}`
result, err := runner.Run(context.Background(), &Executor{Logger: testLogger()}, p, 0, cmd, call, pipe)
require.NoError(t, err)
assert.Contains(t, result, "--title=Override")
assert.NotContains(t, result, "From pipe")
}

func TestCLIRunner_ArgsFromPipe_InvalidJSON(t *testing.T) {
_, wl := setupWhitelist(t, "echo")
runner := &CLIRunner{Whitelist: wl}

cmd := &Command{
Name: "test-bad-json",
Runner: "cli",
Mode: "process",
Binary: "echo",
FixedArgs: []string{"-n"},
Args: []CommandArg{
{Name: "title", Type: "string"},
},
OutputSchema: "text",
Timeout: "5s",
}
call := CommandCall{
Command: "test-bad-json",
Args: map[string]any{"title": "FromArgs"},
}
p := testPipeline()

// Pipe is not valid JSON — should not crash, args from call.Args only.
pipe := "this is not json"
result, err := runner.Run(context.Background(), &Executor{Logger: testLogger()}, p, 0, cmd, call, pipe)
require.NoError(t, err)
assert.Contains(t, result, "--title=FromArgs")
}
Loading