Documentation
¶
Overview ¶
Package feature models parallel feature development inside one cloop project (Task 20341).
A project has one task list and one set of run settings, because it has one .cloop/ directory and one working tree. That is the right shape for a single line of work and the wrong one for three: two features developed in the same checkout edit the same files, their tasks interleave in one plan, and there is no way to say "evolve this one, but let that one finish and stop".
A feature is the smallest unit that removes all three problems at once: a git worktree of the project's repository, on its own branch, with its own .cloop/ directory. Everything cloop already knows how to do to a project — run it, stop it, evolve it, edit its tasks, toggle innovate mode — it can then do to the feature without learning anything new, because a feature *is* a project as far as the orchestrator is concerned. What this package adds is the part that is not: where features live, how one is recognised, and what is recorded about it.
Layout ¶
<project>/.cloop/features/<slug>/ the worktree (branch cloop/feature/<slug>) <project>/.cloop/features/<slug>/.cloop/ the feature's own state, config and task list <project>/.cloop/features/<slug>/.cloop/feature.json what this package records
Inside the parent's .cloop/ rather than beside the project, for three reasons. A feature is then deleted with its project and never outlives it on disk; an executor that bind-mounts the project directory can reach it; and — the one that decides it — the parent is derivable from the path alone. Inheritance of the parent's executor binding, grants and authorization (see ParentOf) is a security property, and one that holds by construction is better than one that holds because a registry entry says so.
The cost of that choice is that everything which treats "a directory below the project" as "the project" has to learn that a feature subtree is not. The process-scoping helpers in pkg/multiui and the .cloop/ walkers in pkg/snapshot, pkg/diskusage and `cloop clean` are the places that do.
What lives here and what does not ¶
This package is pure: it reads and writes files and never runs a process. That is deliberate, because pkg/ui — which may not spawn processes, see pkg/ui/no_direct_exec_test.go — lists features on every projects refresh. Creating, removing and publishing a feature means running git and talking to GitHub, and lives in pkg/featureops, which only the CLI imports.
Index ¶
- Constants
- func BranchName(slug string) string
- func Dir(projectDir string) string
- func InsideFeature(path string) (featureRoot string, ok bool)
- func IsFeature(path string) bool
- func MetaPath(featureDir string) string
- func Orphans(projectDir string) (map[string]string, error)
- func PRBody(m *Meta, tasks []*pm.Task) string
- func PRTitle(m *Meta) string
- func ParentOf(path string) (parent, slug string, ok bool)
- func Path(projectDir, slug string) string
- func SaveMeta(featureDir string, m *Meta) error
- func Slugify(name string) string
- func ValidSlug(slug string) error
- type Info
- type Meta
- type PR
- type Return
Constants ¶
const ( // ControlDir is the name of a project's control directory. ControlDir = ".cloop" // DirName is the directory under ControlDir that holds feature worktrees. DirName = "features" // MetaFile is the feature's own record, inside the feature's ControlDir. MetaFile = "feature.json" // BranchPrefix names every feature branch. It sits inside the cloop/ // namespace on purpose: that is the git interception proxy's default // write-back allowlist (gitproxy.DefaultAllowedRef), so a sandbox can push // a feature branch through the proxy without anyone widening a policy — // and can still not push main. BranchPrefix = "cloop/feature/" // MaxSlugLen bounds a slug. Slugs become directory names and branch // components; 40 keeps both readable and far from any path limit. MaxSlugLen = 40 // MaxPerProject bounds how many features one project may hold. Each one is // a full checkout plus a harness that can run concurrently with the rest, // so an unbounded count is an unbounded amount of disk and parallelism // reachable by anyone who can create one. The figure is generous for // people and small for a loop. MaxPerProject = 32 )
Variables ¶
This section is empty.
Functions ¶
func BranchName ¶
BranchName returns the branch a feature is developed on.
func InsideFeature ¶
InsideFeature reports whether path is a feature root or anything below one, returning that feature's root. Used by the process-scoping helpers, which see working directories such as <feature>/.cloop/worktrees/task-3 and have to attribute them to the feature rather than to its parent.
func IsFeature ¶
IsFeature reports whether path is the root of a real feature: shaped like one (ParentOf) and carrying a readable record for the same slug.
The record's Parent field is deliberately not compared with the parent the path implies. The path is authoritative — it is what inheritance follows — and a project directory that was moved or reached through a different mount would otherwise lose every feature it has to a string mismatch in a file nobody edits. The slug is compared because a record copied into the wrong directory is a record describing some other feature.
func Orphans ¶
Orphans returns the directories under .cloop/features that List skipped, with the reason, so an operator can see what is taking up room.
func PRBody ¶
PRBody renders the description of a feature's pull request from its record and its task list: what the feature is for, which tasks were completed, and which were not — so a reviewer can tell a finished feature from one being proposed early without opening the dashboard.
It is deterministic on purpose. The PR is opened from a button, often by someone other than the person reviewing it, and a description that changes on every click — or that needs a model call to exist at all — is a worse default than one that says exactly what the plan says.
func ParentOf ¶
ParentOf reports whether path is, by its shape, the root of a feature worktree — <parent>/.cloop/features/<slug> with a valid slug — and if so returns the parent project directory and the slug.
It is purely lexical. It does not check that anything exists, which is what makes it usable from hot paths (every executor dispatch consults it) and what makes it trustworthy: the answer cannot be changed by writing a file. Callers that need to know a feature is real rather than merely well-named use IsFeature.
A nested feature — a feature of a feature — is not recognised: the parent of such a path would itself be a feature, and ParentOf reports false for it. Features are one level deep by design (see pkg/featureops.Create).
func SaveMeta ¶
SaveMeta writes a feature's record atomically. The control directory must exist — it is created with the feature's state.
Types ¶
type Info ¶
Info is one discovered feature.
func List ¶
List returns the features of projectDir, oldest first.
It only reads files. A directory under .cloop/features that has no valid record — a half-created feature, a worktree someone made by hand, the leftovers of a crash — is skipped rather than reported: listing is what the dashboard does every two seconds, and it must describe what is usable, not fail on what is not. `cloop feature list` reports the skipped ones.
A project that is itself a feature has no features.
type Meta ¶
type Meta struct {
Version int `json:"version"`
Slug string `json:"slug"`
Title string `json:"title"`
// Description is the feature's goal as the user wrote it.
Description string `json:"description,omitempty"`
Branch string `json:"branch"`
// Base is the branch the feature was cut from and the one its pull
// request targets.
Base string `json:"base"`
// BaseCommit is the commit Base pointed at when the feature was created.
BaseCommit string `json:"base_commit,omitempty"`
// Parent is the project directory this feature belongs to, absolute.
Parent string `json:"parent"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
// AutoPR asks the hub to open the pull request by itself once the
// feature's plan completes.
AutoPR bool `json:"auto_pr,omitempty"`
PR *PR `json:"pr,omitempty"`
// Return is the outcome of the last run's write-back, for a feature run
// on an isolating executor; nil for one that ran on the hub.
Return *Return `json:"return,omitempty"`
}
Meta is a feature's record, persisted as .cloop/feature.json inside the feature's worktree.
type PR ¶
type PR struct {
Number int `json:"number"`
URL string `json:"url"`
// State is GitHub's state as last seen: open, closed or merged.
State string `json:"state"`
// Repo is owner/name on the forge the PR was opened on.
Repo string `json:"repo,omitempty"`
Head string `json:"head,omitempty"`
Base string `json:"base,omitempty"`
// HeadSHA is the commit the branch pointed at when the PR was opened or
// last refreshed, so the dashboard can tell "the PR shows this work" from
// "there are commits the PR does not have yet".
HeadSHA string `json:"head_sha,omitempty"`
Draft bool `json:"draft,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
PR is what is recorded about a feature's pull request.
type Return ¶ added in v0.0.5
type Return struct {
At time.Time `json:"at"`
// Outcome is fast_forwarded, conflict (the work was kept on KeptOn
// rather than applied), nothing (the run made no commits), or failed
// (the work did not come back; Message says why).
Outcome string `json:"outcome"`
// Commit is the returned tip, when there was one.
Commit string `json:"commit,omitempty"`
// KeptOn is the branch conflicting work was kept on.
KeptOn string `json:"kept_on,omitempty"`
// Message is the operator-facing account.
Message string `json:"message,omitempty"`
// Executor is where the run happened.
Executor string `json:"executor,omitempty"`
}
Return is what became of the work of a feature's last run on an executor that isolates from the hub (Task 20367): such a run works on a standalone checkout of the feature's branch, and its commits come back as a bundle the hub vets and applies to the worktree here — by fast-forward only.