Skip to content

Epic: Repo setup evolution — detect setup drift + propose remedial manifest updates#1792

Open
psdjungpulzze wants to merge 15 commits into
mainfrom
goal/epic-repo-setup-evolution-detect-setup-drift-propose-remedia
Open

Epic: Repo setup evolution — detect setup drift + propose remedial manifest updates#1792
psdjungpulzze wants to merge 15 commits into
mainfrom
goal/epic-repo-setup-evolution-detect-setup-drift-propose-remedia

Conversation

@psdjungpulzze

Copy link
Copy Markdown
Contributor

No description provided.

pulzzejason and others added 15 commits July 3, 2026 19:31
…ic 583e469f S1)

Foundation for the repo-setup evolution flow (docs/design/repo-setup-evolution.md
§5+§6) — the pure, unit-tested server "brain" later tasks wire up. No DB, no HTTP,
no LLM call; no writes to any customer repo.

- detector-registry.ts: data-driven, deterministic map of repo signals
  (mix.exs/package.json deps, config env refs) -> required services (redis/postgres/
  mysql with default probes), env-required + implied service, and nix.packages for
  native-lib deps. `detectRequirements(signals)` is pure + order-independent; unknown
  deps fall through (no requirement) to LLM inference.
- manifest-inference.ts: MANIFEST_DELTA_INFERENCE_PROMPT + buildManifestDeltaPrompt +
  draftManifestDelta — the delta sibling of the full-draft flow (minimal update; only
  changed sections). A delta that doesn't parse or adds no runnable change is discarded.
- manifest-delta.ts: mergeManifestDelta (deterministic section-replace, §5),
  buildDeterministicDelta (short-circuit a known-service gap without the LLM),
  buildProposedManifestSummary + defaultProposalReason (display diff — added
  services/steps/env + reason + source), mirroring manifestToReviewSummary's
  vocabulary-clean style.

63 unit tests cover each detector mapping + fall-through, the merge/short-circuit/
diff-summary paths, and the delta prompt/parser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al store (epic 583e469f S1)

Wires T1's detector registry + delta core into the server: a detected requirement
gap now raises a REVIEWABLE manifest-update proposal on RepoAgentProfile, without
touching the operative setup (design §5/§6/§9).

- schema: four additive, nullable RepoAgentProfile columns — proposedManifest (candidate
  body; operative keeps serving until adopted), proposedManifestSummary (computed diff +
  reason + source), proposedManifestAt, proposedManifestDismissedAt. Migration is
  additive/IF NOT EXISTS — applies cleanly, existing rows load with no backfill.
- repo-readiness: ReadinessReportSchema gains OPTIONAL, bounded detectedRequirements
  (discriminated service|env|nix wire shape, structurally === T1's DetectedRequirement).
  Additive/back-compat — reports without it still validate. Inferred type exported.
- manifest-proposal.ts: proposeManifestUpdate({connectionId, detectedRequirements}) loads
  the operative manifest (accepted-draft rule mirrors the report route), diffs via T1's
  deterministic short-circuit (buildDeterministicDelta), section-replace merges the
  candidate (mergeManifestDelta), builds the display summary, and stores it profile-level.
  No gap / empty delta ⇒ nothing stored. A newer detection supersedes an un-acted proposal;
  a fresh proposal clears the prior dismissal. Operative manifest untouched, NO EngineGoal.
- assess/report route: calls proposeManifestUpdate after the profile upsert when the report
  carries detectedRequirements — best-effort, so a proposal error never fails a committed
  readiness report.

Store unit tests: gap→stored, no-gap→nothing, no-requirements→nothing (+ no profile read),
supersede overwrites + clears dismissal, operative untouched, no goal. Route tests: a
report with detectedRequirements raises a proposal (server-side only, not persisted on the
profile); absent ⇒ none; a proposal failure still 200s.

No agent-config update needed: this is server-internal setup evolution — no new PM-agent
tool or agent-visible domain entity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e469f S1)

Adopt or drop a stored manifest-update proposal from the connection-scoped setup
dispatcher — the operator's "Use this update" / "Dismiss" on the driver card
(design §9 DECIDED / §10). Both are profile-level (NO EngineGoal) and never write
to the customer repo (the "commit back" PR is S1b, out of scope).

- manifest-proposal.ts: applyProposedManifestUpdate({repo,userId}) resolves the
  connection (resolveRepoProposalContext), requires a pending proposedManifest
  (else no_pending), and in ONE update makes the candidate the operative MANAGED
  manifest for EVERY manifestSource — manifest ← proposedManifest,
  manifestSource='manual' (committed is SUPERSEDED, never edited), proposed fields
  cleared — then runs accept-then-verify EXACTLY as acceptManifestDraft: credit
  testToolingDetected iff a runnable test step, stamp assessRequestedAt, set
  preflightStatus pending/blocked_no_compute by hasIdleCompute over the user's
  daemon machines, clear assessClaimedAt on the compute path. No goal status touched.
- dismissProposedManifestUpdate stamps proposedManifestDismissedAt + clears the
  candidate; operative manifest/source untouched.
- test-mechanism-actions.ts: wire 'apply-update' / 'dismiss-update' into
  TEST_MECHANISM_ACTIONS + dispatchTestMechanismAction (reusing errorFor), flowing
  through the existing POST /connected-repos/[connectionId]/test-mechanism route
  with no route change.

Tests: apply for each source (generated/manual/none/committed → operative managed +
verify requested; committed→manual; no goal); no-test-step, blocked_no_compute,
no_pending, forbidden gate; dismiss clears candidate + stamps dismissedAt. Route
test for both new actions incl. 409 no_pending.

No agent-config update needed: server-internal setup evolution, no new PM-agent
tool or agent-visible domain entity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elta card + manual check-for-updates (epic 583e469f S1)

Closes the operator-facing end-to-end path for the setup-evolution flow: a
detected requirement gap (raised as a profile-level proposal by T2/T3) now
surfaces on the Fleet-setup driver as a reviewable DELTA the operator adopts or
dismisses, plus a manual "check for updates" affordance that exercises the whole
detect→diff→propose→review path (design §3, §10, §11, §12 S1).

- driver (repo-readiness-driver.ts): new 'A setup update is proposed' state.
  ReadinessDriverSignals gains `updateProposed`; a new `apply-update`
  ReadinessActionKind + "Use this update" label drive the inline card (Dismiss is
  the secondary, rendered by the panel). Precedence surfaces the proposal
  prominently (right after the two in-flight checks) BUT never flips `ready` — a
  proposal is a candidate; the operative setup keeps serving. The pending-assess
  check still wins, so the accept-then-verify window after "Use this update"
  truthfully shows "Checking…". Vocabulary-clean (no manifest/assess/…).
- setup-status route: loads proposedManifest*/DismissedAt in the profile select,
  derives updateProposed (non-dismissed pending proposal) into the signals, and
  includes a `proposal` block (stored diff summary: added services/steps/env +
  reason, plus the full proposed YAML for "View details").
- panel (readiness-panel.tsx): renders the review-then-use DELTA card off
  `apply-update` — Why: <reason>, the "+ Services/Environment/<phase>" delta,
  primary "Use this update" → POST apply-update, secondary "Dismiss" → POST
  dismiss-update, and a "View details" disclosure with the full proposed body
  (parity with the machines-page review). Reuses the inline-review card styling;
  "Use this update" flows through the existing poll-until-settled loop
  (accept-then-verify). Adds a manual "Check for updates" control that POSTs a
  fresh assess/detect (the sole S1 trigger — no automatic detection yet).
- tests: driver new state + precedence (proposal alongside ready/failed base
  states; in-flight beats proposal; ready not flipped); setup-status route
  (proposal block + updateProposed + dismissed-guard); panel (delta card, View
  details, apply/dismiss/check-for-updates wiring).

