Documentation
¶
Overview ¶
Package completion is the atomic-sentinel completion detector marunage promises in docs/requirement.md "Crash safety" (invariant #5) and "atomic sentinel による完了検知". It owns the second half of the Act phase that PR-42's dispatcher hands off: once a row has flipped to running and Claude is loose inside the cmux workspace, this package polls the per-task workspace directory for the sentinel file the prompt instructed Claude to write at exit.
Wire shape:
- The dispatcher (PR-42 + PR-43 wiring) creates ~/.marunage/workspaces/<id>/ and embeds the path in the prompt as "echo $? > .exit_code.tmp && mv .exit_code.tmp .exit_code". `mv` on the same filesystem is atomic, so a reader either sees the final byte or no file at all.
- The Watcher polls store.List(Statuses=[running]) on each tick, stats <dir>/.exit_code, and on success transitions the row to done (with a result_summary lifted from <dir>/.result_summary). A non-zero exit code or a malformed sentinel surfaces as failed with judgment_reason, plus a completion.fail audit entry.
What this package does NOT cover (deferred to PR-44 reaper):
- cmux workspace deleted out from under us. Per task spec, "sentinel 未到達でワークスペースが消えていた場合は PR-44 reaper の責務 — 本 PR では running 維持で良い (失敗扱いしない)". The watcher therefore no-ops when the workspace dir is missing rather than failing the row.
- "started_at + 24h" stuck-timeout probe.
Tests (internal/completion/completion_test.go) drive every branch via a per-test temp directory, so the package never assumes the real ~/.marunage/workspaces/ tree exists.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidConfig = errors.New("completion: missing required option")
ErrInvalidConfig signals a missing required Option at construction. Mirrors dispatch.ErrInvalidConfig so a buggy CLI wiring fails loud at startup with a typed sentinel callers can errors.Is against.
Functions ¶
This section is empty.
Types ¶
type DoneHook ¶
DoneHook fires once per row that the watcher transitions to done. The PR-102 reflection hook (internal/dispatch.Reflector.OnDone) is the production consumer: after the row commits to done it asks Claude to reflect on the work in the same cmux workspace. Hook implementations MUST be non-blocking (the watcher invokes them inline on the tick goroutine) — long-running work belongs in a goroutine the hook spawns and the daemon shutdown path drains.
type Option ¶
type Option func(*Watcher)
Option mutates Watcher construction. The functional-option shape matches dispatch.Option and store.Option so the package surface stays uniform across the execution layer.
func WithAuditor ¶
WithAuditor installs the audit-log sink. Defaults to config.NopAuditor so existing tests / CLI paths that have not yet wired audit.log keep building. Production wires the same *logging.AuditLog the dispatcher already opens, so completion entries land in the same audit.log as dispatch.start / dispatch.fail.
func WithClock ¶
WithClock injects a deterministic clock used to stamp completed_at. Defaults to time.Now in production. The Run-loop polling cadence still depends on the real wall clock through time.NewTicker — this option pins the timestamp decision but does not eliminate sleep-based timing for the loop itself.
func WithDoneHook ¶
WithDoneHook installs a callback invoked once per row the watcher successfully transitions to done. Optional; when nil (the default), the watcher behaves exactly as before. Wired by the daemon to a dispatch.Reflector for the PR-102 reflection-on-done feature.
func WithPollInterval ¶
WithPollInterval overrides the 5s default. Tests squash this to a few milliseconds so Run-loop tests finish in well under a second.
func WithWorkspaceDirs ¶
func WithWorkspaceDirs(d WorkspaceDirs) Option
WithWorkspaceDirs injects the per-task directory resolver. Required. Production wraps a closure rooted at ~/.marunage/workspaces; tests hand back t.TempDir() subdirectories.
type Store ¶
type Store interface {
List(ctx context.Context, f store.ListFilter) ([]store.Task, error)
MarkDoneWithSummary(ctx context.Context, id int64, summary string, completedAt time.Time) error
MarkFailedWithReason(ctx context.Context, id int64, reason string) error
SetCompletedAt(ctx context.Context, id int64, t time.Time) error
}
Store is the narrow read/write surface the Watcher needs against the tasks table. Same test-seam pattern as dispatch.Store: production wires *store.TaskRepo, tests inject a fake. The method set is intentionally a subset of *store.TaskRepo so the concrete type satisfies it implicitly.
type Watcher ¶
type Watcher struct {
// contains filtered or unexported fields
}
Watcher polls running tasks for sentinel completion files and drives status transitions. Safe for one Run goroutine per process; the underlying Store + Auditor must themselves be concurrency-safe (the production *store.TaskRepo and *logging.AuditLog already are).
func New ¶
New builds a Watcher. Required: WithStore, WithWorkspaceDirs. Returns ErrInvalidConfig naming the missing field so a buggy CLI wiring fails loud at startup.
func (*Watcher) Run ¶
Run polls every running task's workspace for sentinel completion until ctx is cancelled. The first Tick fires immediately so a pre-existing sentinel is detected without a poll-interval wait; subsequent ticks fire on the configured cadence.
Context cancellation returns nil (clean shutdown for daemon callers). Per-tick errors are logged via the audit trail when they cause a row transition; transient infrastructure errors (Store.List blew up) are returned so the daemon caller can decide whether to keep going or fail loud.
func (*Watcher) Tick ¶
Tick scans every running row exactly once. Per-row failures (sentinel parse error, Store update failure) do not abort the scan — the next row is checked regardless, mirroring the dispatcher's "巻き込み故障 させない" stance from requirement.md.
The only error Tick returns is a Store.List failure: without the candidate set the scan cannot proceed at all, so surfacing the error lets Run decide whether to retry or shut down.
type WorkspaceDirs ¶
WorkspaceDirs resolves the per-task on-disk directory marunage owns (separate from cmux's workspace and from task.CWD). Used by both the dispatcher (to embed the sentinel write path in the prompt) and the watcher (to read the sentinel back). Production wires a function rooted at ~/.marunage/workspaces/<id>; tests use t.TempDir() per task.