Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lab/internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func Run(ctx context.Context, args []string, in io.Reader, out, stderr io.Writer
fail := func(err error) int { fmt.Fprintln(stderr, "forgecell:", err); return 1 }
harness.SetRunningVersion(version)
if len(args) == 0 || args[0] == "--help" || args[0] == "help" {
fmt.Fprintln(out, "Forgecell Lab — Go migration preview\n\n"+demoUsage+"\ninit [--lab DIR] [--harness ID | --rebind ID] [--meta-harness ID | --meta-command JSON_ARGV] [--harness-timeout-ms MS] [--max-timeout-ms MS] [--json] [--review ID | --approve ID | --dismiss ID]\ninit --model ID [--role analysis|coding|meta|review] [--lab DIR] [--json]\ninit --refresh-baseline [--lab DIR] [--json]\ninit --enable-review [--lab DIR] [--json]\ndoctor [--lab DIR] [--harness ID] [--json] [--verbose]\ndoctor --clean [--yes] [--lab DIR]\nrun <issue> [--lab DIR] [--formula ID] [--base COMMIT] [--target BRANCH] [--approve DIGEST] [--json]\namend <molecule> [--add-scope PATH --reason TEXT]... [--add-acceptance TEXT]... [--note TEXT] [--continuation checks-only] [--accept-existing-tree TREE] [--run] [--lab DIR]\namend <molecule> --accept-protected-failures [--add-acceptance TEXT]... [--note TEXT] [--lab DIR] (declare the tests that failed by design in the latest protected regression run as expectedProtectedDeltas; continues checks-only; takes no --run: review the printed list, then approve the digest with run <issue> --approve DIGEST)\namend <molecule> --parent DIGEST --plan FILE [--accept-existing-tree TREE] [--run] [--lab DIR]\namend <molecule> --parent DIGEST --rebase-formula [--lab DIR]\nrecover <molecule> --plan DIGEST --confirm-stopped [--lab DIR]\nretire <molecule> --reason TEXT [--lab DIR] (mark a stale Molecule retired: one-line reason of at most 200 characters; every record is kept, the loop never resumes it and may start a fresh Molecule for its Issue)\nloop [--lab DIR] [--select oldest|label:L|milestone:M] [--max-concurrent N] [--once]\nloop [--lab DIR] --watch [--interval DURATION] [--learn]\nledger <molecule> [--lab DIR]\nissues --repo OWNER/REPO [--page N --page-size N | --select ISSUE_REF] [--lab DIR] [--json]\nlearn <molecule...> [--lab DIR] [--json]\nlearn --pending [--lab DIR] [--json]\nsuggestion <id> [--lab DIR] [--approve | --dismiss | --link-evaluation DIR | --baseline-ledger ID --candidate-ledger ID --comparability-basis TEXT] [--evaluation-parent DIR] [--json]\nrollback\ndev install [--from CHECKOUT] [--activate] [--allow-dirty]\nevaluate --inputs PLAN --out NEW_DIRECTORY [--source REPO] [--approve DIGEST]\nevaluate --cohort FILE --out DIRECTORY [--source REPO] [--approve DIGEST] [--parallel N] [--recheck]\nevaluate replay --repo OWNER/REPO --pr N [--pr M ...] --source CHECKOUT --out NEW_DIRECTORY\ndeliver <molecule> --lab DIR --base BRANCH [--approve DIGEST --title TITLE] -- FILE...\nchecks --lab DIR --molecule ID --repo OWNER/REPO --pr NUMBER --commit SHA --required NAME [--required NAME]\n"+findingsUsage+"\nlabs [--json]\nplugin test <manifest> [--json]\nui [--lab DIR] [--lab-name NAME] [--port N] [--no-open] [--demo]\n--version\n--protocol\n\nThe default Lab is keyed by the checkout's GitHub origin (OWNER/REPO), not its path; --lab DIR always wins and --lab-name NAME selects a named Lab for the repository on any command that takes a Lab. labs lists every Lab, read-only.\n\ndev install is developer-only: it builds the checkout's lab/cmd/forgecell with the local Go toolchain and then runs the result, so the checkout is trusted code. Without --activate the build is only staged under releases/ and current is unchanged. A tree with uncommitted, untracked or ignored Go source changes is refused unless --allow-dirty is given. --protocol prints the adapter protocol version.\n\nui serves the Lab UI on 127.0.0.1 behind a per-launch token and opens it in the browser unless --no-open is given. Its writes go through the engine and are bound to the digest you saw; it never merges or deploys. With no ready Lab it serves Set up your Lab; --demo shows synthetic sample data. It stops on SIGINT or SIGTERM.\n\n"+demoHelp+"\n\nRuns analyze a ticket before coding and wait for exact scope approval. Required checks run independently before final review.\n\nloop starts intake for eligible open Issues of the Formula's repository, oldest first, and approves a scope gate only where the Formula declares a policy that approves that exact plan. It stops each Molecule at any human gate, open question or failure and reports why; it never delivers, merges or approves a human gate. --once makes a single pass. --watch repeats single passes every --interval (default 5m, at least 1m) until SIGINT or SIGTERM, which stop it between passes after in-flight Molecules finish; it cannot be combined with --once. --learn, which is off by default, runs the pending learn requests after each pass like learn --pending; suggestions stay pending for your decision. For an interrupted Molecule whose owner is gone, the loop saves a recovery proposal (the exact forgecell recover command) under the Lab's recovery-proposals directory and shows it in its report and in doctor; it never runs recover or amend.\n\nTicket readiness is a development preview; existing Formulas require an explicitly reviewed scope gate.")
fmt.Fprintln(out, "Forgecell Lab — Go migration preview\n\n"+demoUsage+"\ninit [--lab DIR] [--harness ID | --rebind ID] [--meta-harness ID | --meta-command JSON_ARGV] [--harness-timeout-ms MS] [--max-timeout-ms MS] [--json] [--review ID | --approve ID | --dismiss ID]\ninit --model ID [--role analysis|coding|meta|review] [--lab DIR] [--json]\ninit --refresh-baseline [--lab DIR] [--json]\ninit --enable-review [--lab DIR] [--json]\ndoctor [--lab DIR] [--harness ID] [--json] [--verbose]\ndoctor --clean [--yes] [--lab DIR]\nrun <issue> [--lab DIR] [--formula ID] [--base COMMIT] [--target BRANCH] [--approve DIGEST] [--json]\namend <molecule> [--add-scope PATH --reason TEXT]... [--add-acceptance TEXT]... [--note TEXT] [--continuation checks-only] [--accept-existing-tree TREE] [--run] [--lab DIR]\namend <molecule> --accept-protected-failures [--add-acceptance TEXT]... [--note TEXT] [--lab DIR] (declare the tests that failed by design in the latest protected regression run as expectedProtectedDeltas; continues checks-only; takes no --run: review the printed list, then approve the digest with run <issue> --approve DIGEST)\namend <molecule> --parent DIGEST --plan FILE [--accept-existing-tree TREE] [--run] [--lab DIR]\namend <molecule> --parent DIGEST --rebase-formula [--lab DIR]\nrecover <molecule> --plan DIGEST --confirm-stopped [--lab DIR]\nretire <molecule> --reason TEXT [--lab DIR] (mark a stale Molecule retired: one-line reason of at most 200 characters; every record is kept, the loop never resumes it and may start a fresh Molecule for its Issue)\nloop [--lab DIR] [--select oldest|label:L|milestone:M] [--max-concurrent N] [--once]\nloop [--lab DIR] --watch [--interval DURATION] [--learn]\nledger <molecule> [--lab DIR]\nissues --repo OWNER/REPO [--page N --page-size N | --select ISSUE_REF] [--lab DIR] [--json]\nlearn <molecule...> [--lab DIR] [--json]\nlearn --pending [--lab DIR] [--json]\nsuggestion <id> [--lab DIR] [--approve | --dismiss | --link-evaluation DIR | --baseline-ledger ID --candidate-ledger ID --comparability-basis TEXT] [--evaluation-parent DIR] [--json]\nrollback\ndev install [--from CHECKOUT] [--activate] [--allow-dirty]\nevaluate --inputs PLAN --out NEW_DIRECTORY [--source REPO] [--approve DIGEST]\nevaluate --cohort FILE --out DIRECTORY [--source REPO] [--approve DIGEST] [--parallel N] [--recheck]\nevaluate replay --repo OWNER/REPO --pr N [--pr M ...] --source CHECKOUT --out NEW_DIRECTORY\ndeliver <molecule> --lab DIR --base BRANCH [--approve DIGEST --title TITLE] -- FILE...\nchecks --lab DIR --molecule ID --repo OWNER/REPO --pr NUMBER --commit SHA --required NAME [--required NAME]\n"+findingsUsage+"\nlabs [--json]\n"+pluginHelp+"\nui [--lab DIR] [--lab-name NAME] [--port N] [--no-open] [--demo]\n--version\n--protocol\n\nThe default Lab is keyed by the checkout's GitHub origin (OWNER/REPO), not its path; --lab DIR always wins and --lab-name NAME selects a named Lab for the repository on any command that takes a Lab. labs lists every Lab, read-only.\n\ndev install is developer-only: it builds the checkout's lab/cmd/forgecell with the local Go toolchain and then runs the result, so the checkout is trusted code. Without --activate the build is only staged under releases/ and current is unchanged. A tree with uncommitted, untracked or ignored Go source changes is refused unless --allow-dirty is given. --protocol prints the adapter protocol version.\n\nui serves the Lab UI on 127.0.0.1 behind a per-launch token and opens it in the browser unless --no-open is given. Its writes go through the engine and are bound to the digest you saw; it never merges or deploys. With no ready Lab it serves Set up your Lab; --demo shows synthetic sample data. It stops on SIGINT or SIGTERM.\n\n"+demoHelp+"\n\nRuns analyze a ticket before coding and wait for exact scope approval. Required checks run independently before final review.\n\nloop starts intake for eligible open Issues of the Formula's repository, oldest first, and approves a scope gate only where the Formula declares a policy that approves that exact plan. It stops each Molecule at any human gate, open question or failure and reports why; it never delivers, merges or approves a human gate. --once makes a single pass. --watch repeats single passes every --interval (default 5m, at least 1m) until SIGINT or SIGTERM, which stop it between passes after in-flight Molecules finish; it cannot be combined with --once. --learn, which is off by default, runs the pending learn requests after each pass like learn --pending; suggestions stay pending for your decision. For an interrupted Molecule whose owner is gone, the loop saves a recovery proposal (the exact forgecell recover command) under the Lab's recovery-proposals directory and shows it in its report and in doctor; it never runs recover or amend.\n\nTicket readiness is a development preview; existing Formulas require an explicitly reviewed scope gate.")
return 0
}
if args[0] == "--version" || args[0] == "-v" {
Expand Down
35 changes: 27 additions & 8 deletions lab/internal/cli/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,33 @@ import (
"github.com/DanielNetzer/forgecell/lab/internal/plugin/conformance"
)

const pluginUsage = "usage: forgecell plugin test <manifest> [--json] | forgecell plugin list [--lab DIR] [--lab-name NAME] [--json] | forgecell plugin doctor [NAME] [--lab DIR] [--lab-name NAME] [--json]"
// The form of each plugin command, without "usage: forgecell ". pluginUsage,
// the per-command usages and the --help lines are all built from these, so
// they cannot drift apart.
const (
pluginNewCommand = "plugin new --kind tracker|forge|reviewer --lang go|python|typescript [--dir DIR] <name>"
pluginPinCommand = "plugin pin <dir> --server <path> [--interpreter <path>]"
pluginTestCommand = "plugin test <manifest> [--json] [--live --target T [--config FILE] [--change REF --commit SHA] [--allow-write --head BRANCH --base BRANCH]]"
pluginListCommand = "plugin list [--lab DIR] [--lab-name NAME] [--json]"
pluginDoctorCommand = "plugin doctor [NAME] [--lab DIR] [--lab-name NAME] [--json]"
)

// pluginUsage names every plugin command, in the order new, pin, test, list
// and doctor.
const pluginUsage = "usage: forgecell " + pluginNewCommand +
" | forgecell " + pluginPinCommand +
" | forgecell " + pluginTestCommand +
" | forgecell " + pluginListCommand +
" | forgecell " + pluginDoctorCommand

// pluginHelp is the plugin commands' lines of forgecell --help, one per line.
const pluginHelp = pluginNewCommand + "\n" + pluginPinCommand + "\n" + pluginTestCommand + "\n" +
pluginListCommand + "\n" + pluginDoctorCommand

// The usage of plugin new and plugin pin, printed by those commands only, so
// the usage that test, list and doctor print stays as it was.
// The usage of plugin new and plugin pin, printed by those commands only.
const (
pluginNewUsage = "usage: forgecell plugin new --kind tracker|forge|reviewer --lang go|python|typescript [--dir DIR] <name>"
pluginPinUsage = "usage: forgecell plugin pin <dir> --server <path> [--interpreter <path>]"
pluginNewUsage = "usage: forgecell " + pluginNewCommand
pluginPinUsage = "usage: forgecell " + pluginPinCommand
)

// pluginTestOptions configures plugin test runs; tests replace it to keep runs
Expand Down Expand Up @@ -101,9 +121,8 @@ func runPlugin(ctx context.Context, args []string, out, stderr io.Writer) int {
return 0
}

// pluginTestLiveUsage is printed only when live flags are refused, so the usage
// that test, list and doctor print stays as it was.
const pluginTestLiveUsage = "usage: forgecell plugin test <manifest> [--json] [--live --target T [--config FILE] [--change REF --commit SHA] [--allow-write --head BRANCH --base BRANCH]]"
// pluginTestLiveUsage is printed when live flags are refused.
const pluginTestLiveUsage = "usage: forgecell " + pluginTestCommand

// Bounds of a --config file: the contract's config maxProperties, and 64 KiB.
const (
Expand Down
20 changes: 12 additions & 8 deletions lab/internal/cli/plugin_new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import (
"github.com/DanielNetzer/forgecell/lab/internal/plugin/plugintest"
)

// pluginUsageBeforeNew is the usage of plugin test, list and doctor, which the
// new and pin commands extend without changing.
const pluginUsageBeforeNew = "usage: forgecell plugin test <manifest> [--json] | forgecell plugin list [--lab DIR] [--lab-name NAME] [--json] | forgecell plugin doctor [NAME] [--lab DIR] [--lab-name NAME] [--json]"
// pluginUsageAllCommands is the usage that names every plugin command: new,
// pin, test with its live flags, list and doctor.
const pluginUsageAllCommands = "usage: forgecell plugin new --kind tracker|forge|reviewer --lang go|python|typescript [--dir DIR] <name>" +
" | forgecell plugin pin <dir> --server <path> [--interpreter <path>]" +
" | forgecell plugin test <manifest> [--json] [--live --target T [--config FILE] [--change REF --commit SHA] [--allow-write --head BRANCH --base BRANCH]]" +
" | forgecell plugin list [--lab DIR] [--lab-name NAME] [--json]" +
" | forgecell plugin doctor [NAME] [--lab DIR] [--lab-name NAME] [--json]"

// noAbsolutePath fails when text names the temporary root or holds any
// absolute path.
Expand Down Expand Up @@ -186,20 +190,20 @@ func TestPluginPinPrintsTheDigestAndFormulaLine(t *testing.T) {
}

func TestPluginUsageKeepsTestListAndDoctor(t *testing.T) {
if pluginUsage != pluginUsageBeforeNew {
t.Fatalf("the test, list and doctor usage changed:\n%s", pluginUsage)
if pluginUsage != pluginUsageAllCommands {
t.Fatalf("the plugin usage is not the all-commands line:\n%s", pluginUsage)
}
if pluginNewUsage != "usage: forgecell plugin new --kind tracker|forge|reviewer --lang go|python|typescript [--dir DIR] <name>" ||
pluginPinUsage != "usage: forgecell plugin pin <dir> --server <path> [--interpreter <path>]" {
t.Fatalf("new and pin usage: %q, %q", pluginNewUsage, pluginPinUsage)
}
// Each usage error of plugin test, list and doctor prints the base text,
// byte for byte.
// Each usage error of plugin test, list and doctor prints the all-commands
// line, byte for byte.
for _, args := range [][]string{
{"plugin"}, {"plugin", "frobnicate"}, {"plugin", "test"}, {"plugin", "test", "a", "b"},
{"plugin", "list", "extra"}, {"plugin", "doctor", "a", "b"},
} {
if code, out, errOut := runCLI(args...); code != 1 || out != "" || errOut != "forgecell: "+pluginUsageBeforeNew+"\n" {
if code, out, errOut := runCLI(args...); code != 1 || out != "" || errOut != "forgecell: "+pluginUsageAllCommands+"\n" {
t.Errorf("%v: exit %d, %q%q", args, code, out, errOut)
}
}
Expand Down
Loading
Loading