Documentation
¶
Overview ¶
Package planengine implements the storage, tools, and embedded skills that power the /plan and /execute slash commands.
A plan is a hierarchical record of an implementation strategy with four levels: an intent statement, an ordered list of tickets, an ordered list of phases within each ticket, and an ordered list of tasks within each phase. A ticket maps to an independently reviewable, deployable stacked git branch; a phase maps to one atomic git commit on that branch; a task is a single work item that goes into the commit. The model collaborates with the user to build the structure during /plan, then walks it during /execute — cutting each ticket's stacked branch, committing each phase, and pushing the branch when the user confirms the ticket is done. Forge-specific publishing (e.g. opening a pull request) is handled externally via the ticket.completed lifecycle hook.
State lives in the shared SQLite database at <repo>/.jungi/state/state.db (opened by the statestore package); a rendered markdown view is written alongside at .jungi/state/plans/<slug>/plan.md whenever the model invokes render_plan_markdown. The database is the source of truth — the markdown file is for humans and is overwritten on every render.
Index ¶
- Variables
- func BranchName(planSlug, ticketSlug string) string
- func DeriveBranch(plan Plan, ticketCount int, ticket Ticket) string
- func MarkdownPath(repoRoot, slug string) string
- func PlansDir(repoRoot string) string
- func RenderMarkdown(p FullPlan) string
- func ShortSHA(sha string) string
- func WriteMarkdown(repoRoot string, p FullPlan) (string, error)
- type FullPlan
- type Phase
- type PhaseStatus
- type PhaseUpdate
- type PhaseWithTasks
- type Plan
- type PlanStatus
- type PlanUpdate
- type Store
- func (s *Store) AddPhase(ticketID int64, name string) (Phase, error)
- func (s *Store) AddTicket(planID int64, name, slug string, skills ...string) (Ticket, error)
- func (s *Store) Bootstrap(ctx context.Context) error
- func (s *Store) CompleteTicket(ticketID int64) (bool, error)
- func (s *Store) CreatePlan(slug, intent, branch string) (Plan, error)
- func (s *Store) GetPhase(phaseID int64) (Phase, error)
- func (s *Store) GetPlan(slug string) (FullPlan, error)
- func (s *Store) GetPlanByID(planID int64) (string, error)
- func (s *Store) GetTicket(ticketID int64) (Ticket, error)
- func (s *Store) GetTicketByOrd(planID int64, ord int) (Ticket, error)
- func (s *Store) ListPlans() ([]Plan, error)
- func (s *Store) MarkTicketReadyForReview(ticketID int64) (bool, error)
- func (s *Store) MarkTicketReviewed(ticketID int64) (bool, error)
- func (s *Store) MovePhase(phaseID int64, targetPos int) error
- func (s *Store) MoveTicket(ticketID int64, targetPos int) error
- func (s *Store) RepoRoot() string
- func (s *Store) SetPhaseCommit(phaseID int64, sha string) error
- func (s *Store) SetPlanBase(planID int64, baseBranch, baseSHA string) error
- func (s *Store) StartTicket(ticketID int64, branchName, baseRef string) error
- func (s *Store) TicketHasPhaseCommit(ticketID int64, sha string) (bool, error)
- func (s *Store) TouchPlanForTask(taskID int64) error
- func (s *Store) UpdatePhase(phaseID int64, upd PhaseUpdate) error
- func (s *Store) UpdatePlan(planID int64, upd PlanUpdate) error
- func (s *Store) UpdatePlanStatus(planID int64, status PlanStatus) error
- func (s *Store) UpdateTicket(ticketID int64, upd TicketUpdate) error
- type Ticket
- type TicketStatus
- type TicketUpdate
- type TicketWithPhases
Constants ¶
This section is empty.
Variables ¶
ErrNotFound is returned when a lookup by slug or ID finds no row. Callers can match against this with errors.Is.
Functions ¶
func BranchName ¶
BranchName derives a ticket's stacked branch name from the plan and ticket slugs. The scheme is deliberately FLAT (a single trailing path segment after the prefix) so two tickets never produce a directory/file ref conflict in .git/refs/heads. Both slugs are already kebab-cased by the plan tools' slugify, so the result is a valid git ref by construction. Overlapping segments between the plan and ticket slugs (e.g. a ticket slug that repeats the plan's trailing words) are de-duplicated via mergeSlugs so names like "na-1769-property-tabbed-nav-tabbed-nav" collapse to "na-1769-property-tabbed-nav".
func DeriveBranch ¶
DeriveBranch resolves the branch name for a ticket, honoring a user-supplied plan.Branch override when present. With no override, it falls back to the generated jungi/<plan>-<ticket> scheme (BranchName), which de-duplicates overlapping segments.
With an override: a single-ticket plan (ticketCount == 1) uses plan.Branch verbatim, with no jungi/ prefix, since there is only one ticket to disambiguate. A multi-ticket plan appends the ticket slug via mergeSlugs (still unprefixed) so each ticket's branch is unique while staying anchored to the user's requested name.
func MarkdownPath ¶
MarkdownPath returns the absolute path where the rendered markdown view of a plan should be written. Each render overwrites this file; the SQLite DB is the source of truth.
func PlansDir ¶
PlansDir returns the directory under which per-plan subdirectories live. Each plan gets <PlansDir>/<slug>/, currently containing only plan.md but reserved for future per-plan artifacts (research findings, etc.).
func RenderMarkdown ¶
RenderMarkdown produces the human-readable view of a plan. The output shape is deliberate and stable so users can scan progress at a glance: a banner, an H1 with the slug, a paragraph for the intent, then one H2 per ticket (with its branch/stacks-on/status, and its relevant skills when any were recorded), one H3 per phase (with its commit when made), and a checkbox list of each phase's tasks.
The function is pure: it takes a FullPlan and returns a string. The caller (the render_plan_markdown tool) is responsible for creating the target directory and writing the bytes.
func ShortSHA ¶
ShortSHA trims a commit hash to the conventional 7-character prefix for compact display in tool-event variants and rendered plan markdown. Inputs shorter than 7 are returned unchanged.
func WriteMarkdown ¶
WriteMarkdown renders the plan and writes it to the canonical path under the repo root, creating the per-plan subdirectory if missing. Returns the absolute path of the file that was written so the tool dispatcher can surface it to the model.
Types ¶
type FullPlan ¶
type FullPlan struct {
Plan Plan
Tickets []TicketWithPhases
}
FullPlan groups a plan with its tickets, each ticket's phases, and each phase's tasks, all in display order. It is the shape returned by Store.GetPlan and emitted by the get_plan tool so the model can load everything it needs in one round-trip.
type Phase ¶
type Phase struct {
ID int64
TicketID int64
Ord int
Name string
Status PhaseStatus
CommitSHA string
StatusLastChangedAt time.Time
TaskListID int64
}
Phase is one row of the phases table. Ord is the 1-based display order within the parent ticket; the store assigns it on insert as MAX(ord)+1 so the model can add phases without having to track sequencing. A phase is one atomic commit — CommitSHA is empty until commit_phase records the commit it produced. Status is active by default and dropped when update_phase removes it (only permitted before a commit is recorded). StatusLastChangedAt is bumped whenever Status changes. TaskListID is the tasklist-owned list linked to this phase via phase_task_lists; the model threads it into add_task_to_task_list so tasks land in the phase's list.
type PhaseStatus ¶
type PhaseStatus string
PhaseStatus names the lifecycle state of a single phase. active is the default, ordinary state a phase carries from creation through commit. dropped is a structural removal state set by update_phase("dropped") for a phase that has not yet produced a commit (guarded so a committed phase can never be dropped).
const ( PhaseStatusActive PhaseStatus = "active" PhaseStatusDropped PhaseStatus = "dropped" )
func (PhaseStatus) IsValid ¶
func (s PhaseStatus) IsValid() bool
IsValid reports whether s is one of the recognised phase statuses.
type PhaseUpdate ¶
type PhaseUpdate struct {
Name *string
Status *PhaseStatus
}
PhaseUpdate carries an optional, partial edit to a phase: name is display-only and always allowed; Status is restricted by UpdatePhase to the active/dropped structural transition.
type PhaseWithTasks ¶
PhaseWithTasks pairs a phase with the tasks in its linked task list, in display order. Tasks are tasklist.Task values hydrated from the tasklist tables via the phase_task_lists link.
type Plan ¶
type Plan struct {
ID int64
Slug string
Intent string
Status PlanStatus
// BaseBranch is the human-readable ref the first ticket stacks on
// (e.g. "main"), surfaced to ticket lifecycle hooks as BASE_REF so
// forge hook scripts know which branch to target when opening a review
// request. BaseSHA is the exact commit ticket 1 branches from. Both are
// empty until /execute starts the first ticket, at which point they are
// resolved from the worktree's HEAD and the repo's default branch.
BaseBranch string
BaseSHA string
// Branch is the user-supplied, already-slugified branch base captured
// at plan creation (empty when none was supplied). When set, it
// overrides the generated jungi/<plan>-<ticket> naming scheme:
// DeriveBranch uses it verbatim (no jungi/ prefix) for a single-ticket
// plan, or with a unique per-ticket suffix for a multi-ticket plan.
Branch string
CreatedAt time.Time
UpdatedAt time.Time
}
Plan is one row of the plans table. ID is the rowid; Slug is the human-readable kebab-case identifier the user sees and that the model uses to address the plan. Slug uniqueness is enforced by the schema with the store appending -2, -3, ... on collision so the model never has to handle the conflict.
type PlanStatus ¶
type PlanStatus string
PlanStatus names the lifecycle state of an entire plan. A plan is in_progress from creation until the user marks it accepted (planning complete, ready to execute) or abandoned. Today only in_progress is ever written — accepted/abandoned are reserved for future use when the planning workflow needs to gate execution on user acceptance.
const ( PlanStatusInProgress PlanStatus = "in_progress" PlanStatusAccepted PlanStatus = "accepted" PlanStatusAbandoned PlanStatus = "abandoned" PlanStatusCompleted PlanStatus = "completed" )
func (PlanStatus) IsValid ¶
func (s PlanStatus) IsValid() bool
IsValid reports whether s is one of the recognised plan statuses. Used to validate input on update_plan so a typo can be surfaced to the model instead of silently corrupting the DB.
type PlanUpdate ¶
type PlanUpdate struct {
Intent *string
Slug *string
// Branch, when non-nil, sets the branch override; pointing at "" clears
// it (stored as SQL NULL via nullableString).
Branch *string
Status *PlanStatus
}
PlanUpdate carries an optional, partial edit to a plan: each non-nil pointer field is applied, each nil field is left unchanged. Mirrors tasklist.TaskUpdate's pattern so update_plan can change any subset of intent, slug, branch, and status in one call.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps a SQLite connection and offers narrowly-scoped helpers for the operations the plan tools and skills need. Method receivers are safe for concurrent use because *sql.DB serialises access internally; the schema's UNIQUE constraints prevent races between two callers trying to add a phase or task with the same ord.
func New ¶
New returns a Store backed by the shared statestore connection, bootstrapping its tables on first use. It does not open or own the database — statestore does — so the connection is shared with every other state-owning package and is closed once, by the session, via the statestore handle.
tasks is the tasklist store whose tables back this plan's phase task lists; it must already be bootstrapped (its task_lists/tasks tables created) before planengine bootstraps, because planengine's phase_task_lists table has a foreign key into task_lists. The caller (session.Manager) constructs the tasklist store first and passes it here. planengine does not write tasks through it — task writes flow through tasklist's generic tools, with the plan-touch wired via tasklist.Store.SetAfterMutate(store.TouchPlanForTask).
The caller passes a handle whose repo root has already been validated via worktree.RepoRoot.
func (*Store) AddPhase ¶
AddPhase appends a new phase to the ticket identified by ticketID, auto- assigning the next ord, creates the phase's task list, and records the phase_task_lists link — all in one transaction. Returns the persisted Phase. Errors if ticketID does not exist.
The task list is created via tasklist.CreateListTx (not the tasklist Store's CreateList) so it runs on this transaction's connection: grabbing a second pool connection while this write transaction is open would deadlock under SQLite's single-writer model.
func (*Store) AddTicket ¶
AddTicket appends a new ticket to the plan identified by planID, auto- assigning the next ord. The slug is made unique within the plan by appending -2, -3, ... on collision (mirroring CreatePlan). skills is the optional set of relevant skill names persisted with the ticket (stored as a JSON array); callers are expected to have already filtered out any names they do not want recorded. Returns the persisted Ticket. Errors if planID does not exist.
func (*Store) Bootstrap ¶
Bootstrap creates the plan/ticket/phase/task tables on the shared connection via the embedded CREATE TABLE IF NOT EXISTS script. It is idempotent: a no-op on an already-bootstrapped database, full creation on a fresh one. modernc.org/sqlite executes the multi-statement script as a batch.
func (*Store) CompleteTicket ¶
CompleteTicket marks a started ticket completed. It errors if the ticket has not been started (no branch), so completion can never precede the branch/commit work it represents. pushed_at is preserved from MarkTicketReadyForReview and is not written here. It is idempotent: if the ticket is already completed it returns (false, nil) without bumping timestamps or touching the plan.
func (*Store) CreatePlan ¶
CreatePlan inserts a new plan row, resolving slug collisions by appending -2, -3, ... to the requested slug until insertion succeeds. Returns the persisted Plan with its allocated ID and final slug.
intent is required; slug may be empty in which case the caller (the create_plan tool) is expected to have derived one from intent already. Slug normalisation (lowercase, kebab-case) is the caller's responsibility — the store enforces uniqueness, not formatting.
func (*Store) GetPhase ¶
GetPhase loads a single phase by ID, including the commit it produced (empty until commit_phase records one).
func (*Store) GetPlan ¶
GetPlan returns the full plan tree (plan + tickets + phases + tasks, all in display order) for the given slug. Used by get_plan during /execute to load context, and by the renderer to assemble plan.md. Each result set is read to completion and closed before the next query so only one cursor is open at a time.
func (*Store) GetPlanByID ¶
GetPlanByID returns the slug for the plan with the given ID, regardless of its status. Used by execRenderMarkdown to resolve a plan_id to a slug without being blocked by the completed-plan filter in ListPlans.
func (*Store) GetTicket ¶
GetTicket loads a single ticket by ID.
func (*Store) GetTicketByOrd ¶
GetTicketByOrd loads the ticket at a given 1-based position within a plan. Used to resolve "the previous ticket" when computing a stacked ticket's base branch.
func (*Store) ListPlans ¶
ListPlans returns all plans ordered by most-recently-updated first, excluding plans with status "completed" or "abandoned". Used by the list_plans tool when the user runs /execute without naming a plan and the model needs to surface options.
func (*Store) MarkTicketReadyForReview ¶
MarkTicketReadyForReview marks a started ticket ready_for_review and records pushed_at to capture when the branch was pushed. It errors if the ticket has not been started (no branch). It is idempotent: if the ticket is already ready_for_review it returns (false, nil) without bumping timestamps or touching the plan.
func (*Store) MarkTicketReviewed ¶
MarkTicketReviewed marks a started ticket reviewed. It errors if the ticket has not been started (no branch). It is idempotent: if the ticket is already reviewed it returns (false, nil) without bumping timestamps or touching the plan.
func (*Store) MovePhase ¶
MovePhase repositions the phase identified by phaseID to targetPos (1-based) among its ticket's phases, using the collision-safe reorder algorithm. Guarded so a committed phase — real git history — can never be moved: a phase that already has a commit_sha is rejected. Errors with ErrNotFound if the phase does not exist.
func (*Store) MoveTicket ¶
MoveTicket repositions the ticket identified by ticketID to targetPos (1-based) among its plan's tickets, using the collision-safe reorder algorithm. Guarded so the stacked-branch ordering can never be disturbed: a started ticket (branch_name set) cannot itself be moved, and no ticket may be moved to or before the ord of the last started ticket in the plan (which would insert it "underneath" already-cut branches). Errors with ErrNotFound if the ticket does not exist.
func (*Store) RepoRoot ¶
RepoRoot returns the repo root the store was opened against. The renderer and tool dispatcher use it to resolve markdown paths.
func (*Store) SetPhaseCommit ¶
SetPhaseCommit records the commit a phase produced. Errors if the phase does not exist.
func (*Store) SetPlanBase ¶
SetPlanBase records the ref and commit the first ticket stacks on. Called once, when /execute starts ticket 1, so the first ticket's PR base and the stack's root commit are persisted for later reference.
func (*Store) StartTicket ¶
StartTicket records the branch a ticket was cut on and marks it in_progress. It rejects a ticket that has already been started so a double start_ticket is surfaced rather than silently re-pointing the branch. baseRef is the ref the branch was created from (the plan's base SHA for ticket 1, the previous ticket's branch otherwise).
func (*Store) TicketHasPhaseCommit ¶
TicketHasPhaseCommit reports whether any phase of the ticket already records the given commit SHA. Used to tell an unrecorded manual commit (which should be adopted by the current phase) apart from a truly empty phase (HEAD already attributed to an earlier phase).
func (*Store) TouchPlanForTask ¶
TouchPlanForTask bumps the updated_at of the plan that owns the given task, resolved through the task -> phase_task_lists -> phase -> ticket -> plan link walk, so list_plans ordering reflects task edits. It is the plan-touch side effect that planengine's own AddTask/UpdateTaskStatus used to perform inline; now that all task writes flow through tasklist's generic tools, the session wires this method as tasklist.Store.SetAfterMutate so every committed task insert or status change touches the owning plan.
It is a deliberate no-op (returns nil) for standalone tasks — those whose list is not linked to any phase — because they belong to no plan. Only a genuine lookup failure is surfaced as an error.
func (*Store) UpdatePhase ¶
func (s *Store) UpdatePhase(phaseID int64, upd PhaseUpdate) error
UpdatePhase applies the provided fields of upd to the phase identified by phaseID. Name is display-only and always editable, even after a commit has been recorded. Status is restricted to the active/dropped structural transition: dropping a phase that already has a commit_sha is rejected, since a committed phase represents real git history that must not be silently removed from the plan. A nil-everywhere update is a no-op error. Errors with ErrNotFound if the phase does not exist.
func (*Store) UpdatePlan ¶
func (s *Store) UpdatePlan(planID int64, upd PlanUpdate) error
UpdatePlan applies the provided fields of upd to the plan identified by planID. Only non-nil fields are written. Slug edits reject on a UNIQUE collision (no auto-suffix, unlike CreatePlan, since the caller is making a deliberate rename). A nil-everywhere update is a no-op error. Errors with ErrNotFound if the plan does not exist.
func (*Store) UpdatePlanStatus ¶
func (s *Store) UpdatePlanStatus(planID int64, status PlanStatus) error
UpdatePlanStatus sets the status of the plan identified by planID. Errors if the plan does not exist or the status is not a recognised PlanStatus value.
func (*Store) UpdateTicket ¶
func (s *Store) UpdateTicket(ticketID int64, upd TicketUpdate) error
UpdateTicket applies the provided fields of upd to the ticket identified by ticketID. name/slug/skills are always editable. Status is restricted to the structural pending/dropped transition: dropping a ticket whose branch_name is already set (started) is rejected, since executed git work must never be silently removed from the plan. Lifecycle statuses (in_progress, ready_for_review, reviewed, completed) must go through StartTicket / MarkTicketReadyForReview / MarkTicketReviewed / CompleteTicket instead, which also fire their hooks; UpdateTicket rejects them so a caller cannot bypass that path. A nil-everywhere update is a no-op error. Errors with ErrNotFound if the ticket does not exist.
type Ticket ¶
type Ticket struct {
ID int64
PlanID int64
Ord int
Name string
Slug string
Status TicketStatus
BranchName string
BaseRef string
StartedAt time.Time
PushedAt time.Time
StatusLastChangedAt time.Time
// Skills lists the names of available skills the planning model judged
// relevant to executing this ticket. /execute loads each one (via
// load_skill) when it starts the ticket. Empty when no skill was deemed
// relevant. Stored as a JSON array in the tickets.skills column.
Skills []string
}
Ticket is one row of the tickets table. A ticket maps to a stacked git branch, independently reviewable and deployable. Ord is the 1-based stacking order within the plan (ticket 1 branches from the plan's base; ticket N branches from ticket N-1). BranchName, BaseRef, and StartedAt are empty/zero until start_ticket cuts the branch; Status tracks the full lifecycle: pending → in_progress → ready_for_review → reviewed → completed, with dropped as a structural removal state for tickets that were never started. PushedAt is set by submit_ticket_for_review when the branch is pushed. StatusLastChangedAt is bumped every time Status changes (including on StartTicket/MarkTicketReadyForReview/MarkTicketReviewed/CompleteTicket and drop/restore), independent of StartedAt/PushedAt which mark specific lifecycle events rather than "status changed at all".
type TicketStatus ¶
type TicketStatus string
TicketStatus names the lifecycle state of a single ticket. The full lifecycle is:
pending → in_progress → ready_for_review → reviewed → completed
pending is the starting state when the ticket is created during planning. in_progress is set by start_ticket once the ticket's stacked branch has been cut. ready_for_review is set by submit_ticket_for_review after the branch has been pushed; this is when a PR is opened. reviewed is set by update_ticket("reviewed") after the review is complete. completed is an explicit, user-instructed final state set by update_ticket("completed"). dropped is a structural removal state set by update_ticket("dropped") for a ticket that has not been started yet (guarded so executed git work can never be dropped).
const ( TicketStatusPending TicketStatus = "pending" TicketStatusInProgress TicketStatus = "in_progress" TicketStatusReadyForReview TicketStatus = "ready_for_review" TicketStatusReviewed TicketStatus = "reviewed" TicketStatusCompleted TicketStatus = "completed" TicketStatusDropped TicketStatus = "dropped" )
func (TicketStatus) IsValid ¶
func (s TicketStatus) IsValid() bool
IsValid reports whether s is one of the recognised ticket statuses.
type TicketUpdate ¶
type TicketUpdate struct {
Name *string
Slug *string
Skills *[]string
Status *TicketStatus
}
TicketUpdate carries an optional, partial edit to a ticket: each non-nil pointer field is applied, each nil field is left unchanged. Skills is a pointer to a slice so a caller can distinguish "leave skills alone" (nil) from "replace with this set, possibly empty" (non-nil, sanitized by the tool layer before it reaches the store). Status here is restricted by UpdateTicket to the structural pending/dropped transitions — lifecycle transitions (in_progress, ready_for_review, reviewed, completed) go through their dedicated methods, which also fire hooks.
type TicketWithPhases ¶
type TicketWithPhases struct {
Ticket Ticket
Phases []PhaseWithTasks
}
TicketWithPhases pairs a ticket with its phases in stacking/display order.
Source Files
¶
- git.go
- paths.go
- plan.go
- render.go
- store.go