Skip to content

Experiment runner extension - #835

Open
ajonkisz wants to merge 71 commits into
masterfrom
goal/experiment-run-bf9e404c
Open

Experiment runner extension#835
ajonkisz wants to merge 71 commits into
masterfrom
goal/experiment-run-bf9e404c

Conversation

@ajonkisz

Copy link
Copy Markdown
Collaborator

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

  • A/B comparison (default, bounded): fan out variant × repeat child goals (each carrying distinct per-arm metadata + inlineRoles), collect outcomes, aggregate (median + spread, same-completion-bar filtering), and render a comparison. Run-count/cost projected before launch. No self-modification.
  • Autoresearch (opt-in, OFF by default, hard-capped): a deterministic generate → evaluate-under-fixed-budget → keep-iff-improves-AND-verifies → iterate-until-plateau/budget/target loop with a fed-forward ledger. Refuses to start uncapped; enforces per-run budget + 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 (parentGoalId is an assertion only → PARENT_MISMATCH); runKey is idempotent with an in-flight reservation; capacity-blocked children are parked state:"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)

  • Shared reporting library src/shared/experiment-report/** (single source of truth for aggregation/series/widgets), bundled to the pack via build:packs.
  • Metric-extractor registry (built-ins + user-metric channel + generic command metric) and widget-renderer registry + editable dashboard view-spec — add a metric/widget by registration, not refactor. Per-experiment metric selection + dashboard re-render from stored raw outcomes without re-running.

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 spawnGoal with 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:e2e green; test:manual run (flakes only).

🤖 Generated with Bobbit

ajonkisz and others added 30 commits June 19, 2026 19:58
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>
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>
…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>
ajonkisz and others added 28 commits June 20, 2026 13:28
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>
@ajonkisz

Copy link
Copy Markdown
Collaborator Author

Review summary — Experiment Runner extension

Ships a generic experiment & research runner as an installable market pack (market-packs/experiment-runner/), built on nested goals + the #822 goal-metadata layer + inlineRoles + gates/cost accounting. 51 files, +11.9k/-13.

Two modes off one engine:

  • A/B comparison (default): fan out variant × repeat child goals, each with per-arm metadata/inlineRoles, collect outcomes, aggregate (median + spread, same-completion-bar filtering), render a comparison. Cost projected pre-launch.
  • Autoresearch (opt-in, OFF by default): deterministic generate → evaluate-under-fixed-budget → keep-iff-improves-and-verifies → iterate-until-plateau/budget/target loop. Refuses to start uncapped.

The only core change is the host.agents.spawnGoal seam (experiment-spawn-goal.ts), wired as a 7th agents verb through the extension host, plus a shared reporting lib (src/shared/experiment-report/**) and a boot seed that ships the pack disabled-by-default.

Security / bug review — nothing substantial found

Focused on the trust-sensitive seams:

  • spawnGoal parent derivation — authoritative server-side (owner.goalId ?? teamGoalId); caller parentGoalId is an assertion only → PARENT_MISMATCH. No caller-supplied cwd/project/sandbox (all inherited). Child sessions blocked from spawning (assertCanSpawn). Idempotency via in-flight reservation + sibling scan collapses the TOCTOU race. Subgoal/nesting policy enforced. Solid.
  • Route-name regex widening (/^[A-Za-z0-9][A-Za-z0-9_-]*$/) — only adds uppercase; still anchored, no .//, so no traversal. Safe.
  • Cost caps — autoresearch validateDef rejects uncapped runs, requires a positive per-run budget + an effective stop condition; shouldStop enforces maxCostUsd pre-spawn (cumulative + projected next). Per-run budget enforced in framework space (applyBudget). Solid.
  • execFileSync — only git rev-parse --git-common-dir, no shell, no user input. The runnable.command is embedded into goal spec markdown, never executed by the pack. Safe.
  • rejectUnauthorized:false — the sanctioned pattern for the self-signed local gateway cert (matches watchdog.ts), localhost only.
  • HTML rendering — report HTML is server-generated through the shared renderer; every user-controlled value passes through escapeHtml. The panel's innerHTML consumes only that escaped, trusted output.
  • Boot seed — ships the pack disabled, idempotent via a durable marker, respects existing user/admin activation, never throws.

No malicious intent, no verifiable substantial bug.

Minor (non-blocking)

  • buildLedger comment says "only accepted runs advance the best-so-far baseline" but pushes every run to prior; behaviour is correct because computeBestSoFar filters internally — the comment is just slightly misleading.
  • markDoneIfFinished flips A/B state to done on all-settled (pre-collect); it's display-only and doesn't block the collect route, so harmless.
  • The pack's server routes read the gateway bearer token from .bobbit/state/token to call the REST API as the user (documented design choice for reading goal outcomes) — flagging for reviewer awareness, since an installed pack's routes thereby gain full authenticated gateway API access.

🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant