quadwork 2.1.0 → 2.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.
Files changed (83) hide show
  1. package/out/404.html +1 -1
  2. package/out/__next.__PAGE__.txt +3 -3
  3. package/out/__next._full.txt +14 -14
  4. package/out/__next._head.txt +4 -4
  5. package/out/__next._index.txt +8 -8
  6. package/out/__next._tree.txt +2 -2
  7. package/out/_next/static/chunks/{0~-kpl6f_x5s6.js → 02kx5r305y-id.js} +1 -1
  8. package/out/_next/static/chunks/{0ud0uv.699had.js → 044n.~stdsjlo.js} +1 -1
  9. package/out/_next/static/chunks/0fvw~.-bjbvj3.js +27 -0
  10. package/out/_next/static/chunks/0g0w0q.z1ujl0.css +2 -0
  11. package/out/_next/static/chunks/12yxvamsloafv.js +1 -0
  12. package/out/_next/static/chunks/1405wfv.5lt26.js +1 -0
  13. package/out/_not-found/__next._full.txt +13 -13
  14. package/out/_not-found/__next._head.txt +4 -4
  15. package/out/_not-found/__next._index.txt +8 -8
  16. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  17. package/out/_not-found/__next._not-found.txt +3 -3
  18. package/out/_not-found/__next._tree.txt +2 -2
  19. package/out/_not-found.html +1 -1
  20. package/out/_not-found.txt +13 -13
  21. package/out/app-shell/__next._full.txt +13 -13
  22. package/out/app-shell/__next._head.txt +4 -4
  23. package/out/app-shell/__next._index.txt +8 -8
  24. package/out/app-shell/__next._tree.txt +2 -2
  25. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  26. package/out/app-shell/__next.app-shell.txt +3 -3
  27. package/out/app-shell.html +1 -1
  28. package/out/app-shell.txt +13 -13
  29. package/out/index.html +1 -1
  30. package/out/index.txt +14 -14
  31. package/out/project/_/__next._full.txt +14 -14
  32. package/out/project/_/__next._head.txt +4 -4
  33. package/out/project/_/__next._index.txt +8 -8
  34. package/out/project/_/__next._tree.txt +2 -2
  35. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  36. package/out/project/_/__next.project.$d$id.txt +3 -3
  37. package/out/project/_/__next.project.txt +3 -3
  38. package/out/project/_/queue/__next._full.txt +14 -14
  39. package/out/project/_/queue/__next._head.txt +4 -4
  40. package/out/project/_/queue/__next._index.txt +8 -8
  41. package/out/project/_/queue/__next._tree.txt +2 -2
  42. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  43. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  44. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  45. package/out/project/_/queue/__next.project.txt +3 -3
  46. package/out/project/_/queue.html +1 -1
  47. package/out/project/_/queue.txt +14 -14
  48. package/out/project/_.html +1 -1
  49. package/out/project/_.txt +14 -14
  50. package/out/settings/__next._full.txt +14 -14
  51. package/out/settings/__next._head.txt +4 -4
  52. package/out/settings/__next._index.txt +8 -8
  53. package/out/settings/__next._tree.txt +2 -2
  54. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  55. package/out/settings/__next.settings.txt +3 -3
  56. package/out/settings.html +1 -1
  57. package/out/settings.txt +14 -14
  58. package/out/setup/__next._full.txt +14 -14
  59. package/out/setup/__next._head.txt +4 -4
  60. package/out/setup/__next._index.txt +8 -8
  61. package/out/setup/__next._tree.txt +2 -2
  62. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  63. package/out/setup/__next.setup.txt +3 -3
  64. package/out/setup.html +1 -1
  65. package/out/setup.txt +14 -14
  66. package/package.json +2 -1
  67. package/server/index.js +106 -18
  68. package/server/routes.js +1714 -510
  69. package/server/run-tests.js +122 -0
  70. package/server/self-heal.js +100 -0
  71. package/templates/GITHUB.md +46 -0
  72. package/templates/seeds/butler.CLAUDE.md +2 -0
  73. package/templates/seeds/dev.AGENTS.md +26 -0
  74. package/templates/seeds/head.AGENTS.md +35 -6
  75. package/templates/seeds/re1.AGENTS.md +31 -3
  76. package/templates/seeds/re2.AGENTS.md +31 -3
  77. package/out/_next/static/chunks/0_79hkefw1mo2.js +0 -1
  78. package/out/_next/static/chunks/0pfyuhd8ccue..css +0 -2
  79. package/out/_next/static/chunks/0q4bm04c1jl_3.js +0 -1
  80. package/out/_next/static/chunks/0zk4tzycn0w4g.js +0 -25
  81. /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → eQFYDbR6C-ZREUVgx0QFH}/_buildManifest.js +0 -0
  82. /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → eQFYDbR6C-ZREUVgx0QFH}/_clientMiddlewareManifest.js +0 -0
  83. /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → eQFYDbR6C-ZREUVgx0QFH}/_ssgManifest.js +0 -0