No PM-agent config update required: this is operator-facing setup infrastructure
(RepoAgentProfile-level, connection-scoped), not an end-user PM entity/tool or a
new agent-visible workflow — nothing in src/server/agents/configs surfaces it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-s1-proposal-surface-incremental-inferen

Repo setup evolution S1 — proposal surface + incremental inference + detector registry
…3e469f S3)

S3 (§7B/§12): a pure server-side hook in acceptGoal — when a prBoundary goal
merges and its PR touched setup-relevant files, request a fresh detect+assess for
the affected connection so S1's report handler diffs detectedRequirements vs the
operative manifest and raises a proposal. S3 only REQUESTS the refreshed assess;
it never computes the diff or raises the proposal (S1's contract).

Reusable seams (same PR):
- Extract fetchPrFiles(ref, prNumber) into src/lib/engine/pr-files.ts; goal-
  supersession.ts now reuses it.
- Extract the assess-request DB logic (writeCapable probe + hasIdleCompute + upsert
  /clear-lease) into requestConnectionAssess(connectionId, {requestedByUserId}); the
  assess route calls it too (no HTTP behavior change).
- resolveConnectionGitHub(connectionId) — per-connection GitHub read-token resolver
  (integration PAT -> connectedByUser fallback), so fetchPrFiles uses the correct
  repo's owner/repo+token per target.
- isSetupRelevantPath / anySetupRelevantPath — pure, data-defined matcher over
  mix.exs/mix.lock/config/*.exs/package.json/lockfiles/*.nix/Dockerfile. Extensible.

Trigger (maybeRequestSetupDriftAssess): iterates the goal's repo-targets keyed by
connectionId via listGoalRepos (N-repo-ready; falls back to the singular goal.prNumber
+ primary connection until the multi-repo EngineGoalRepo junction lands in S5); for each
merged target it debounces per connection (coalesces when pending or requested within a
short window), fetches PR files, runs the matcher, and requests the assess. Never throws;
logs and swallows all errors. Wired into acceptGoal as a best-effort void call after the
post-merge publishEngineChanged on both prBoundary success paths (main merge + already-
merged reconcile), never affecting acceptGoal's return/throw.

Tests: path matcher (positive/negative/glob), multi-repo iteration, singular fallback,
debounce coalescing, and no-op/safety cases (non-prBoundary, no setup-relevant change,
no token, never-throws).

No PM-agent config change: this is a backend engine trigger with no new user-facing
tool/entity/workflow, so no TOOL_NAMES or specialist-prompt update is required (per
CLAUDE.md's agent-maintenance rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(epic 583e469f S2a)

Server-side S2a: the wire contract + failure-classification reuse for the daemon's
structured setup-failure signal.

- Extend ReadinessReportSchema (src/lib/agent/repo-readiness.ts) with an OPTIONAL,
  bounded, STRUCTURED `setupFailure` discriminated union over kinds: service
  {host,port}, tool {bin}, env {name}. Unknown kinds rejected, strings capped, port
  range-bounded — a report omitting it validates exactly as today. Rides the
  connection-scoped assess/report flow (co-located with S1's detectedRequirements).
  Add the SetupFailure type export.
- Extract a shared classifySetupFailure helper co-located with classifyReportedFailure
  (src/lib/engine/pause.ts) that maps the three high-precision signatures (connection
  refused:<host:port> → service; command not found:<bin> → tool; missing env <NAME> →
  env) into the structured signal + a legible reason. A setup failure classifies as the
  existing retryable `infra` class, NOT a new RunFailureClass. High-precision only — no
  fuzzy substring inference.
- On the failed-run escalation path (runs/[runId]/report/route.ts), derive the signal
  from body.error and stamp a bounded setupFailure + setupFailureReason marker on
  EnginePause.detail (mirrors the attachmentCredsFailure precedent) so /attention
  renders a human cause — no raw logs persisted.

Tests: schema accepts each kind / rejects unknown-kind + oversized / validates when
omitted; the shared classifier + reason helper; the EnginePause.detail marker.

No agent-config update needed: this is an internal daemon↔server wire contract +
failure-classification seam — no user-facing tool or agent-visible entity changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3e469f S2b)

Bind the daemon's structured setupFailure signal (S2a) to S1's proposal surface:
a failed run's setup signature becomes a reviewable, failure-sourced manifest-update
proposal on RepoAgentProfile — no LLM, no touch to the operative setup (design §6/§9,
spec §13.3/§13.4).

- detector-registry: add knownServiceForPort(port) — the reverse of SERVICE_PROBE
  (6379→redis, 5432→postgres, 3306→mysql) with the registry's canonical probe, derived
  from SERVICE_PROBE so the port↔service mapping has one source of truth. Powers the
  deterministic service short-circuit; an unknown port names nothing → server no-op.
- setup-failure-proposal.ts: setupFailureRequirements maps the signal into a
  DetectedRequirement in the SAME shape the registry mints — service {host,port} →
  services: delta via the known-service short-circuit; tool {bin} → nix.packages delta;
  env {name} → env-required delta; an unnameable service port → [] (falls through to the
  daemon-side inference, so a server no-op). setupFailureProposalReason gives a
  plain-language cause ("A run failed connecting to redis."). proposeSetupUpdateFromFailure
  hands the requirements to proposeManifestUpdate with source: 'failure' + the reason,
  reusing all of S1's parseManifest/hasRunnableSteps discard discipline.
- manifest-proposal: debounce (spec §13.4) — one proposedManifest per connection; when the
  identical candidate is already the pending proposal, skip the write (new 'unchanged' skip
  reason) so a repeated identical signal doesn't churn proposedManifestAt or re-notify. A
  different candidate still supersedes; apply/dismiss null the candidate so a recurrence
  re-raises.
- assess/report route: after the profile upsert, when the report carries setupFailure, call
  proposeSetupUpdateFromFailure — best-effort + side-channel, mirroring detectedRequirements
  (a proposal error never fails a committed readiness report). Behind the field: with no
  daemon emitting setupFailure the branch never runs — a pure no-op; run-report unchanged.

Tests: signal→delta per kind incl. the known-port service short-circuit + unknown-port
no-op, source/reason stamping, debounce + supersede, and the no-setupFailure no-op /
schema-rejected kind at the route. Full suite + typecheck green.

No agent-config update needed: internal daemon↔server setup-evolution seam — no PM-agent
tool or agent-visible entity changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ION note (epic 583e469f)

Docs / ship-tracking only — no daemon code ships from this repo (thin-daemon /
fat-server; the daemon source is out-of-repo). Captures the GATED, hand-driven
fleet rollout that activates the S2 failure-driven trigger end to end.

- docs/design/repo-setup-evolution-s2-daemon-ship-runbook.md: the runbook —
  (1) the exact setupFailure wire contract the daemon must emit (the S2a tagged
  union: service {host,port} / tool {bin} / env {name}), high-precision only,
  structured signal never raw logs, with the three failure-tail signatures and
  the two transports (structured proposal via assess/report vs server-derived
  /attention marker via run-report); (2) daemon image change + smoke against a
  controlled failing run per signature, incl. a negative case; (3) set
  PM_MIN_DAEMON_VERSION to the new floor AFTER rolling the image (deploy-time env
  var read by daemon-version.ts, NOT a code constant); (4) note the server side
  (S2a+S2b) is already live behind the optional field, so the daemon rollout is
  independent and the last step to activate the trigger. Plus rollback.
- .env.example: refresh the PM_MIN_DAEMON_VERSION note to point at the runbook and
  the roll-image-then-raise-floor ordering.

No pm-agent config / TOOL_NAMES change: S2 adds no agent-visible tool or entity —
setupFailure is an internal daemon<->server wire field and the proposal reuses the
S1 RepoAgentProfile surface. Docs-only slice; nothing agent-visible changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…9f S2c)

Review rework: the runbook existed but was not discoverable from the design
doc. Add a link from §7A (Failure-driven trigger, daemon-dependency bullet) and
§12's S2 slice bullet to docs/design/repo-setup-evolution-s2-daemon-ship-runbook.md
so a reader on the design doc can find the gated hand-driven rollout runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-s3-goal-delivery-diff-trigger-acceptgoa

Repo setup evolution S3 — goal-delivery diff trigger (acceptGoal hook)
…-s2-failure-driven-trigger-daemon-setupf

Repo setup evolution S2 — failure-driven trigger (daemon setupFailure signal)
…epic 583e469f S4)

Reach half of S4: make a pending manifest-update proposal
(RepoAgentProfile.proposedManifest*, raised by S1) reach the operator through the
project /attention feed, deep-linking the Fleet-setup driver.

- attention.ts: new `AttentionSetupProposedItem` union member (link-only, like
  goal_ready); shared `SETUP_PROPOSED_SELECT` + `listSetupProposals` single source
  (proposedManifest set, un-dismissed, connection.projectId == project);
  `coerceProposedSummary` for the stored JSON; owner ladder gains `connectionOwnerId`
  (= connection.connectedByUserId) so the badge bumps only for the connection owner.
- Materialized in listAttentionItems (project-scoped, skipped for a taskId) and in
  every parallel surface — v2 pauses route, SSR seed (load-attention), and
  countMyAttentionItems — so the CLI list, web page, and badge can't disagree.
- Web: SetupProposedCard renders "A setup update is proposed", the reason + added
  services, deep-linking to /settings/integrations anchored to the connection; wired
  through attention-client + approvals-client.
- Digest helpers (ATTENTION_LABELS / title / displayNumber / href) handle the new type.
- Tests: materializing query (pending shows / absent nothing / dismissed+adopted hidden
  at the source), owner resolution + badge parity, summary coercion, web card render+href.

No agent-config change: this only READS existing profile data into an existing feed —
no new tool, entity, or agent-invoked workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…83e469f S4)

Raise a per-user notification-bell alert to a connection's OWNER when a
manifest-update proposal is written — "<repo>'s setup needs an update: add
redis" — instead of relying only on the shared profile-level proposed-manifest
card. Mirrors the owner-only offline-fleet signal in engine/starvation.ts.

- Add `SETUP_UPDATE_PROPOSED_NOTIFICATION_TYPE = "setup_update_proposed"`,
  deliberately kept OUT of TYPE_TO_PREF_KEY so it is never preference-suppressed
  (exactly as fleet_offline_starvation).
- Hook the single trigger-agnostic site: proposeManifestUpdate's proposed-write
  branch, so both S2 (failure) and S3 (dep-change) triggers converge for free.
  Owner = IntegrationConnection.connectedByUserId (null → skip; no goal for a
  profile-level proposal). Message built from proposedManifestSummary.added +
  the repo label; actionUrl = integration settings; resourceId = connectionId.
- Collapse N→1 PER CONNECTION (dedup on userId + type + resourceId): starvation-
  style findFirst → refresh-in-place if unread (no re-push), re-open if read
  (clear readAt, bump createdAt, re-publish), else create. Written directly via
  db.notification for collapse + suppression-bypass + manual realtime publish.
- Resolve-on-adopt/dismiss: conditional updateMany(readAt:null) + publish
  notification.read so the bell clears without a push and a concurrent manual
  read wins.

Tests: create / collapse-in-place / re-open / resolve-on-adopt / resolve-on-
dismiss, owner = connectedByUserId, and absence from TYPE_TO_PREF_KEY.

No agent-config update needed: no src/server/agents/configs/* prompt enumerates
notification types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-s4-reach-attention-item-owner-notificat

Repo setup evolution S4 — reach (attention item + owner notification)
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.

2 participants