completion

package
v0.0.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 13 Imported by: 0

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

View Source
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

type DoneHook func(ctx context.Context, task store.Task)

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

func WithAuditor(a config.Auditor) Option

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

func WithClock(now func() time.Time) Option

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

func WithDoneHook(h DoneHook) Option

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

func WithPollInterval(d time.Duration) Option

WithPollInterval overrides the 5s default. Tests squash this to a few milliseconds so Run-loop tests finish in well under a second.

func WithStore

func WithStore(s Store) Option

WithStore injects the tasks-table repository. Required.

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

func New(opts ...Option) (*Watcher, error)

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

func (w *Watcher) Run(ctx context.Context) error

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

func (w *Watcher) Tick(ctx context.Context) error

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

type WorkspaceDirs interface {
	Dir(taskID int64) string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL