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.
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +3 -3
- package/out/__next._full.txt +14 -14
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +8 -8
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/{0~-kpl6f_x5s6.js → 02kx5r305y-id.js} +1 -1
- package/out/_next/static/chunks/{0ud0uv.699had.js → 044n.~stdsjlo.js} +1 -1
- package/out/_next/static/chunks/0fvw~.-bjbvj3.js +27 -0
- package/out/_next/static/chunks/0g0w0q.z1ujl0.css +2 -0
- package/out/_next/static/chunks/12yxvamsloafv.js +1 -0
- package/out/_next/static/chunks/1405wfv.5lt26.js +1 -0
- package/out/_not-found/__next._full.txt +13 -13
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +8 -8
- package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +13 -13
- package/out/app-shell/__next._full.txt +13 -13
- package/out/app-shell/__next._head.txt +4 -4
- package/out/app-shell/__next._index.txt +8 -8
- package/out/app-shell/__next._tree.txt +2 -2
- package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
- package/out/app-shell/__next.app-shell.txt +3 -3
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +13 -13
- package/out/index.html +1 -1
- package/out/index.txt +14 -14
- package/out/project/_/__next._full.txt +14 -14
- package/out/project/_/__next._head.txt +4 -4
- package/out/project/_/__next._index.txt +8 -8
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
- package/out/project/_/__next.project.$d$id.txt +3 -3
- package/out/project/_/__next.project.txt +3 -3
- package/out/project/_/queue/__next._full.txt +14 -14
- package/out/project/_/queue/__next._head.txt +4 -4
- package/out/project/_/queue/__next._index.txt +8 -8
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.txt +3 -3
- package/out/project/_/queue/__next.project.txt +3 -3
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +14 -14
- package/out/project/_.html +1 -1
- package/out/project/_.txt +14 -14
- package/out/settings/__next._full.txt +14 -14
- package/out/settings/__next._head.txt +4 -4
- package/out/settings/__next._index.txt +8 -8
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +3 -3
- package/out/settings/__next.settings.txt +3 -3
- package/out/settings.html +1 -1
- package/out/settings.txt +14 -14
- package/out/setup/__next._full.txt +14 -14
- package/out/setup/__next._head.txt +4 -4
- package/out/setup/__next._index.txt +8 -8
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +3 -3
- package/out/setup/__next.setup.txt +3 -3
- package/out/setup.html +1 -1
- package/out/setup.txt +14 -14
- package/package.json +2 -1
- package/server/index.js +106 -18
- package/server/routes.js +1714 -510
- package/server/run-tests.js +122 -0
- package/server/self-heal.js +100 -0
- package/templates/GITHUB.md +46 -0
- package/templates/seeds/butler.CLAUDE.md +2 -0
- package/templates/seeds/dev.AGENTS.md +26 -0
- package/templates/seeds/head.AGENTS.md +35 -6
- package/templates/seeds/re1.AGENTS.md +31 -3
- package/templates/seeds/re2.AGENTS.md +31 -3
- package/out/_next/static/chunks/0_79hkefw1mo2.js +0 -1
- package/out/_next/static/chunks/0pfyuhd8ccue..css +0 -2
- package/out/_next/static/chunks/0q4bm04c1jl_3.js +0 -1
- package/out/_next/static/chunks/0zk4tzycn0w4g.js +0 -25
- /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → eQFYDbR6C-ZREUVgx0QFH}/_buildManifest.js +0 -0
- /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → eQFYDbR6C-ZREUVgx0QFH}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1074
|
-
|
|
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:
|
|
1742
|
+
_bridgeBatchPrev.set(project.id, { complete: shouldStop, hasItems });
|
|
1668
1743
|
|
|
1669
|
-
if (bp &&
|
|
1744
|
+
if (bp && shouldStop) {
|
|
1670
1745
|
if (hasTriggerAuto) {
|
|
1671
|
-
|
|
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
|
-
|
|
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})`);
|