package/server/index.js CHANGED
@@ -11,6 +11,7 @@ const routes = require("./routes");
11
11
  const fileChat = require("./file-chat");
12
12
  const { dispatchToAgentPTY, cleanupSession: cleanupPtyDispatcher } = require("./pty-dispatcher");
13
13
  const { runAcMigration } = require("./migrate-ac");
14
+ const selfHeal = require("./self-heal");
14
15
 
15
16
  const net = require("net");
16
17
  const config = readConfig();
@@ -502,6 +503,39 @@ async function spawnAgentPty(project, agent, opts = {}) {
502
503
  if (session.scrollback.length > SCROLLBACK_SIZE) {
503
504
  session.scrollback = session.scrollback.slice(-SCROLLBACK_SIZE);
504
505
  }
506
+
507
+ // #797: observe-only self-heal detector. Wrapped in its own try/catch so
508
+ // a detector bug can never break the PTY → xterm pipeline; the chunk is
509
+ // never consumed or altered (the WS viewer forwards it independently).
510
+ try {
511
+ selfHeal.observeChunk(key, data, {
512
+ now: Date.now(),
513
+ recovering: !!session._autoRecovering,
514
+ onRestart: () => {
515
+ session._autoRecovering = true;
516
+ console.log(`[self-heal] ${key}: thinking-block 400 detected — restarting session`);
517
+ // #825: NO clearSelfHeal here — the breaker window must persist
518
+ // across auto-restarts so repeated trips can pause auto-recovery.
519
+ restartAgentSession(key, { reason: "thinking-block-400" })
520
+ .then((result) => {
521
+ if (result && result.ok) {
522
+ emitSystemMessage(project, `${agent} auto-restarted (recovered from thinking-block API error)`);
523
+ }
524
+ })
525
+ .catch((err) => console.error(`[self-heal] ${key}: auto-restart failed:`, err.message))
526
+ .finally(() => {
527
+ const s = agentSessions.get(key);
528
+ if (s) s._autoRecovering = false;
529
+ });
530
+ },
531
+ onBreaker: (message) => {
532
+ console.log(`[self-heal] ${key}: ${message}`);
533
+ emitSystemMessage(project, message);
534
+ },
535
+ });
536
+ } catch (err) {
537
+ console.error(`[self-heal] ${key}: detector error (ignored):`, err.message);
538
+ }
505
539
  });
506
540
 
