ai-whisper 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1031,6 +1031,7 @@ function rowToRecord(row) {
1031
1031
  clipSample: row.clip_sample,
1032
1032
  turnSample: row.turn_sample,
1033
1033
  abortedByRaceGuard: row.aborted_by_race_guard === 1,
1034
+ interferenceDetected: row.interference_detected === 1,
1034
1035
  createdAt: row.created_at
1035
1036
  };
1036
1037
  }
@@ -1038,8 +1039,9 @@ function insertCaptureDiagnostic(db, input) {
1038
1039
  db.prepare(`INSERT INTO relay_capture_diagnostics
1039
1040
  (capture_id, handoff_id, collab_id, chain_id, workflow_id, target_provider,
1040
1041
  capture_status, clip_len, turn_len, turn_confidence, jaccard_score,
1041
- containment_score, clip_sample, turn_sample, aborted_by_race_guard, created_at)
1042
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.captureId, input.handoffId, input.collabId, input.chainId, input.workflowId, input.targetProvider, input.captureStatus, input.clipLen, input.turnLen, input.turnConfidence, input.jaccardScore, input.containmentScore, input.clipSample, input.turnSample, input.abortedByRaceGuard ? 1 : 0, input.createdAt);
1042
+ containment_score, clip_sample, turn_sample, aborted_by_race_guard,
1043
+ interference_detected, created_at)
1044
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.captureId, input.handoffId, input.collabId, input.chainId, input.workflowId, input.targetProvider, input.captureStatus, input.clipLen, input.turnLen, input.turnConfidence, input.jaccardScore, input.containmentScore, input.clipSample, input.turnSample, input.abortedByRaceGuard ? 1 : 0, input.interferenceDetected ? 1 : 0, input.createdAt);
1043
1045
  }
1044
1046
  function listCaptureDiagnosticsByCollab(db, collabId2, limit, opts) {
1045
1047
  const filter = opts?.workflowFilter;
@@ -2025,6 +2027,36 @@ Findings: (omit this block entirely if none)
2025
2027
  Non-blocking risks:
2026
2028
  - <quality risk that does NOT block this gate, or "None.">
2027
2029
  --- end protocol ---`;
2030
+ var WORKFLOW_DIAGNOSIS_PROTOCOL = `--- ai-whisper diagnosis review protocol ---
2031
+ You are the gatekeeper for an ai-whisper complex-bug-fixing DIAGNOSIS gate. No human is in the loop. Do NOT trust the implementer's diagnosis \u2014 verify it yourself. The gate stays shut until YOU independently agree on the root cause and that the fix is net-safe.
2032
+
2033
+ Required procedure:
2034
+ 1. Independently reproduce: re-run the implementer's reproduction yourself; do not trust pasted output. If YOU cannot reproduce it, that is a blocking finding (the repro is not real/reliable). Speculation from reading code paths is not a valid reproduction.
2035
+ 2. Attack the causal claim: is the cause PROVEN by the evidence chain, or merely asserted? Could the named cause be a correlate, or a symptom of something deeper? Demand any missing link.
2036
+ 3. Attack the fix (anti-whack-a-mole): does the proposed fix remove the root cause, or just this symptom's surface? Could the bug resurface through another path? Name it if so.
2037
+ 4. Attack the blast radius: is every affected area/module/contract listed? Add what is missing.
2038
+ 5. Attack residual risks: are the real foreseeable risks named, or hand-waved?
2039
+ 6. Mutual-agreement gate: approve ONLY when you have independently confirmed the reproduction and agree the cause is proven and the fix is net-safe (fixes more than it risks). "Plausible" is not approval.
2040
+
2041
+ Severity: a blocking finding must tie to a concrete diagnosis-contract item (unreproducible repro, unproven cause, symptom-masking fix, incomplete blast radius, un-named risk). Suppress style/taste. Do NOT gag valuable non-contract risk signals \u2014 surface them under "Non-blocking risks:" (always last) so they reach the human at escalation/completion.
2042
+
2043
+ Findings vs escalation: a fixable defect in the diagnosis is a FINDING (loops back for revision) \u2014 do NOT say you "cannot proceed" for a defect you can describe. ESCALATE only when you genuinely cannot review (a required reproduction input is absent and is not the implementer's to supply).
2044
+
2045
+ Never reply with only a bare verdict; your full reply must be well over 100 characters.
2046
+
2047
+ Output format \u2014 the verdict line MUST come before the Non-blocking risks section, which is always LAST:
2048
+ Diagnosis review matrix:
2049
+ | Claim | Required evidence | Implementer evidence | Independently verified? | Result |
2050
+ | ... |
2051
+
2052
+ Findings: (omit this block entirely if none)
2053
+ - <blocking finding tied to an exact diagnosis-contract item, with evidence>
2054
+
2055
+ <verdict line: "Approved. <one or two sentences>" OR, when blocked/cannot-proceed, state you cannot proceed and why>
2056
+
2057
+ Non-blocking risks:
2058
+ - <risk that does NOT block this gate, or "None.">
2059
+ --- end protocol ---`;
2028
2060
  var SDD_SPEC_REVIEW = "Review the spec at {specPath}. This is an autonomous workflow with no human in the loop.\n\n" + WORKFLOW_REVIEW_PROTOCOL;
2029
2061
  var SDD_CODE_REVIEW = "Review the implementer's changes for this phase \u2014 the commits in {commitRange}. The upper bound is a LIVE `HEAD`: resolve it against the current repository at review time and INCLUDE any commits added during this review round (e.g. fixes for your prior findings); do not pin the review to an earlier tip. Verify against the spec's acceptance criteria and run the project's verification/tests. This is an autonomous workflow with no human in the loop.\n\n" + WORKFLOW_REVIEW_PROTOCOL;
2030
2062
  var SPEC_DRIVEN_DEVELOPMENT = {
@@ -2145,13 +2177,113 @@ var RALPH_LOOP = {
2145
2177
  }
2146
2178
  ]
2147
2179
  };
2180
+ var BUGFIX_DIAGNOSIS_KICKOFF = `You are the implementer in an autonomous complex-bug-fixing workflow. No human is in the loop \u2014 never ask for confirmation, permission, or clarification; do the work yourself.
2181
+
2182
+ Read the bug report at {specPath}. A human (dev or QA) already observed this bug, so you must reproduce it too \u2014 do NOT theorize from reading code paths.
2183
+
2184
+ Write a diagnosis artifact to {diagnosisPath} (create the file) with these sections:
2185
+ 1. Reproduction \u2014 an ACTUALLY OBSERVED reproduction you ran yourself. Strongly prefer a failing test (RED) failing for the right reason; commit that test in THIS phase. If the project supports e2e/real-browser (e.g. Playwright), use it. Only if no automated test can capture it, give command/log output WITH an explicit justification.
2186
+ 2. Root cause \u2014 the causal chain symptom\u2192cause, each link backed by concrete evidence (stack trace, log line, failing assertion, bisect), not assertion.
2187
+ 3. Proposed fix approach \u2014 what changes and WHY that removes the root cause rather than masking the symptom.
2188
+ 4. Blast radius \u2014 every area/module/contract the fix could affect.
2189
+ 5. Residual risks \u2014 foreseeable risks remaining after the fix.
2190
+
2191
+ Commit the RED reproduction test (do NOT commit {bugfixDir}; it is gitignored). End your handback with a 1-2 sentence summary; your reply must be at least two sentences, well over 100 characters \u2014 never hand back only a single word.`;
2192
+ var BUGFIX_DIAGNOSIS_REVIEW = "Review the diagnosis artifact at {diagnosisPath} for the bug reported at {specPath}. This is an autonomous workflow with no human in the loop.\n\n" + WORKFLOW_DIAGNOSIS_PROTOCOL;
2193
+ var BUGFIX_DIAGNOSIS_FIX = "Apply the reviewer's findings to the diagnosis at {diagnosisPath} now. This is an autonomous workflow \u2014 no human will respond. Make the edits yourself (re-reproduce if needed) and hand back the corrected diagnosis; never ask for confirmation, permission, or clarification. If you added or changed the RED reproduction test while addressing the findings, COMMIT it in this round (do NOT commit {bugfixDir}; it is gitignored) \u2014 the next phase reviews the committed change set, so an uncommitted repro update would be invisible to it. End your handback with a 1-2 sentence summary of what you changed; your reply must be at least two sentences, well over 100 characters \u2014 never hand back only a single word.";
2194
+ var BUGFIX_FIX_KICKOFF = `The diagnosis at {diagnosisPath} is APPROVED \u2014 treat it as ground truth (re-orient from it, not prior conversation). This is an autonomous workflow \u2014 no human will respond; do the work yourself.
2195
+
2196
+ 1. Implement the fix per the APPROVED approach.
2197
+ 2. Turn the reproduction GREEN \u2014 the failing test now passes for the right reason; if the repro was a non-test demonstration, re-run it and show the symptom is gone.
2198
+ 3. Run the project's verification/test command PLUS targeted checks across the declared blast radius, including the full suite, to catch regressions.
2199
+ 4. Commit the fix and any added happy-path/edge-case coverage tests (the RED test was committed in diagnosis). Do NOT commit {bugfixDir} (gitignored).
2200
+
2201
+ If you discover the approved cause was WRONG, do NOT silently switch to a different fix \u2014 the cause was the agreed premise of this run. Stop and hand back that you CANNOT PROCEED because the approved cause is wrong, explaining what the evidence now shows; do not deliver a fix built on a second, unreviewed theory. This halts the run and returns control to a human, who re-opens the diagnosis (a new run, or a resume after they adjust the report) \u2014 the workflow does not loop back to the diagnosis phase on its own. Otherwise, hand back the commit SHAs + verification output + a 1-2 sentence summary; your reply must be at least two sentences, well over 100 characters.`;
2202
+ var BUGFIX_FIX_REVIEW = "The implementer claims the fix for the bug at {specPath} is complete \u2014 the changes are commits {commitRange}; resolve the upper bound against LIVE HEAD and include fix-round commits. Verify against the APPROVED diagnosis at {diagnosisPath}. Independently re-run the reproduction (it must be GREEN) and the verification suite \u2014 do not trust pasted output. Confirm: the root cause is actually removed (not just relocated \u2014 anti-whack-a-mole); every declared blast-radius area is regression-free; residual risks are handled or explicitly accepted; and COVERAGE is adequate \u2014 every happy path has at least one covering test and edge cases are covered. A case that genuinely cannot be covered must be explicitly noted (not silently passed); thin coverage is a blocking finding. This is an autonomous workflow with no human in the loop.\n\n" + WORKFLOW_REVIEW_PROTOCOL;
2203
+ var BUGFIX_FIX_FIX = "Apply the reviewer's findings to commits {commitRange} now (amend or add commits). This is an autonomous workflow \u2014 no human will respond. Do the work yourself and hand back the updated commit SHAs and verification output; never ask for confirmation, permission, or clarification. End your handback with a 1-2 sentence summary; your reply must be at least two sentences, well over 100 characters.";
2204
+ var BUGFIX_POSTMORTEM_KICKOFF = "Write a post-mortem report to {postmortemPath} (create the file) for the bug fixed in this run. This is an autonomous workflow \u2014 no human will respond; do the work yourself. Recap: confirmed root cause; the fix applied; reproduction\u2192GREEN evidence; blast radius touched; coverage gaps explicitly listed (carried from the fix review); residual risks; and lessons learned. Do NOT commit {bugfixDir} (gitignored). End your handback with a 1-2 sentence summary; your reply must be at least two sentences, well over 100 characters.";
2205
+ var BUGFIX_POSTMORTEM_REVIEW = "Review the post-mortem report at {postmortemPath}. Confirm it faithfully reflects what actually happened in this run \u2014 confirmed cause, the fix, the noted coverage gaps, and residual risks are all present and honest. This is not a rubber stamp; gloss-overs or omissions are findings. This is an autonomous workflow with no human in the loop.\n\n" + WORKFLOW_REVIEW_PROTOCOL;
2206
+ var BUGFIX_POSTMORTEM_FIX = "Apply the reviewer's findings to the post-mortem at {postmortemPath} now. This is an autonomous workflow \u2014 no human will respond. Make the edits yourself and hand back the corrected report; never ask for confirmation, permission, or clarification. End your handback with a 1-2 sentence summary; your reply must be at least two sentences, well over 100 characters.";
2207
+ var COMPLEX_BUG_FIXING = {
2208
+ type: "complex-bug-fixing",
2209
+ displayName: "Complex Bug Fixing",
2210
+ description: "Reproduce \u2192 adversarially-gated diagnosis \u2192 fix & verify \u2192 post-mortem, for a reported bug whose root cause is unknown",
2211
+ defaultImplementer: "claude",
2212
+ defaultReviewer: "codex",
2213
+ phases: [
2214
+ {
2215
+ name: "diagnosis",
2216
+ implementerRole: "implementer",
2217
+ reviewerRole: "reviewer",
2218
+ maxRounds: 5,
2219
+ initialHandoffStep: "implement",
2220
+ kickoffTemplate: BUGFIX_DIAGNOSIS_KICKOFF,
2221
+ stepTemplates: {
2222
+ implement: BUGFIX_DIAGNOSIS_KICKOFF,
2223
+ review: BUGFIX_DIAGNOSIS_REVIEW,
2224
+ fix: BUGFIX_DIAGNOSIS_FIX
2225
+ },
2226
+ reviewMode: "phase-review",
2227
+ evaluatorPromptKey: "review-loop",
2228
+ artifactOut: { kind: "spec", pathTemplate: "{diagnosisPath}" },
2229
+ anchorCommitBaseOnEntry: true,
2230
+ renderFixTemplateOnFindings: true
2231
+ },
2232
+ {
2233
+ name: "fix-and-verify",
2234
+ implementerRole: "implementer",
2235
+ reviewerRole: "reviewer",
2236
+ maxRounds: 5,
2237
+ initialHandoffStep: "implement",
2238
+ kickoffTemplate: BUGFIX_FIX_KICKOFF,
2239
+ stepTemplates: {
2240
+ implement: BUGFIX_FIX_KICKOFF,
2241
+ review: BUGFIX_FIX_REVIEW,
2242
+ fix: BUGFIX_FIX_FIX
2243
+ },
2244
+ reviewMode: "acceptance-review",
2245
+ evaluatorPromptKey: "review-loop",
2246
+ artifactOut: { kind: "commit-range" },
2247
+ renderFixTemplateOnFindings: true
2248
+ },
2249
+ {
2250
+ name: "post-mortem",
2251
+ implementerRole: "implementer",
2252
+ reviewerRole: "reviewer",
2253
+ maxRounds: 3,
2254
+ initialHandoffStep: "implement",
2255
+ kickoffTemplate: BUGFIX_POSTMORTEM_KICKOFF,
2256
+ stepTemplates: {
2257
+ implement: BUGFIX_POSTMORTEM_KICKOFF,
2258
+ review: BUGFIX_POSTMORTEM_REVIEW,
2259
+ fix: BUGFIX_POSTMORTEM_FIX
2260
+ },
2261
+ reviewMode: "phase-review",
2262
+ evaluatorPromptKey: "review-loop",
2263
+ artifactOut: { kind: "spec", pathTemplate: "{postmortemPath}" },
2264
+ renderFixTemplateOnFindings: true
2265
+ }
2266
+ ]
2267
+ };
2148
2268
  var REGISTRY = {
2149
2269
  [SPEC_DRIVEN_DEVELOPMENT.type]: SPEC_DRIVEN_DEVELOPMENT,
2150
- [RALPH_LOOP.type]: RALPH_LOOP
2270
+ [RALPH_LOOP.type]: RALPH_LOOP,
2271
+ [COMPLEX_BUG_FIXING.type]: COMPLEX_BUG_FIXING
2151
2272
  };
2152
2273
  function ralphRunDir(workspaceRoot, workflowId) {
2153
2274
  return join(workspaceRoot, ".ai-whisper", "ralph", workflowId);
2154
2275
  }
2276
+ function bugfixRunDir(workspaceRoot, workflowId) {
2277
+ return join(workspaceRoot, ".ai-whisper", "bugfix", workflowId);
2278
+ }
2279
+ function bugfixPaths(workspaceRoot, workflowId) {
2280
+ const bugfixDir = bugfixRunDir(workspaceRoot, workflowId);
2281
+ return {
2282
+ bugfixDir,
2283
+ diagnosisPath: join(bugfixDir, "diagnosis.md"),
2284
+ postmortemPath: join(bugfixDir, "postmortem.md")
2285
+ };
2286
+ }
2155
2287
  function getWorkflowDefinition(type) {
2156
2288
  return REGISTRY[type];
2157
2289
  }
@@ -2371,12 +2503,16 @@ function createWorkflowControl(deps) {
2371
2503
  const collab2 = getCollab(db, input.workflow.collabId);
2372
2504
  const ralphDir = collab2 ? ralphRunDir(collab2.workspaceRoot, input.workflow.workflowId) : "";
2373
2505
  const reviewMode = useAcceptance ? "acceptance-review" : input.phase.reviewMode ?? "phase-review";
2506
+ const bf = collab2 ? bugfixPaths(collab2.workspaceRoot, input.workflow.workflowId) : { bugfixDir: "", diagnosisPath: "", postmortemPath: "" };
2374
2507
  return renderTemplate(tmpl, {
2375
2508
  specPath: input.workflow.specPath,
2376
2509
  planPath: safeDerivePlanPath(input.workflow.specPath, input.workflow.createdAt),
2377
2510
  commitRange: liveReviewCommitRange(ctx),
2378
2511
  ralphDir,
2379
- reviewMode
2512
+ reviewMode,
2513
+ bugfixDir: bf.bugfixDir,
2514
+ diagnosisPath: bf.diagnosisPath,
2515
+ postmortemPath: bf.postmortemPath
2380
2516
  });
2381
2517
  }
2382
2518
  function createContinuationHandoff(input) {
@@ -2413,12 +2549,16 @@ function createWorkflowControl(deps) {
2413
2549
  const ctx = input.workflow.workflowContext;
2414
2550
  const collab2 = getCollab(db, input.workflow.collabId);
2415
2551
  const ralphDir = collab2 ? ralphRunDir(collab2.workspaceRoot, input.workflow.workflowId) : "";
2552
+ const bf = collab2 ? bugfixPaths(collab2.workspaceRoot, input.workflow.workflowId) : { bugfixDir: "", diagnosisPath: "", postmortemPath: "" };
2416
2553
  const kickoffText = renderTemplate(phase.kickoffTemplate, {
2417
2554
  specPath: input.workflow.specPath,
2418
2555
  planPath: safeDerivePlanPath(input.workflow.specPath, input.workflow.createdAt),
2419
2556
  commitRange: liveReviewCommitRange(ctx),
2420
2557
  ralphDir,
2421
- reviewMode: phase.reviewMode ?? "phase-review"
2558
+ reviewMode: phase.reviewMode ?? "phase-review",
2559
+ bugfixDir: bf.bugfixDir,
2560
+ diagnosisPath: bf.diagnosisPath,
2561
+ postmortemPath: bf.postmortemPath
2422
2562
  });
2423
2563
  const chainId = `relay_ch_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
2424
2564
  const phaseRunId = `wfp_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
@@ -2676,15 +2816,19 @@ function createWorkflowControl(deps) {
2676
2816
  } else if (normalized.verdict === "findings") {
2677
2817
  const findingsText = input.followUpMessage ?? "Address the reviewer's findings.";
2678
2818
  let fixRequestText;
2679
- if (phase.repeatUntilComplete && phase.stepTemplates.fix) {
2819
+ if ((phase.repeatUntilComplete || phase.renderFixTemplateOnFindings) && phase.stepTemplates.fix) {
2680
2820
  const ctx = workflow.workflowContext;
2681
2821
  const collab2 = getCollab(db, workflow.collabId);
2682
2822
  const ralphDir = collab2 ? ralphRunDir(collab2.workspaceRoot, workflow.workflowId) : "";
2823
+ const bf = collab2 ? bugfixPaths(collab2.workspaceRoot, workflow.workflowId) : { bugfixDir: "", diagnosisPath: "", postmortemPath: "" };
2683
2824
  const fixTmpl = renderTemplate(phase.stepTemplates.fix, {
2684
2825
  specPath: workflow.specPath,
2685
2826
  planPath: safeDerivePlanPath(workflow.specPath, workflow.createdAt),
2686
2827
  commitRange: liveReviewCommitRange(ctx),
2687
- ralphDir
2828
+ ralphDir,
2829
+ bugfixDir: bf.bugfixDir,
2830
+ diagnosisPath: bf.diagnosisPath,
2831
+ postmortemPath: bf.postmortemPath
2688
2832
  });
2689
2833
  fixRequestText = `${fixTmpl}
2690
2834
 
@@ -3723,6 +3867,7 @@ function createControlService(db, events) {
3723
3867
  clipSample: input.clipSample,
3724
3868
  turnSample: input.turnSample,
3725
3869
  abortedByRaceGuard: input.abortedByRaceGuard,
3870
+ interferenceDetected: input.interferenceDetected ?? false,
3726
3871
  createdAt: input.now
3727
3872
  });
3728
3873
  return { captureId };
@@ -3799,8 +3944,130 @@ function createBrokerApp(input) {
3799
3944
  return app;
3800
3945
  }
3801
3946
 
3947
+ // ../broker/dist/storage/enforce-one-active-collab.js
3948
+ function defaultIsPidAlive(pid) {
3949
+ try {
3950
+ process.kill(pid, 0);
3951
+ return true;
3952
+ } catch (err) {
3953
+ return err.code === "EPERM";
3954
+ }
3955
+ }
3956
+ function dedupeActiveCollabs(db, opts) {
3957
+ const rows = db.prepare(`SELECT c.collab_id, c.workspace_id, c.created_at, d.pid AS pid,
3958
+ (SELECT COUNT(*) FROM workflows w
3959
+ WHERE w.collab_id = c.collab_id AND w.status = 'running') AS running_workflows
3960
+ FROM collab c
3961
+ LEFT JOIN broker_daemon d ON d.collab_id = c.collab_id
3962
+ WHERE c.status = 'active' AND c.workspace_id IS NOT NULL`).all();
3963
+ const byWorkspace = /* @__PURE__ */ new Map();
3964
+ for (const row of rows) {
3965
+ const key = row.workspace_id;
3966
+ const list = byWorkspace.get(key);
3967
+ if (list)
3968
+ list.push(row);
3969
+ else
3970
+ byWorkspace.set(key, [row]);
3971
+ }
3972
+ const conflicted = [];
3973
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3974
+ const stop = db.prepare("UPDATE collab SET status = 'stopped', stopped_at = ?, updated_at = ? WHERE collab_id = ?");
3975
+ for (const [workspaceId, group] of byWorkspace) {
3976
+ if (group.length < 2)
3977
+ continue;
3978
+ const workflowOwners = group.filter((r) => r.running_workflows > 0);
3979
+ if (workflowOwners.length >= 2) {
3980
+ conflicted.push(workspaceId);
3981
+ opts.warn(`Workspace ${workspaceId} has ${workflowOwners.length} active collabs that each own a running workflow: ${workflowOwners.map((r) => r.collab_id).join(", ")}. Refusing to auto-stop any (that would orphan a live run). Stop the extra collab(s) manually with \`whisper collab stop --collab <id>\`. The one-active-collab-per-workspace index will be created automatically once only one remains.`);
3982
+ continue;
3983
+ }
3984
+ let survivor;
3985
+ if (workflowOwners.length === 1) {
3986
+ survivor = workflowOwners[0];
3987
+ } else {
3988
+ const live = group.filter((r) => r.pid !== null && opts.isPidAlive(r.pid));
3989
+ if (live.length >= 1) {
3990
+ survivor = live.reduce((a, b) => a.created_at >= b.created_at ? a : b);
3991
+ } else {
3992
+ survivor = group.reduce((a, b) => a.created_at >= b.created_at ? a : b);
3993
+ }
3994
+ }
3995
+ for (const row of group) {
3996
+ if (row.collab_id === survivor.collab_id)
3997
+ continue;
3998
+ stop.run(now, now, row.collab_id);
3999
+ }
4000
+ }
4001
+ return conflicted;
4002
+ }
4003
+ var CREATE_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_one_active_per_workspace ON collab(workspace_id) WHERE status = 'active'";
4004
+ function residualDuplicateCount(db) {
4005
+ const row = db.prepare(`SELECT COUNT(*) AS n FROM (
4006
+ SELECT workspace_id FROM collab
4007
+ WHERE status = 'active' AND workspace_id IS NOT NULL
4008
+ GROUP BY workspace_id HAVING COUNT(*) > 1
4009
+ )`).get();
4010
+ return row.n;
4011
+ }
4012
+ function enforceOneActiveCollabPerWorkspace(db, options = {}) {
4013
+ const opts = {
4014
+ isPidAlive: options.isPidAlive ?? defaultIsPidAlive,
4015
+ warn: options.warn ?? ((m) => console.error(m))
4016
+ };
4017
+ const tx = db.transaction(() => {
4018
+ dedupeActiveCollabs(db, opts);
4019
+ if (residualDuplicateCount(db) === 0) {
4020
+ db.exec(CREATE_INDEX_SQL);
4021
+ } else {
4022
+ opts.warn("Skipping idx_collab_one_active_per_workspace: an irreducible duplicate active collab remains. The index will be created on a later startup once only one active collab per workspace exists.");
4023
+ }
4024
+ });
4025
+ tx();
4026
+ }
4027
+
4028
+ // ../broker/dist/storage/clipboard-capture-lease.js
4029
+ var LEASE_ID = 1;
4030
+ var DEFAULT_LEASE_TTL_MS = 5e3;
4031
+ function defaultIsPidAlive2(pid) {
4032
+ try {
4033
+ process.kill(pid, 0);
4034
+ return true;
4035
+ } catch (err) {
4036
+ return err.code === "EPERM";
4037
+ }
4038
+ }
4039
+ function resolveOptions(options) {
4040
+ return {
4041
+ isPidAlive: options.isPidAlive ?? defaultIsPidAlive2,
4042
+ ttlMs: options.ttlMs ?? DEFAULT_LEASE_TTL_MS,
4043
+ now: options.now ?? Date.now
4044
+ };
4045
+ }
4046
+ function isStale(row, opts) {
4047
+ if (row.holder_collab_id === null)
4048
+ return true;
4049
+ if (row.holder_pid === null || !opts.isPidAlive(row.holder_pid))
4050
+ return true;
4051
+ if (row.acquired_at === null)
4052
+ return true;
4053
+ const age = opts.now() - Date.parse(row.acquired_at);
4054
+ return age > opts.ttlMs;
4055
+ }
4056
+ function sweepStaleCaptureLease(db, options = {}) {
4057
+ const opts = resolveOptions(options);
4058
+ const tx = db.transaction(() => {
4059
+ const row = db.prepare("SELECT holder_collab_id, holder_pid, acquired_at FROM clipboard_capture_lease WHERE id = ?").get(LEASE_ID);
4060
+ if (!row || row.holder_collab_id === null)
4061
+ return;
4062
+ if (isStale(row, opts)) {
4063
+ db.prepare("UPDATE clipboard_capture_lease SET holder_collab_id = NULL, holder_pid = NULL, acquired_at = NULL WHERE id = ?").run(LEASE_ID);
4064
+ }
4065
+ });
4066
+ tx();
4067
+ }
4068
+
3802
4069
  // ../broker/dist/storage/apply-migrations.js
3803
- var CURRENT_SCHEMA_VERSION = 4;
4070
+ var CURRENT_SCHEMA_VERSION = 5;
3804
4071
  var initMigrationSql = `
3805
4072
  CREATE TABLE IF NOT EXISTS broker_state (
3806
4073
  id INTEGER PRIMARY KEY CHECK (id = 1),
@@ -4087,6 +4354,13 @@ CREATE TABLE IF NOT EXISTS recovery_state (
4087
4354
  recovered_at TEXT
4088
4355
  );
4089
4356
 
4357
+ CREATE TABLE IF NOT EXISTS clipboard_capture_lease (
4358
+ id INTEGER PRIMARY KEY CHECK (id = 1),
4359
+ holder_collab_id TEXT,
4360
+ holder_pid INTEGER,
4361
+ acquired_at TEXT
4362
+ );
4363
+
4090
4364
  `;
4091
4365
  function ensureBrokerStateRow(db) {
4092
4366
  db.prepare(`INSERT INTO broker_state (id, schema_version, migrated)
@@ -4097,20 +4371,20 @@ function ensureBrokerStateRow(db) {
4097
4371
  }
4098
4372
  function applyMigrations(db) {
4099
4373
  const current = db.pragma("user_version", { simple: true });
4100
- if (current >= CURRENT_SCHEMA_VERSION) {
4101
- ensureBrokerStateRow(db);
4102
- return;
4103
- }
4104
- db.exec("BEGIN EXCLUSIVE");
4105
- try {
4106
- runMigrationBody(db);
4107
- db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
4108
- ensureBrokerStateRow(db);
4109
- db.exec("COMMIT");
4110
- } catch (err) {
4111
- db.exec("ROLLBACK");
4112
- throw err;
4374
+ if (current < CURRENT_SCHEMA_VERSION) {
4375
+ db.exec("BEGIN EXCLUSIVE");
4376
+ try {
4377
+ runMigrationBody(db);
4378
+ db.pragma(`user_version = ${CURRENT_SCHEMA_VERSION}`);
4379
+ db.exec("COMMIT");
4380
+ } catch (err) {
4381
+ db.exec("ROLLBACK");
4382
+ throw err;
4383
+ }
4113
4384
  }
4385
+ ensureBrokerStateRow(db);
4386
+ enforceOneActiveCollabPerWorkspace(db);
4387
+ sweepStaleCaptureLease(db);
4114
4388
  }
4115
4389
  function runMigrationBody(db) {
4116
4390
  db.exec(initMigrationSql);
@@ -4247,6 +4521,7 @@ function runMigrationBody(db) {
4247
4521
  clip_sample TEXT,
4248
4522
  turn_sample TEXT,
4249
4523
  aborted_by_race_guard INTEGER NOT NULL DEFAULT 0,
4524
+ interference_detected INTEGER NOT NULL DEFAULT 0,
4250
4525
  created_at TEXT NOT NULL
4251
4526
  );
4252
4527
  CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_collab_created
@@ -4258,6 +4533,10 @@ function runMigrationBody(db) {
4258
4533
  CREATE INDEX IF NOT EXISTS idx_relay_capture_diagnostics_status
4259
4534
  ON relay_capture_diagnostics (capture_status);
4260
4535
  `);
4536
+ const captureDiagColumns = db.prepare("PRAGMA table_info(relay_capture_diagnostics)").all();
4537
+ if (!captureDiagColumns.some((column) => column.name === "interference_detected")) {
4538
+ db.exec("ALTER TABLE relay_capture_diagnostics ADD COLUMN interference_detected INTEGER NOT NULL DEFAULT 0");
4539
+ }
4261
4540
  db.exec(`
4262
4541
  CREATE TABLE IF NOT EXISTS relay_evaluator_diagnostics (
4263
4542
  evaluator_id TEXT PRIMARY KEY,
@@ -4512,6 +4791,23 @@ function ensureRalphWorkspace(workspaceRoot, workflowId) {
4512
4791
  return dir;
4513
4792
  }
4514
4793
 
4794
+ // ../broker/dist/runtime/bugfix-setup.js
4795
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "node:fs";
4796
+ import { join as join3 } from "node:path";
4797
+ function ensureBugfixWorkspace(workspaceRoot, workflowId) {
4798
+ const dir = bugfixRunDir(workspaceRoot, workflowId);
4799
+ mkdirSync2(dir, { recursive: true });
4800
+ const ignorePath = join3(workspaceRoot, ".ai-whisper", ".gitignore");
4801
+ if (!existsSync3(ignorePath)) {
4802
+ writeFileSync2(ignorePath, "*\n");
4803
+ }
4804
+ const bugfixIgnorePath = join3(workspaceRoot, ".ai-whisper", "bugfix", ".gitignore");
4805
+ if (!existsSync3(bugfixIgnorePath)) {
4806
+ writeFileSync2(bugfixIgnorePath, "*\n");
4807
+ }
4808
+ return dir;
4809
+ }
4810
+
4515
4811
  // ../broker/dist/runtime/workflow-driver.js
4516
4812
  function createWorkflowDriver(deps) {
4517
4813
  const { broker: broker2, headReader } = deps;
@@ -4582,7 +4878,7 @@ function createWorkflowDriver(deps) {
4582
4878
  const sender = phase.initialHandoffStep === "review" ? implementerAgent : reviewerAgent;
4583
4879
  const target = phase.initialHandoffStep === "review" ? reviewerAgent : implementerAgent;
4584
4880
  let executionBaseHeadSha;
4585
- if (phase.initialHandoffStep === "execute") {
4881
+ if (phase.initialHandoffStep === "execute" || phase.anchorCommitBaseOnEntry === true) {
4586
4882
  try {
4587
4883
  executionBaseHeadSha = await headReader.readHead(collab2.workspaceRoot);
4588
4884
  } catch (err) {
@@ -4605,6 +4901,15 @@ function createWorkflowDriver(deps) {
4605
4901
  } else {
4606
4902
  ralphDir = ralphRunDir(collab2.workspaceRoot, workflowId);
4607
4903
  }
4904
+ if (workflow.workflowType === "complex-bug-fixing") {
4905
+ try {
4906
+ ensureBugfixWorkspace(collab2.workspaceRoot, workflowId);
4907
+ } catch (err) {
4908
+ broker2.control.haltWorkflow({ workflowId, reason: `bugfix setup failed: ${String(err)}`, now });
4909
+ return;
4910
+ }
4911
+ }
4912
+ const bugfix = bugfixPaths(collab2.workspaceRoot, workflowId);
4608
4913
  const ctx = workflow.workflowContext;
4609
4914
  let planPath = workflow.specPath;
4610
4915
  try {
@@ -4616,7 +4921,10 @@ function createWorkflowDriver(deps) {
4616
4921
  planPath,
4617
4922
  commitRange: ctx.commitRange ?? "HEAD",
4618
4923
  ralphDir,
4619
- reviewMode: phase.reviewMode ?? "phase-review"
4924
+ reviewMode: phase.reviewMode ?? "phase-review",
4925
+ bugfixDir: bugfix.bugfixDir,
4926
+ diagnosisPath: bugfix.diagnosisPath,
4927
+ postmortemPath: bugfix.postmortemPath
4620
4928
  });
4621
4929
  try {
4622
4930
  broker2.control.beginPhaseRun({
@@ -5548,7 +5856,7 @@ function writeOwnPidToBrokerDaemon(db, input) {
5548
5856
 
5549
5857
  // src/runtime/evaluator-config.ts
5550
5858
  import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
5551
- import { join as join3 } from "node:path";
5859
+ import { join as join4 } from "node:path";
5552
5860
 
5553
5861
  // src/runtime/state-root.ts
5554
5862
  import os from "node:os";
@@ -5607,15 +5915,15 @@ function loadEvaluatorConfig() {
5607
5915
  const root = getStateRoot();
5608
5916
  let dotenv = {};
5609
5917
  try {
5610
- dotenv = parseDotEnv(readFileSync2(join3(root, ".env"), "utf8"));
5918
+ dotenv = parseDotEnv(readFileSync2(join4(root, ".env"), "utf8"));
5611
5919
  } catch (err) {
5612
5920
  if (err.code !== "ENOENT") throw err;
5613
5921
  }
5614
5922
  const envGet = (k) => process.env[k] ?? dotenv[k];
5615
- const authPath = join3(root, "auth.json");
5923
+ const authPath = join4(root, "auth.json");
5616
5924
  warnIfLoosePerms(authPath);
5617
5925
  const auth = readJsonFile(authPath, "auth.json");
5618
- const config = readJsonFile(join3(root, "config.json"), "config.json");
5926
+ const config = readJsonFile(join4(root, "config.json"), "config.json");
5619
5927
  const evalCfg = config?.evaluator ?? {};
5620
5928
  const evalOllama = evalCfg.ollama ?? {};
5621
5929
  const provider = (envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider) === "ollama" ? "ollama" : "anthropic";