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
24 changes: 22 additions & 2 deletions lab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ approved Formula with only the target binding keys and those recorded launcher
rewrites applied, and `--rebind` refuses with "nothing to rebind" when no key would
change.

`forgecell init --lab DIR --harness ID --role analysis|coding|meta` proposes a reviewed,
binding-only change to one role override through the same flow (pending proposal,
`--review`, `--approve`). If `harnesses.ROLE` is declared it rebinds it to `ID` with the
native command `[launcher, "__adapter", ID, provider]` on the running launcher, keeping its
other keys; otherwise it declares it with that binding and command. `ID` must be a
discovered, ready harness whose adapter can perform the role (for example Cursor is refused
for `analysis`, with Cursor's own reason). A request that leaves the role's effective
binding unchanged, such as `--role coding` with the default harness, is refused with
"nothing to change"; there is no remove mode. The proposal is verified to equal the approved
Formula with only `harnesses.ROLE` binding and command changed, each change is listed as
`path: old -> new` (or `path: (none) -> new`), and the result must still pass Formula
validation, so for example a coding override must match the harness Atom's binding and a
meta override must match the learn Atom's command. It needs a current exact approval and no
pending proposal, and cannot be combined with `--rebind`, `--model`, `--meta-harness`,
`--meta-command`, the timeout flags, `--review`, `--approve` or `--dismiss`. `--role`
without `--harness` or `--model` is refused. When `init --harness ID` on an approved Formula
is refused because `ID` cannot do read-only ticket analysis and no `harnesses.analysis` is
declared, declare one first, for example with
`forgecell init --lab DIR --harness claude-code --role analysis`.

## Review a ticket (development build)

Ticket readiness on this branch is **not in the published `0.2.0-preview.1`**.
Expand Down Expand Up @@ -385,8 +405,8 @@ rewritten. `ID` is validated with the same rules as `harness.model`.
Atom's command, which has no model, and does not inherit it. The proposal summary states
this for each such role, derived from the resolution code.
- With `--role ROLE` it changes only `harnesses.ROLE`. If that override is not declared the
proposal is refused, naming the role; run without `--role` or declare the override first.
Init never creates an override.
proposal is refused, naming the role; run without `--role` or declare the override first
with `init --harness ID --role ROLE` (below). `--model --role` never creates an override.
- Setting a pin on a binding that is not native Claude Code is refused: the message names
each Codex, Cursor or custom command binding (Forgecell cannot enforce a pin on a custom
command), and without `--role` the whole proposal is refused. A Formula that declares no
Expand Down
24 changes: 19 additions & 5 deletions 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]\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. After its first pass, a watch prints only what changed, plus a one-line summary. --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 --harness ID --role analysis|coding|meta [--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. After its first pass, a watch prints only what changed, plus a one-line summary. --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 Expand Up @@ -411,7 +411,7 @@ func Run(ctx context.Context, args []string, in io.Reader, out, stderr io.Writer
timeoutMS := flags.Int("harness-timeout-ms", 0, "Reviewed coding attempt budget in ms (default 900000)")
maxTimeoutMS := flags.Int("max-timeout-ms", 0, "Ceiling for reviewed per-Molecule timeouts in ms (default 3600000)")
model := flags.String("model", "", "Propose a model pin on the approved Formula's native Claude bindings; an empty value removes the pin")
role := flags.String("role", "", "With --model, limit the pin to one declared harnesses.ROLE override (analysis, coding, meta or review)")
role := flags.String("role", "", "With --model, limit the pin to one declared harnesses.ROLE override (analysis, coding, meta or review); with --harness, propose declaring or rebinding the harnesses.ROLE override (analysis, coding or meta) for review")
refreshBaseline := flags.Bool("refresh-baseline", false, "Propose a baseline-only requiredChecks refresh of the approved Formula, derived from the current CI workflows, for review")
enableReview := flags.Bool("enable-review", false, "Propose inserting the adversarial review Atom after the check Atom of the approved Formula, for review")
forgePlugin := flags.String("forge-plugin", "", "Lab-relative manifest of a forge plugin; with --project, --tracker-plugin and --tracker-config, propose a bootstrap Formula whose forge and tracker are plugins")
Expand Down Expand Up @@ -487,10 +487,24 @@ func Run(ctx context.Context, args []string, in io.Reader, out, stderr io.Writer
return fail(fmt.Errorf("--refresh-baseline proposes a change; review, approve or dismiss the saved proposal in a separate command"))
}
}
if modelRequested || *role != "" {
roleHarness := *role != "" && !modelRequested && *preferred != ""
if roleHarness {
switch {
case rebindRequested:
return fail(fmt.Errorf("--role cannot be combined with --rebind; --rebind changes the default binding, while --harness ID --role ROLE changes only harnesses.ROLE"))
case metaRequested:
return fail(fmt.Errorf("--harness ID --role ROLE cannot be combined with --meta-harness or --meta-command; it changes only harnesses.ROLE"))
case *timeoutMS != 0 || *maxTimeoutMS != 0:
return fail(fmt.Errorf("--harness ID --role ROLE cannot be combined with the timeout flags; it changes only harnesses.ROLE"))
case *review != "" || *approve != "" || *dismiss != "":
return fail(fmt.Errorf("--harness ID --role ROLE proposes a change; review, approve or dismiss the saved proposal in a separate command"))
}
} else if modelRequested || *role != "" {
switch {
case !modelRequested && rebindRequested:
return fail(fmt.Errorf("--role cannot be combined with --rebind; use --harness ID --role ROLE to change one role, or --model ID --role ROLE to pin its model"))
case !modelRequested:
return fail(fmt.Errorf("--role needs --model"))
return fail(fmt.Errorf("--role needs --model ID to pin a model, or --harness ID to propose a role harness override"))
case *preferred != "" || rebindRequested || metaRequested || *timeoutMS != 0 || *maxTimeoutMS != 0:
return fail(fmt.Errorf("--model changes only the model pin; do not combine it with --harness, --rebind, --meta-harness, --meta-command or the timeout flags"))
case *review != "" || *approve != "" || *dismiss != "":
Expand Down Expand Up @@ -600,7 +614,7 @@ func Run(ctx context.Context, args []string, in io.Reader, out, stderr io.Writer
for _, key := range []string{"CODEX_THREAD_ID", "CLAUDECODE", "CURSOR_AGENT", "TERM_PROGRAM"} {
env[key] = os.Getenv(key)
}
result, err := onboarding.Prepare(ctx, onboarding.InitOptions{Cwd: cwd, LabDir: dir, Launcher: launcher, Preferred: *preferred, Rebind: *rebind, Env: env, MetaHarness: *metaHarness, MetaCommand: metaCommand, HarnessTimeoutMS: *timeoutMS, MaxTimeoutMS: *maxTimeoutMS, Model: *model, ModelSet: modelRequested, Role: *role, RefreshBaseline: *refreshBaseline, EnableReview: *enableReview, ForgePlugin: *forgePlugin, Project: *project, TrackerPlugin: *trackerPlugin, TrackerConfig: *trackerConfig})
result, err := onboarding.Prepare(ctx, onboarding.InitOptions{Cwd: cwd, LabDir: dir, Launcher: launcher, Preferred: *preferred, Rebind: *rebind, Env: env, MetaHarness: *metaHarness, MetaCommand: metaCommand, HarnessTimeoutMS: *timeoutMS, MaxTimeoutMS: *maxTimeoutMS, Model: *model, ModelSet: modelRequested, Role: *role, RoleHarness: roleHarness, RefreshBaseline: *refreshBaseline, EnableReview: *enableReview, ForgePlugin: *forgePlugin, Project: *project, TrackerPlugin: *trackerPlugin, TrackerConfig: *trackerConfig})
if err != nil {
return fail(err)
}
Expand Down
Loading
Loading