507
541
  term.onExit(({ exitCode }) => {
@@ -525,7 +559,16 @@ async function spawnAgentPty(project, agent, opts = {}) {
525
559
  }
526
560
  }
527
561
 
528
- async function stopAgentSession(key) {
562
+ async function stopAgentSession(key, { clearSelfHeal = false } = {}) {
563
+ // #825 (#797 follow-up): a MANUAL stop/restart/reset must reset the per-agent
564
+ // self-heal circuit-breaker window, so a fresh operator-driven session isn't
565
+ // suppressed by a stale "paused" state from a prior trip. This is gated:
566
+ // the auto-restart path (restartAgentSession with reason "thinking-block-400")
567
+ // leaves clearSelfHeal=false, because clearing on every auto-restart would
568
+ // reset countInWindow each time and defeat the #797 breaker entirely. Cleared
569
+ // before the session lookup so a manual stop resets the window even when no
570
+ // live session remains (e.g. the agent already exited).
571
+ if (clearSelfHeal) selfHeal.clearState(key);
529
572
  const session = agentSessions.get(key);
530
573
  if (!session) {
531
574
  agentSessions.set(key, { projectId: null, agentId: null, term: null, viewers: new Set(), viewerDims: new Map(), lastDims: null, state: "stopped", error: null });
@@ -596,7 +639,7 @@ app.post("/api/agents/:project/reset", async (req, res) => {
596
639
  for (const agentId of allAgentIds) {
597
640
  const s = agentSessions.get(`${projectId}/${agentId}`);
598
641
  if (s) s._suppressLifecycleMsg = true;
599
- await stopAgentSession(`${projectId}/${agentId}`);
642
+ await stopAgentSession(`${projectId}/${agentId}`, { clearSelfHeal: true }); // #825: manual reset resets the self-heal window
600
643
  }
601
644
 
602
645
  // Respawn all agents with fresh MCP tokens
@@ -635,7 +678,7 @@ app.post("/api/full-reset", async (_req, res) => {
635
678
  console.log("[full-reset] stopping all agent sessions...");
636
679
  const sessionKeys = [...agentSessions.keys()];
637
680
  for (const key of sessionKeys) {
638
- await stopAgentSession(key);
681
+ await stopAgentSession(key, { clearSelfHeal: true }); // #825: manual full-reset resets the self-heal window
639
682
  }
640
683
 
641
684
  console.log("[full-reset] stopping Butler...");
@@ -707,23 +750,35 @@ app.post("/api/agents/:project/:agent/start", async (req, res) => {
707
750
  app.post("/api/agents/:project/:agent/stop", async (req, res) => {
708
751
  const { project, agent } = req.params;
709
752
  const key = `${project}/${agent}`;
710
- await stopAgentSession(key);
753
+ await stopAgentSession(key, { clearSelfHeal: true }); // #825: manual stop resets the self-heal window
711
754
  res.json({ ok: true, state: "stopped" });
712
755
  });
713
756
 
714
757
  // --- Lifecycle: restart ---
715
758
 
716
- app.post("/api/agents/:project/:agent/restart", async (req, res) => {
717
- const { project, agent } = req.params;
718
- const key = `${project}/${agent}`;
759
+ // #797: shared restart sequence, used by both the manual restart route and
760
+ // the self-heal detector. Exactly the prior route body — no new lifecycle
761
+ // logic. The `reason` is informational (logged by callers).
762
+ async function restartAgentSession(key, { reason, clearSelfHeal = false } = {}) {
763
+ const [project, agent] = key.split("/");
764
+ console.log(`[restart] ${key}: restarting session (reason: ${reason || "unspecified"})`);
719
765
 
720
766
  // #241: must await deregister before respawn so the slot frees and
721
767
  // the fresh register lands at slot 1 instead of head-2.
722
768
  const existing = agentSessions.get(key);
723
769
  if (existing) existing._suppressLifecycleMsg = true;
724
- await stopAgentSession(key);
770
+ // #825: forward clearSelfHeal — a manual restart resets the breaker window;
771
+ // the self-heal auto-restart does NOT (preserves the #797 circuit breaker).
772
+ await stopAgentSession(key, { clearSelfHeal });
773
+
774
+ return spawnAgentPty(project, agent, { suppressLifecycleMsg: true });
775
+ }
776
+
777
+ app.post("/api/agents/:project/:agent/restart", async (req, res) => {
778
+ const { project, agent } = req.params;
779
+ const key = `${project}/${agent}`;
725
780
 
726
- const result = await spawnAgentPty(project, agent, { suppressLifecycleMsg: true });
781
+ const result = await restartAgentSession(key, { reason: "manual", clearSelfHeal: true }); // #825
727
782
  if (result.ok) {
728
783
  emitSystemMessage(project, `${agent} restarted`);
729
784
  res.json({ ok: true, state: "running", pid: result.pid });
@@ -972,10 +1027,11 @@ app.get("/api/butler/status", (_req, res) => {
972
1027
  const triggers = new Map();
973
1028
 
974
1029
  const DEFAULT_MESSAGE = `@head @re1 @re2 @dev — Queue check.
975
- Head: Merge any PR with both approvals, assign next from queue.
1030
+ Discovery: read GITHUB.md (or GET /api/github-parsed) for issue/PR state instead of running gh. If GITHUB.md is absent or stale (>2 cycles / _stale), do ONE direct gh read to confirm. GITHUB.md may lag — confirm with a direct gh read before any merge/review decision.
1031
+ Head: Merge any PR with both current-revision approvals, assign next from queue.
976
1032
  Dev: Work on assigned ticket or address review feedback.
977
- RE1/RE2: Review open PRs. If Dev pushed fixes, re-review. Post verdict on PR AND notify here.
978
- ALL: Communicate via this chat by tagging agents. Your terminal is NOT visible.`;
1033
+ RE1/RE2: Review ONLY PRs you were @mentioned on in this chat (not all open PRs). If Dev pushed fixes, re-review. Post verdict on PR AND notify here.
1034
+ ALL: If nothing is assigned or pending for you, no-op quietly. Communicate via this chat by tagging agents. Your terminal is NOT visible.`;
979
1035
 
980
1036
  // #518: server-side bridge lifecycle helpers. Stop and start Telegram +
981
1037
  // Discord bridges so they respond to batch transitions even when the
@@ -1070,8 +1126,16 @@ async function sendTriggerMessage(projectId) {
1070
1126
  );
1071
1127
  if (bpRes.ok) {
1072
1128
  const bp = await bpRes.json();
1073
- if (bp && bp.complete) {
1074
- console.log(`[auto-trigger] ${projectId}: batch complete, auto-stopped`);
1129
+ // #810: gate auto-stop on completeConfirmed (two distinct successful
1130
+ // fetch cycles), NOT a single transient/stale `complete`.
1131
+ // #864: also auto-stop on an explicit operator clear (`liveActiveBatchCleared`).
1132
+ // The preserved snapshot may keep `items` non-empty / `complete` mixed, so
1133
+ // `completeConfirmed` alone won't fire when items don't all resolve as
1134
+ // merged/closed (e.g. a duplicate unmerged PR). The cleared flag is the
1135
+ // operator's intent and overrides those signals for lifecycle purposes.
1136
+ const clearedByOperator = !!(bp && bp.liveActiveBatchCleared);
1137
+ if (bp && (bp.completeConfirmed || clearedByOperator)) {
1138
+ console.log(`[auto-trigger] ${projectId}: batch ${clearedByOperator ? "cleared by operator" : "complete (confirmed)"}, auto-stopped`);
1075
1139
  stopTrigger(projectId);
1076
1140
  // Also stop caffeinate if no other triggers remain running
1077
1141
  // (#441 companion fix). caffeinateProcess is global (not
@@ -1663,12 +1727,24 @@ async function autoStopPollingTick() {
1663
1727
  if (!res.ok) continue;
1664
1728
  const bp = await res.json();
1665
1729
  const hasItems = bp.items && bp.items.length > 0;
1730
+ // #810: gate auto-stop on completeConfirmed (two distinct successful fetch
1731
+ // cycles), not a single transient/stale `complete`. Track prev on the
1732
+ // confirmed value so the bridge-stop transition guard fires on it.
1733
+ // #864: an explicit operator clear (`liveActiveBatchCleared`) ALSO triggers
1734
+ // the stop path, so trigger + bridges shut down when Head sets the Active
1735
+ // Batch section to empty even if the preserved snapshot's items don't all
1736
+ // resolve as merged/closed (e.g. a duplicate unmerged PR keeps the items
1737
+ // in `in_review`). The cleared flag is the operator's intent.
1738
+ const confirmed = !!bp.completeConfirmed;
1739
+ const clearedByOperator = !!bp.liveActiveBatchCleared;
1740
+ const shouldStop = confirmed || clearedByOperator;
1666
1741
  const prev = _bridgeBatchPrev.get(project.id);
1667
- _bridgeBatchPrev.set(project.id, { complete: bp.complete, hasItems });
1742
+ _bridgeBatchPrev.set(project.id, { complete: shouldStop, hasItems });
1668
1743
 
1669
- if (bp && bp.complete) {
1744
+ if (bp && shouldStop) {
1670
1745
  if (hasTriggerAuto) {
1671
- console.log(`[auto-trigger] ${project.id}: batch complete, auto-stopped (poller)`);
1746
+ const reason = clearedByOperator ? "cleared by operator" : "complete (confirmed)";
1747
+ console.log(`[auto-trigger] ${project.id}: batch ${reason}, auto-stopped (poller)`);
1672
1748
  stopTrigger(project.id);
1673
1749
  if (caffeinateProcess.process && triggers.size === 0) {
1674
1750
  try { caffeinateProcess.process.kill("SIGTERM"); } catch {}
@@ -1684,7 +1760,9 @@ async function autoStopPollingTick() {
1684
1760
  }
1685
1761
 
1686
1762
  // #518: detect batch-start transition → auto-start bridges
1687
- if (hasBridgeAuto && hasItems && !bp.complete) {
1763
+ // #864: do NOT auto-start on a cleared queue even though hasItems may be
1764
+ // true from the preserved snapshot — the operator's clear is a stop signal.
1765
+ if (hasBridgeAuto && hasItems && !bp.complete && !clearedByOperator) {
1688
1766
  const isNewBatch = !prev || prev.complete || !prev.hasItems;
1689
1767
  if (isNewBatch) {
1690
1768
  await autoStartBridges(project.id, project, qwPort);
@@ -1842,6 +1920,16 @@ server.listen(PORT, "127.0.0.1", async () => {
1842
1920
 
1843
1921
  runStartupMigrations(startupCfg);
1844
1922
 
1923
+ // #856: Auto-reseed worktree AGENTS.md when the package version changes.
1924
+ // Per-project completion state lives in ~/.quadwork/reseed-state.json, so
1925
+ // projects deferred mid-batch stay pending and retry on the next startup.
1926
+ // Failures here MUST NOT block server boot.
1927
+ try {
1928
+ await routes.autoReseedOnStartup(startupCfg);
1929
+ } catch (err) {
1930
+ console.error(`[reseed] auto-reseed failed: ${err.message}`);
1931
+ }
1932
+
1845
1933
  if (startupCfg.butler && startupCfg.butler.enabled && startupCfg.butler.auto_start) {
1846
1934
  const result = spawnButlerPty();
1847
1935
  if (result.ok) console.log(`[butler] auto-started (PID: ${result.pid})`);