8.7 KiB
Self-Evolution Arc — Steals #2 + #3 (CowAgent teardown)
Date: 2026-07-10 · Branch: feat/steals-2-3 (worktree channels-arc) · Orchestration: Fable plans/verifies, Opus executes.
Source: docs/analysis/cowagent-vs-waggle-2026-07-09.md §2 items 2–3; handoff 0709 S1 steal specs.
Scope
Steal #2 — anti-nag material-change gate + "fix the source, not the symptom" Proactive/self-review outputs only notify when a real artifact changed; "capability X failed" observations route to the improvement-signal path instead of durable memory.
Steal #3 — runtime idle-triggered self-evolution (v1 = review-before-apply only)
60s daemon scans sessions; fires on idle+turns; spawns a restricted reviewer through loopback /api/chat; reviewer proposes skill patches / finishes promised-but-undelivered deliverables via the held-action approval queue; skill writes gain backup+undo. No autonomous disk writes in v1 — every apply goes through ApprovalsApp (founder-ratified trust boundary from 0709 handoff).
Verified primitives (recon 2026-07-10, 3-agent Opus sweep)
- Notification chokepoint:
emitNotificationpackages/server/src/local/routes/notifications.ts:50(persist→SSE); cron fan-outpackages/agent/src/cron-delivery-router.ts:93. No dedupe/material check anywhere today. Nag sources: proactive handlers (proactive-handlers.ts:113/186/221/245),prompt_optimization+monthly_assessmentcron cases (index.ts:1805/2055) emit unconditionally. - Memory-lint seam:
tool-executor.ts:182-197firespre:memory-writewith cancel support; handler registeredroutes/chat.ts:227(currently warn-only). Fix-or-flag sink exists:recordCapabilityGap(improvement-detector.ts:58) →ImprovementSignalStore(threshold capability_gap:2) → awareness summary →acquire_capability. Cognify writes bypass this hook (accepted v1 gap). - Sessions:
dataDir/workspaces/<ws>/sessions/<id>.jsonl; last-activity = mtime; turns =readSessionMeta().messageCount(NOTE: has lazy-backfill side effect — watcher uses raw stat+line count). Scheduler pattern to mirror:LocalScheduler(cron.ts:68, 60s setInterval, single-flighttickingguard). Wire afterscheduler.start()(index.ts:2183), guardprocess.env.VITESTlike ChannelManager (:2391). - Loopback agent turn:
runChannelChatTurn(channels/chat-client.ts:61) — POST127.0.0.1:<port>/api/chat{message, workspace, session, persona, autonomy}, inherits injection scan/persona/governance/memory. Reviewer uses dedicatedevolve-<sessionId>session id, never the user's. - Restriction:
applyPersonaToolFilter(persona-tool-filter.ts:82) allowlist→denylist→readonly-strip;ALWAYS_AVAILABLE_TOOLSre-adds save_memory etc — must be explicitly disallowed. Spawn intersection cannot escape persona pool. - Review-before-apply EXISTS: held-action queue (
held-action-executor.ts—isProposableTool:35,enqueueHeldAction:64,executeHeldAction:105with execute-time re-validation) +ApprovalsApp.tsx+routes/approval.ts. - Skill writes: single seam
skill-write-service.ts:105 writeSkill(redact/provenance/audit) — no .bak today;deleteSkill:159no trash. Skill change detection:SkillHashStore.checkAll(core/skill-hashes.ts:39). Skill audit machinery (synth-test/judge/rewrite/demote, all fail-safe) already inskill-audit.ts— reuse, don't rebuild. - Deterministic no-invention summary reference:
dream-journal.ts:72 composeSummary.
Phases
Wave 1 (parallel, disjoint files)
P-A. Notification material-change gate — packages/server/src/local/notification-gate.ts (new)
materialFingerprint(parts: unknown): string— sha256 over canonical JSON.NotificationGate— persistent last-hash perdedupeKey(JSON storedataDir/notification-fingerprints.json, immutable update, corrupt-file tolerant).shouldNotify(key, hash)→ boolean + record.emitNotificationgains optional{dedupeKey, materialHash}— unchanged hash ⇒ suppress (no row, no SSE), return{suppressed: true}. Callers without keys behave exactly as today.- Wire fingerprints into nag sources: morning briefing / stale workspaces / pending tasks / capability suggestions (fingerprint = ids+counts+relevant mtimes),
prompt_optimization(correction-rate bucket + period),monthly_assessment(period + counters). Suppression logged at debug level. - Tests: gate unit (new key fires; same hash suppresses; changed hash fires; corrupt store recovers), emitNotification opt-in behavior, one proactive-source integration test.
P-B. Memory-write lint — packages/agent/src/memory-write-lint.ts (new)
- Deterministic classifier (regex/heuristic, no LLM):
lintMemoryWrite(content, type) → {verdict: 'allow' | 'capability_symptom', capability?: string, reason?: string}. Symptom shapes: tool/skill/connector/capability + failure verbs ("failed", "doesn't work", "unavailable", "errors when", "cannot", "broken"). Conservative: ambiguous ⇒ allow (memory loss is worse than one nag). - Extend
chat.ts:227handler: oncapability_symptom⇒cancelled: true+recordCapabilityGap(...)into ImprovementSignalStore + cancel message instructing agent to fix or flag (acquire_capability), not memorize. - Tests: classifier table-driven (symptoms caught, real facts pass, edge: user preference about a tool passes), hook integration (cancel + signal recorded).
P-C. Skill backup + undo — packages/agent/src/skill-write-service.ts
- Before overwrite in
writeSkill: stash old bytes to<skillsDir>/.backups/<name>-<ISO-ts>.md; keep newest 5 per skill.deleteSkill: same stash (trash instead of hard loss). - New
undoSkillWrite(name)— restore newest backup via the same sanctioned write path (provenance-stampedrestored-from-backup, audited), returns restored ts. .backups/excluded fromloadSkills/hygiene scans (verify loaders ignore subdirs; fix if not).- Tests: backup created on overwrite, cap enforced, undo restores exact bytes, delete stashes, loaders ignore
.backups.
Wave 2 (single lane, depends on Wave 1)
P-D. IdleSessionWatcher + restricted reviewer + proposal path
packages/server/src/local/idle-watcher.ts(new):start(intervalMs=60_000)/stop()/tick()single-flight; enumerate sessions (pure stat+line-count, skipchannel-*andevolve-*prefixed, skip turnCount<minTurns); fire whennow-mtime ≥ idleMsANDturnCount ≥ minTurns; RAM fired-set keyedsessionId:mtimeMsso a session refires only after advancing.- Config
dataDir/self-evolution.json:{enabled: false, idleMinutes: 15, minTurns: 6, maxReviewsPerDay: 5}— default OFF (founder opt-in), corrupt/absent ⇒ defaults. Daily cap enforced. - Reviewer persona
session-reviewerinpersona-data.ts: tools = read-only set +read_skill+propose_*-capable writes routed to held queue;disallowedToolsexplicitly:save_memory,delete_skill,install_capability, bash/exec, connectors. System prompt: examine transcript for (1) promised-but-undelivered deliverables, (2) recurring capability failures fixable by a skill patch; default SILENT — outputNOTHING_TO_DOunless a material, actionable finding exists; never invent. - Fire:
runChannelChatTurn-style loopback withsession: evolve-<sessionId>,persona: session-reviewer,autonomydefault (approval-gated). Proposals: extendisProposableToolto acceptcreate_skill; held actions land in ApprovalsApp;executeHeldActionexecutes throughwriteSkill(now backup-protected). - Notify on proposal via gated
emitNotification(dedupeKey: self-evolution:<sessionId>, hash of proposal set) — silent when reviewer found nothing. - Wire in
index.tsafter scheduler start;onClosestop; VITEST guard. - Tests: watcher fire-condition matrix, fired-set no-refire, config default-off, daily cap, enumerate skips channel/evolve sessions, isProposableTool create_skill, end-to-end route test with mocked loopback (reviewer proposes → held action exists → approve → skill written with backup).
Wave 3 — adversarial verifier (Fable-side gate)
Read-only Opus verifier: spec-vs-implementation audit + security review (trust boundary: no autonomous writes when disabled or unapproved; loopback confinement; ALWAYS_AVAILABLE leak check; path traversal in backups; fingerprint store injection). VERDICT format. Bounce loop until APPROVED.
Gates per wave
npx tsc --noEmit on packages/agent + packages/server (+ apps/web if touched) · targeted vitest for new/changed files · Wave 3: full server + agent suites. Commit per wave (conventional commits).
Residuals (declared, not in v1)
Auto-apply mode behind additional founder opt-in · cognify-path lint coverage · Settings UI toggle for self-evolution · reviewer completing deliverables beyond skill proposals (drafting files into workspace) — v1 proposals only.