Documentation
¶
Overview ¶
Package cli implements the user-facing CLI commands.
Index ¶
- Variables
- func Actor() string
- func Dispatch(args []string) error
- func DocDescription(data []byte) (description string, present bool)
- func Emit(data any) error
- func Fail(err error) error
- func FailWith(err error, data any) error
- func FormatTable() bool
- func Override(modify func(*GlobalFlags)) func()
- func RunAgent(args []string) error
- func RunConfig(args []string) error
- func RunContext(args []string) error
- func RunDocs(args []string) error
- func RunEvents(args []string) error
- func RunExport(args []string) error
- func RunInit(args []string) error
- func RunIssue(args []string) error
- func RunMetrics(args []string) error
- func RunNote(args []string) error
- func RunOrchestrate(args []string) error
- func RunPhase(args []string) error
- func RunPlan(args []string) error
- func RunReady(args []string) error
- func RunRoot(args []string) error
- func RunScaffold(args []string) error
- func RunServe(args []string) error
- func RunStatus(args []string) error
- func RunTask(args []string) error
- func RunUpgrade(args []string) error
- func RunVersion(args []string) error
- func RunWorktree(args []string) error
- func Usage(msg string) error
- type Envelope
- type GlobalFlags
- type PlanImport
- type PlanImportPhase
- type PlanImportTask
Constants ¶
This section is empty.
Variables ¶
var ( Version = "dev" Commit = "unknown" )
Version and Commit are populated at build time via -ldflags -X github.com/icento/stint/internal/cli.Version=... -X github.com/icento/stint/internal/cli.Commit=... Declared here (not in main) so the linker target stays inside the cli package and we don't have to thread the values through main.
var ErrUsage = errors.New("usage")
ErrUsage is a sentinel returned from Usage(); main maps it to exit 2.
var ErrWatchTimeout = errors.New("watch: deadline exceeded")
ErrWatchTimeout is the sentinel returned by `stint task watch` when the wall-clock --timeout elapses before any matching transition arrives. main maps it to exit code 2 so orchestrators can branch on "watch gave up" vs "watch saw the target" (exit 0) vs "real failure" (exit 1). Distinct from ErrUsage which also exits 2 — the JSONL line carries `"timeout":true` so callers can disambiguate at the payload level.
Functions ¶
func Actor ¶
func Actor() string
Actor returns the actor for the current invocation: --actor flag wins, else STINT_ACTOR env, else empty string.
func DocDescription ¶ added in v0.2.2
DocDescription returns the trimmed value of the `description:` key from a doc's YAML-style frontmatter block, and whether such a value was present. It is the single frontmatter description reader in package cli: the docs-list command and the import soft-warn both call it, so the contract stays narrow and the parsing lives in exactly one place.
present is false — with an empty string — when any of the structural preconditions fail: no opening fence, no closing fence, or no description key inside the block. Those are all expected operating states (a doc may legitimately carry no frontmatter), not programmer errors, so they are handled by return value rather than asserted away.
func Emit ¶
Emit writes a success envelope to stdout. Uses table rendering when the global --format flag is "table" / "human"; JSON envelope otherwise.
func FailWith ¶
FailWith is Fail with an attached `data` payload — used by commands that want to surface machine-readable context alongside an error (e.g. the `reason` field on `stint task next` not_found responses).
The error message is cleaned before encoding: the wrapped sentinel string (": invalid argument" / ": not found" / ": conflict") is stripped because `code` already classifies the failure, and store-layer errors are already authored without a "FuncName: " prefix so nothing else needs peeling at the CLI layer.
func FormatTable ¶
func FormatTable() bool
FormatTable reports whether output should be rendered as a human table.
func Override ¶
func Override(modify func(*GlobalFlags)) func()
Override applies an in-place mutation to the Globals singleton and returns a function that restores the prior value. The intended pattern is:
restore := cli.Override(func(g *cli.GlobalFlags) { g.DBPath = scratch })
defer restore()
The mutator pattern (rather than "pass a whole replacement struct") matters because the previous wholesale-assignment shape silently zeroed every field the caller didn't bother to set — Override(GlobalFlags{DBPath: scratch}) would wipe Actor / Format / Readonly, surprising any caller that had configured one of those already. The mutator approach preserves whatever the caller didn't touch, mirroring the field-scoped pattern test helpers use.
Why expose it from the non-test file: a future library embedder (a long-running process that wants to point at one DB, do work, then reset) needs a path that doesn't depend on testing.T. Tests still use the t.Cleanup-bound pointGlobalsAt for the same shape.
Concurrency caveat: this does NOT make Globals safe for concurrent goroutines that want different values. The singleton contract still holds; Override only standardises the save-and-restore dance.
func RunConfig ¶ added in v0.2.1
RunConfig dispatches the `stint config <set|get>` family. The parent holds the control flow (per CLAUDE.md "push ifs up"); each subverb body is a leaf that parses, validates, touches the store, and emits. `set` is a mutation (needs an actor, fails under --readonly at the SQLite layer); `get` is read-only and safe under --readonly.
func RunContext ¶
RunContext implements `stint context <task-id>` — the fat one-shot bundle agents call when they pick up a task. It returns task body + phase outline + plan outline + sibling task titles + dep details + recent notes.
func RunDocs ¶ added in v0.2.2
RunDocs dispatches `stint docs <verb>`. Only `list` exists today; the verb switch mirrors RunNote/RunIssue so a future `show`/`open` slots in without reshaping the dispatcher.
func RunEvents ¶
RunEvents handles `stint events` — the top-level streaming view of the audit log. Differs from `stint task events <id>` (per-task snapshot, JSON envelope) in two ways: it can span every task and it supports --follow.
In --follow mode the command emits JSONL: one event per line, no enclosing envelope. This makes it pipe-friendly (`stint events --follow | jq`). One-shot mode emits the usual {"ok":true,"data":[...]} envelope.
`--since` is polymorphic — see parseSince for the dispatch contract.
func RunExport ¶
RunExport dispatches `stint export <kind> <ref>`. The plan reference is resolved via store.ResolvePlanRef so the byte-identical export holds whether the caller passed the numeric id or the slug.
func RunInit ¶
RunInit handles `stint init` — create .stint/stint.db, a project row, and drop the Claude Code scaffold (agents, skills, CLAUDE.md) into the project root.
func RunIssue ¶
RunIssue dispatches `stint issue <verb>`. The verbs are a thin CLI over the existing notes table — v4 layered severity / resolved_at columns onto notes so review/blocker/test entries become first-class issues.
func RunMetrics ¶ added in v0.2.4
RunMetrics implements `stint metrics [--since ID|DURATION|RFC3339]` — the read-only DORA-style snapshot (lead time, throughput, change-failure rate, restore time) over the half-open window [since, now) of the audit log. It opens the store like RunStatus and Emits the model.MetricsSnapshot the store fills per docs/team-evolution.md §4/§5.
`--since` is resolved by the shared parseSince contract (events.go). Metrics is a *wall-clock window* verb: store.Metrics takes an RFC3339 lower bound, not an event id, so we feed it parseSince's sinceTime (the duration/RFC3339 branch). The pure-digit event-id branch has no wall-clock meaning here, so we reject it with the since-contract message rather than silently widen the window to "all time" — a silent widen would let `--since 12345` read as the opposite of what the user asked for. An omitted --since is the explicit "all of time" sentinel (empty string), which the store understands.
func RunOrchestrate ¶
RunOrchestrate dispatches the `stint orchestrate <subverb>` family. Today the only subverb is `next` — the orchestrator's "what should I dispatch to a subagent now?" query. The verb is intentionally read-only: it picks a candidate, classifies the reason, and emits the envelope; the caller is responsible for any state mutation (claim, dispatch, …). That split is what lets the same command run unmodified under --readonly.
func RunPlan ¶
RunPlan dispatches `stint plan <verb>`. The plan reference accepted by show/update/delete (and the --plan flag on sibling verbs) is either a numeric id or the URL-safe slug derived from the plan's title at create time — see store.ResolvePlanRef for the resolution contract.
func RunRoot ¶ added in v0.2.0
RunRoot implements `stint root` — emit the resolved project root and DB path WITHOUT opening the database. It exists so a worktree (which has no .stint dir of its own) can discover the main tree's DB path from a script and re-export it via STINT_DB. Resolution goes through the same resolveDBPath precedence as every other command, so `stint root` reports exactly what the next command will use.
func RunScaffold ¶
RunScaffold dispatches `stint scaffold <verb>`. The verb-router pattern mirrors RunTask so adding more scaffolding subcommands later (e.g. `scaffold install`) stays trivial.
func RunServe ¶
RunServe implements `stint serve` — a read-only dashboard HTTP server.
The DB is opened read-only by construction regardless of the global --readonly flag. That invariant is asserted on entry; if the caller somehow bypasses openStore's plumbing, the assertion catches it before any handler runs.
func RunUpgrade ¶ added in v0.2.0
RunUpgrade handles `stint upgrade`. Three modes: --check (resolve the latest release and report whether an update is available, writing nothing), --scaffold-only (optionally --dry-run; reconcile the project's stint-managed files against a fresh install, no binary swap), and the DEFAULT mode (neither flag) which performs the full self-update: preflight, resolve, download + checksum-verify, exec-version-verify, reconcile via the new binary, then swap. --force bypasses ONLY the dev-build refusal, never the checksum or exec gates.
Pre: args is the post-`upgrade` argv slice (never nil at the call site, but a nil slice parses cleanly to the default self-update path). Post: exactly one envelope is written to stdout before returning.
func RunVersion ¶
RunVersion implements `stint version`. Emits the build metadata as a JSON envelope (default) or a three-row labeled table when --format table is active. The fields are read from package vars Version/Commit that the linker can override (falling back to embedded VCS info for un-stamped dev builds); runtime.Version() is read every call (cheap).
func RunWorktree ¶ added in v0.2.4
RunWorktree dispatches `stint worktree <verb>`. Only `add` exists today; the verb switch mirrors RunDocs/RunNote/RunIssue so a future `list`/`remove` slots in without reshaping the dispatcher.
func Usage ¶
Usage emits a JSON envelope with code:"usage" on stdout and returns ErrUsage so main exits 2 — the documented usage-error code.
Output policy: stdout carries the authoritative JSON envelope; stderr carries no duplicate. Verb-specific usage one-liners (printed by the FlagSet's Usage func) reach stderr only when the user explicitly asks for --help, never as a side-effect of a bad invocation — bad invocations get the JSON envelope only.
Types ¶
type Envelope ¶
type Envelope struct {
OK bool `json:"ok"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Code string `json:"code,omitempty"`
}
Envelope is the JSON shape every command emits, so agents can parse uniformly.
type GlobalFlags ¶
type GlobalFlags struct {
DBPath string
Actor string // attributed to every mutation; STINT_ACTOR env is the fallback
Format string // "json" (default) or "table"
Readonly bool // open DB in query_only mode; mutations error out at SQLite layer
}
GlobalFlags holds CLI-wide options parsed before the subcommand.
Lifecycle contract: the package-level Globals singleton is set EXACTLY ONCE per process — by `main()` after parseGlobalFlags reads argv — and is read by command handlers thereafter. The singleton shape is intentional for a single-shot CLI: every dispatcher call resolves to one set of flags, so threading them through every function signature would be ceremony without benefit.
The cost: this package cannot be safely embedded as a library with concurrent callers that need different DBPath / Actor values. If that requirement ever lands, GlobalFlags should be threaded as a value through Dispatch — see the trade-off discussion in the codebase review.
Test contract: tests legitimately mutate Globals to point at a scratch DB. The canonical pattern is the package-internal helper `pointGlobalsAt(t, path)` (defined in task_brief_test.go) which saves the previous value and restores it via t.Cleanup. Use Override from non-test code that needs the same save/restore shape — both helpers share the same restore-on-defer contract so the audit story is uniform.
var Globals GlobalFlags
Globals is mutable singleton state set by main before dispatch.
type PlanImport ¶
type PlanImport struct {
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
Perspective string `json:"perspective"`
CreatedBy string `json:"created_by"`
Status string `json:"status"`
// ReviewPerspectives is the declared review-gate. Pointer (not slice)
// so we can distinguish "field missing in JSON" (validator rejects)
// from "field present as empty array" (audited auto-approve). The
// v6 review-perspectives invariant requires explicit author intent
// at import time: an omission would land a plan in the wrong state
// at SubmitPlan and trip the assertion that fires when an empty
// list reaches the auto-approve branch unintentionally.
ReviewPerspectives *[]string `json:"review_perspectives,omitempty"`
Phases []PlanImportPhase `json:"phases"`
}
PlanImport is the JSON shape accepted by `stint plan import`. Deps reference other tasks within the same file by `key` (string) so the planner can author the whole tree without knowing real IDs yet.
Slug is optional. When present, it must be in canonical form (slug.Derive(s) == s) and not collide with any other plan's slug; the store rejects non-canonical or colliding values as ErrInvalid. When absent, the store derives a slug from Title and runs the dedup loop just like `stint plan create`. The asymmetry (auto-derive on omit, strict-reject on explicit) matches the `stint plan update --slug` contract — explicit user input is never silently rewritten.
type PlanImportPhase ¶
type PlanImportPhase struct {
Title string `json:"title"`
Description string `json:"description"`
Tasks []PlanImportTask `json:"tasks"`
}
type PlanImportTask ¶
type PlanImportTask struct {
Key string `json:"key"` // referenced by other tasks' Deps
Title string `json:"title"`
Description string `json:"description"`
Acceptance string `json:"acceptance"`
Role string `json:"role"`
Deps []string `json:"deps"` // other tasks' Key
// Scope is the file/dir paths this task is expected to touch; a JSON
// array of strings. AcceptanceCheck is the executable acceptance
// command. Both are optional: an absent key decodes to the Go zero
// value (nil slice / ""), which CreateTaskArgs already normalizes to
// the schema defaults ("[]" / ""). A `scope` that is present but not
// a JSON array of strings is rejected by the decoder (json.Unmarshal
// returns a *json.UnmarshalTypeError), surfaced as code:"invalid" via
// readPlanImport — see the wrap there.
Scope []string `json:"scope"`
AcceptanceCheck string `json:"acceptance_check"`
// Locks is the set of exact-match mutex tokens this task holds for its
// duration; a JSON array of strings. Like Scope, it is optional: an
// absent key decodes to the Go zero value (nil slice), which
// CreateTaskArgs already normalizes to the schema default ("[]"). A
// `locks` that is present but not a JSON array of strings is rejected by
// the decoder (json.Unmarshal returns a *json.UnmarshalTypeError),
// surfaced as code:"invalid" via readPlanImport — see the wrap there.
Locks []string `json:"locks"`
// Skills is the set of skill slugs the worker INVOKES for this task; Docs
// is the set of filenames the worker READS under docs/. Both are a
// JSON array of strings. Like Scope and Locks, they are optional: an absent
// key decodes to the Go zero value (nil slice), which CreateTaskArgs already
// normalizes to the schema default ("[]"). A `skills`/`docs` that is present
// but not a JSON array of strings is rejected by the decoder (json.Unmarshal
// returns a *json.UnmarshalTypeError), surfaced as code:"invalid" via
// readPlanImport — see the wrap there.
Skills []string `json:"skills"`
Docs []string `json:"docs"`
}
Source Files
¶
- agent.go
- config.go
- context.go
- docs.go
- events.go
- export.go
- flags.go
- frontmatter.go
- init.go
- issue.go
- metrics.go
- note.go
- orchestrate.go
- output.go
- phase.go
- phase_land.go
- plan.go
- plan_import.go
- plan_import_merge.go
- plan_reconcile.go
- root.go
- root_cmd.go
- scaffold.go
- serve.go
- status.go
- table.go
- task.go
- task_brief.go
- task_claim.go
- task_dep_link.go
- task_lifecycle.go
- task_review.go
- task_verify.go
- task_watch.go
- upgrade.go
- validate.go
- version.go
- worktree.go