Experiment runner extension - #835
Open
ajonkisz wants to merge 71 commits into
Open
Conversation
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…p UI Add a simple key/value metadata editor to the goal creation/proposal form. Values JSON-parse when possible, else stay strings; empty editor sends no metadata. createGoal forwards metadata only when non-empty. Carry metadata through the proposal accept/mirror path. Remove the superseded per-goal worktreeSetupCommand/worktreeSetupTimeoutMs UI state, controls, and API opts (component/project worktree_setup_command UI untouched). Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
- REST goal creation accepts non-empty plain-object `metadata` and passes it to createGoal; remove the superseded per-goal worktreeSetupCommand / worktreeSetupTimeoutMs REST parsing and createGoal pass-through (PR #816). - Wire the shared LifecycleHub `goalMetadataResolver` to route STRICTLY by goalId via projectContextManager.getContextForGoal (ignoring caller projectId, warning + {} on unknown goal). - Wire the per-project pipeline `resolveGoalMetadata` closure in buildPipelineContext, bound to that project's GoalManager. - Feature-detect + wire the GoalManager `goalProvisioned` dispatcher into the context configurator (no-op until the data + lifecycle slices land). - createDelegateSession: stamp the parent's effective goal as the delegate's teamGoalId (plan + live session + persisted store record) so team_delegate sub-agents inherit the same hierarchical metadata (anti-asymmetry invariant). - propose_goal.yaml: add optional `metadata`; remove per-goal worktree setup command/timeout params + docs. - Remove obsolete per-goal worktree-setup-command API E2E specs (behaviour removed; new metadata/hook coverage is the tester slice's). Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Supersede PR #816's per-goal worktreeSetupCommand/worktreeSetupTimeoutMs with arbitrary goal metadata across the proposal surface: - propose_goal TypeBox schema (extension.ts): remove worktree setup fields, add optional namespaced metadata object. - proposal-types.ts: GOAL_FRONTMATTER_KEYS carries metadata (not setup fields); validate metadata as a plain object when present. - mock-agent-core.mjs: replace GOAL_PROPOSAL_WORKTREE_SETUP fixture with a GOAL_PROPOSAL_METADATA fixture seeding namespaced metadata. - proposal-files.test.ts: metadata round-trip + empty-object-no-override + malformed-metadata rejection coverage. - Delete stale goal-worktree-setup-command.spec.ts browser E2E (the metadata editor UI lands under a separate task; its browser E2E belongs there). Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…macOS Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…igin/goal/f6c39aa2/coder-c704', 'origin/goal/f6c39aa2/coder-9681', 'origin/goal/f6c39aa2/coder-ae61' and 'origin/goal/f6c39aa2/coder-5d66' into goal/f6c39aa2/test-engineer-4b1b
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…, 'goal/f6c39aa2/coder-9681', 'goal/f6c39aa2/coder-ae61' and 'goal/f6c39aa2/coder-5d66' into goal/f6c39aa2/coder-8f9e
Thread effective goal id (goalId ?? teamGoalId) through the restore / respawn / force-abort tool-activation paths so bobbit.disabledTools and provider-bridge filtering survive restart; use the effective goal id at the afterTurn / sessionShutdown lifecycle dispatches so members/delegates/ reviewers filter disabled providers; pass the offset (agent) cwd to the session goalProvisioned hook instead of the branch-container root; emit Role as its own labeled, reorderable prompt section matching the inspector; and deep-clone arrays in deepMergeMetadata so resolved metadata can never mutate persisted goal arrays. Adds focused unit tests for each. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…rarchical-g-f6c39aa2
1. team_spawn member worktrees now dispatch goalProvisioned with resolved effective metadata via SessionManager.dispatchGoalProvisionedForWorktree, so metadata-driven filesystem treatments land on every member worktree. 2. system-prompt: restore role-only backward compatibility — a role-only session with no sectionOrder keeps the historical # Goal-wrapped shape; metadata-supplied promptSectionOrder still reorders Role standalone. 3. mirrorGoalSetupFields clears stale previewMetadataRows for authoritative empty/absent proposal metadata (slot + merged paths), leaving raw streaming partials untouched. 4. tool-activation no-toolManager fallback applies disabledTools to fallback builtins, including built-in file tools. 5. Focused regression tests for all four fixes. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Two root causes made the goal-metadata browser E2E lose manually-entered rows at submit: 1. renderApp() is rAF-throttled, so the metadata editor's @input handlers, which closed over the render-time 'rows' snapshot, recomputed the full row array from stale state. A rapid key-then-value fill (or Add right after a fill) used the same stale snapshot, so the second edit overwrote the first -> blank-key rows dropped at submit. Switch onMetadataRowsChange to a functional updater applied against the live current rows; both call sites (goal-assistant + proposal-review panels) resolve it against their own source of truth. 2. An authoritative proposal reconcile with no metadata cleared previewMetadataRows even after the user had typed rows. Add a previewMetadataEdited guard (mirrors previewTitle/Spec/CwdEdited) so mirrorGoalSetupFields becomes a no-op once the user edits the rows. Both are additive/backward-compatible; absent metadata still byte-identical. The two unit browser-fixture __ready timeouts were unrelated bundling flake (both pass clean) and are not caused by these changes. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…ox goalProvisioned dispatch Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…ltering can't widen to all tools Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
… respawn Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…preserving contract Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…allowlist as no skills resolvePrompt assembled/cached tool docs before resolveToolActivation filtered bobbit.disabledTools, so the initial system prompt and prompt- sections snapshot could advertise tools the live surface had removed. Apply the disabled-tools filter in _resolvePrompt (normal/delegate branches, and re-applied after the assistant branch recomputes its role-restricted allowlist), preserving undefined (unrestricted) vs [] (explicit no-tools). computeSkillsCatalog treated allowedTools=[] as unrestricted (length>0 gate), emitting an Available Skills affordance for a session with no activate_skill. Treat [] as no skills (no section). Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…antics Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Single-source reporting lib for the experiment-runner extension: - types.ts: canonical ExperimentDef/VariantDef/RunRecord/RunStatus/ CompletionBar/MetricSelection/DashboardSpec/WidgetSpec/ReportModel - aggregate.ts: median/spread/same-completion-bar filtering + direction-aware compare/winner/delta (the only median/percentile definition site) - series.ts: best-so-far curve (correctness-gated), decideCandidate, isPlateau/hitTarget/budget predicates + evaluateStop shared by loop & dashboard - widgets/: single registry + 6 canonical built-ins (comparison-table, score-bars, objective-curve, ledger-table, summary-cards, raw-drilldown), theme-token-only HTML helpers - index.ts: buildReportModel/renderReportHtml/resolveDashboard entry points - tests: aggregate, series, widgets, single-source/no-fork + schema parity Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Add the single core/host change the experiment-runner pack needs: a 7th host.agents verb, spawnGoal, that launches a CHILD GOAL of the bound session's effective goal carrying a per-arm treatment (metadata + inlineRoles + workflow). Parent is server-derived; a mismatched parentGoalId assertion is rejected. Rides capabilities.agents (no new flag), no goalStatus, no cost-cap opts. Extracts the shared nested-goal creation lifecycle into experiment-spawn-goal.ts (cwd derivation, workflow cascade, inlineRoles merge, nesting/pause guards, runKey idempotency, gate init, cap-aware scheduled team start) so the host path and the REST spawn-child path agree. Wires the closure into both route/action host sites; provider host stays masked. Proxies spawnGoal through the confined worker allowlist + bootstrap. Adds unit tests for surface shape, forwarding, validation, recursion denial, backend-unavailable, masked denial, createGoal mapping, parent derivation/assertion, idempotency, and guard refusals. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…, reporting lib) Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Add the experiment-runner pack's front-door UI: one side panel that is a
four-view state machine (mode-select → define → confirm → dashboard), the
three entrypoints (composer-slash, command-palette, deep-link route), and the
pack manifest.
- A/B is the safe, bounded default; Autoresearch is an explicit opt-in that
refuses to launch until ≥1 hard cap, ≥1 stop condition, a per-iteration
budget, and an explicit danger acknowledgement are set (guardrail checklist).
- Panel calls only the canonical route names (defineExperiment, projectCost,
launch, poll, collect, aggregate, iterate, listExperiments, getExperiment,
saveMetrics, saveDashboard, report, listMetrics, listWidgets, cancel) and
degrades gracefully to the pack store when a route is unavailable.
- Dashboard renders from the report route's { model, html } single source of
truth, falling back to a spec-driven client render of the built-in widgets;
metrics + dashboard-spec edits re-render from stored outcomes (no re-run).
- Draft, definition, dashboard spec and metric selection persist via host.store
plus a localStorage mirror for reload/deep-link rehydration.
- Browser E2E covers A/B default, validation gating, autoresearch guardrails,
draft persistence across reload, dashboard render + spec edit, and clean
install/uninstall reconcile.
Build the panel bundle via scripts/build-market-packs.mjs (lib/panel.js).
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…404c Co-Authored-By: Bobbit (Gpt 5.5) <bobbit@bobbit.ai> # Conflicts: # market-packs/experiment-runner/pack.yaml
…cile adapters - Add first-party copy allowlist entry for experiment-runner. - build:packs: bundle src/shared/experiment-report/index.ts to lib/experiment-report.mjs (platform node) via new inFromRoot entry, alongside the panel bundle; regenerate lib/panel.js as the proper minified bundle (was a stale source copy). - Add object-form facades (aggregateExperiment, computeBestSoFar, objectiveSeries, budgetStatus) to the shared index as single-source wrappers over aggregate.ts/series.ts so the pack adapters resolve against real exports; no forked median/percentile/accept-stop logic. - autoresearch.mjs: align decideCandidate call to canonical signature and fold the correctness gate through shared isRunVerified; guard shouldStop when no objective. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…hape, camelCase routes, non-builtin pack
- shared reporting lib (single source of truth) now emits the canonical
experiment-runner-widget/data-widget-type wrapper and per-arm
experiment-runner-comparison-arm/data-arm ids, so the report-route HTML the
panel renders is addressable identically to the client-side fallback.
- panel saveDashboardSpec normalises the edited widget list to the canonical
{ widgets: [...] } DashboardSpec so an edited spec survives the report route
and re-renders without a re-run.
- route-name allowlist regex (pack-manifest + pack-contributions) accepts
camelCase so the pack's canonical routes (defineExperiment, getExperiment, …)
register; unit test added.
- experiment-runner dropped from the built-in first-party pack allowlist: it is
a marketplace-installable pack and must be cleanly uninstallable.
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…404c Co-Authored-By: Bobbit (Gpt 5.5) <bobbit@bobbit.ai> # Conflicts: # src/server/agent/pack-contributions.ts # src/server/agent/pack-manifest.ts
Align the panel's experiment definition to the canonical backend contract and
fix two broken seam interactions, then add the missing live-route API E2E.
- routes.defineExperiment: stop falling back to ctx.sessionId for parentGoalId
(a session id is never a goal id, so the spawnGoal seam rejected launch with
PARENT_MISMATCH). Use an explicit goal id or leave it undefined so the seam
derives the parent. engine forwards parentGoalId only when actually set.
- panel.buildDefinition: emit canonical shapes — runnable {kind:'agent'|'command',
spec|command}, metricId (not metric), objective {metricId, direction:'max'|'min'},
caps {maxIterations,maxWallClockMs,maxCostUsd} with perRunBudget top-level, and
stop.target only when finite.
- panel.doLaunch: POST the def object directly as the body (not {definition}).
- panel index handling: listExperiments returns an array of defs and INDEX_KEY is
an array of ids; stop reading/writing {experiments:[...]}.
- routes.cancel: drop the misleading dismiss-by-goal-id no-op; honestly mark runs
cancelled and set state.stopped so the autoresearch loop stops spawning.
- Dashboard metric reads tolerate canonical metricId (legacy metric fallback).
- New tests/e2e/experiment-runner-routes.spec.ts pins the live route path: A/B 2x2
fan-out via spawnGoal with distinct per-arm metadata + array index, and a short
stubbed autoresearch run showing keep-best + plateau stop + ledger growth.
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
parseRawOutcome / createGoalReader now match the real gateway REST shapes so
live child-goal outcomes actually collect:
- gates: read gateId/status (fallback id/verdict); bypassed → passed.
- cost: totalCost→costUsd, inputTokens/outputTokens→tokensIn/Out, keep
cacheHitRate; tolerant legacy fallbacks retained.
- meta: GET /api/goals (the list; no single-goal endpoint) resolved by id;
wall-clock derived from goal timestamps ((archivedAt|updatedAt)−createdAt),
null when not determinable.
- validateDef: autoresearch now REQUIRES a positive perRunBudget
(PER_RUN_BUDGET_REQUIRED).
- panel: callRoute treats an { error } envelope as a failure; doLaunch no longer
mirrors a rejected def to the store or navigates onward.
- panel treatment editor: mutate against the live draft so editing a row's key
then value before a repaint no longer drops the first edit.
Tests: new tests/experiment-engine-outcome.test.ts exercises the real REST
shapes through parseRawOutcome/createGoalReader; extended
experiment-runner-routes.spec.ts drives createGoalReader with a mock fetch
returning the real shapes end-to-end.
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…locked, pre-spawn cost cap, metric directionOverride Fix 5 code-quality defects across both panel front doors + two safety/scheduling contracts: 1. panel doLaunch: branch on mode — autoresearch launches via iterate (the A/B-only launch route returns LAUNCH_AB_ONLY); no double iterate. 2. panel loadDashboard: call collect after poll before report so live A/B settled runs are extracted into metrics (aggregates were empty). 3. experiment-spawn-goal: honour requestChildStart 'capacity-blocked' — stamp child state:'blocked' + broadcast goal_state_changed (mirrors nested-goal-routes.ts), so capacity-queued experiment children are visible. 4. series.evaluateStop/exceedsCaps: accept projectedNextCostUsd; stop with a budget reason when cumulative+projected exceeds maxCostUsd. routes shouldStop passes def.perRunBudget so the cap is enforced PRE-SPAWN. Math stays in the shared lib (no fork). 5. panel buildDefinition: emit canonical directionOverride (max/min) instead of the dropped display 'direction' so user higher/lower-better overrides reach winner selection. Tests: spawn-goal capacity-blocked unit; series projected-budget unit; three route E2Es (autoresearch launch via iterate; live A/B poll→collect→aggregate non-empty metrics; maxCostUsd pre-spawn stop); browser E2E successful autoresearch launch. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…fety issues 1. [critical] engine.mjs: loadCreds now discovers .bobbit/state by walking up from cwd and falls back to the git-common-dir project root (worker cwd is the session worktree; creds live at the project root). createGoalReader's default transport is a TLS-tolerant node:https/node:http GET (rejectUnauthorized:false) so the self-signed gateway cert no longer rejects under an empty worker env. io.fetchImpl / io.creds seams preserved. New unit tests pin parent-dir cred discovery + a real self-signed HTTPS server (no injected fetch). 2. [high] experiment-spawn-goal.ts: add a per-(parentGoalId+runKey) in-flight reservation so concurrent duplicate spawnGoal calls collapse onto ONE child (closes the check-then-create TOCTOU). Concurrent-dup unit test added. 3. [high] routes.validateDef: autoresearch stop requires an integer plateauK>=1 OR a finite target (rejects 0/negative plateauK and NaN target). 4. [medium] routes.validateDef: A/B requires >=2 variants and integer repeats>=1. 5. [medium] routes poll/collect persist exp/<id>/state.status='done' when all runs settle/collect so the panel stops polling a finished experiment. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
- panel doLaunch: skip the exp/<id> store mirror when defineExperiment succeeds, so the server-normalized def's createdAt survives. iterate reads createdAt for elapsedMs, so panel-launched autoresearch no longer loses its wall-clock cap. - metrics.resolveSelection: surface the registered/resolved metric direction as directionOverride so the shared comparison honors a custom (code-registered) min-metric instead of defaulting to max. - launch: enforce per-experiment maxConcurrency batching (spawn pending runs only up to cap minus in-flight; idempotent/re-entrant). Panel A/B loadDashboard re-invokes launch after poll/collect to top up. - series.buildObjectiveSeries + decideCandidate: thread StopSpec.plateauEps so sub-eps improvements count as no improvement; wired through autoresearch.decideRun/buildLedger and routes buildLedger calls. Tests: series eps-boundary + plateau, custom-min-metric winner, E2E maxConcurrency=1 one-at-a-time, E2E createdAt survives + wall-clock cap. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…under budget Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
…te kind to session-menu Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Add experiment-runner to FIRST_PARTY_PACKS so it copies into the shipped builtin-packs tree and is resolved active-by-default by the built-in band, making it visible/usable in the marketplace. Its clean install/uninstall contract is met by the built-in enable/disable toggle model (like pr-walkthrough/hindsight), not by exclusion. Rework the browser E2E to the built-in lifecycle: assert built-in resolution (panel + 15 routes + 3 entrypoints; /installed flagged builtin:true), keep functional coverage (A/B default+validation, autoresearch guardrails + successful launch, dashboard render/edit/metric toggle, deep-link), and replace uninstall with built-in disable/re-enable + non-removable (DELETE /installed -> 403). Update docs to state it ships as a first-party built-in, active by default. Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
Collaborator
Author
Review summary — Experiment Runner extensionShips a generic experiment & research runner as an installable market pack ( Two modes off one engine:
The only core change is the Security / bug review — nothing substantial foundFocused on the trust-sensitive seams:
No malicious intent, no verifiable substantial bug. Minor (non-blocking)
🤖 Generated with Bobbit |
- autoresearch.mjs: fix misleading buildLedger comment (every collected run is pushed; computeBestSoFar filters internally, so baseline only advances for accepted runs) - routes.mjs: make collect the sole owner of the A/B done flip; remove premature markDoneIfFinished from poll (all-settled-pre-collect) - docs/experiment-runner.md: add Security / trust model paragraph on the gateway-token outcome reads and the future scoped host capability Co-authored-by: bobbit-ai <bobbit@bobbit.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Experiment Runner extension
Ships a generic experiment & research runner as an installable Bobbit market pack (
market-packs/experiment-runner/), turning nested goals + the #822 goal-metadata layer + inlineRoles + gates/cost accounting into a data-driven experimentation + autonomous-optimization capability.One engine, two front doors
variant × repeatchild goals (each carrying distinct per-armmetadata+inlineRoles), collect outcomes, aggregate (median + spread, same-completion-bar filtering), and render a comparison. Run-count/cost projected before launch. No self-modification.maxCostUsd(incl. next-iteration projection) + plateau/target/wall-clock stops; correctness-gated; the LLM proposes, the framework decides keep/stop.Minimal core/host change
The only core change is the sanctioned
host.agents.spawnGoal({ title, spec, runKey, parentGoalId?, metadata?, inlineRoles?, workflowId?, workflow? }) → { goalId }seam (src/server/agent/experiment-spawn-goal.ts, wired through the extension host). Parent is server-derived (parentGoalIdis an assertion only →PARENT_MISMATCH);runKeyis idempotent with an in-flight reservation; capacity-blocked children are parkedstate:"blocked"; per-arm treatment propagates across each run's sub-tree via #822. The route-name allowlist was widened to accept camelCase route ids.Extensibility (seams from day one)
src/shared/experiment-report/**(single source of truth for aggregation/series/widgets), bundled to the pack viabuild:packs.Tests
Pack unit (run-config/aggregate/accept-reject/stop/guardrails/extensibility), shared-report unit (+ no-fork single-source guards), API E2E (A/B fan-out via
spawnGoalwith distinct per-arm metadata; stubbed autoresearch keep-best + stop; live REST outcome parsing against a self-signed HTTPS server), and browser E2E (A/B default, autoresearch guardrails + successful launch, dashboard spec edit/re-render, install/uninstall).npm run check/test:unit/test:e2egreen;test:manualrun (flakes only).🤖 Generated with Bobbit