quadwork 2.1.0 → 2.2.0
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/0dbfrdfwj565f.css +2 -0
- package/out/_next/static/chunks/0fvw~.-bjbvj3.js +27 -0
- package/out/_next/static/chunks/0y_59cdk3r4z6.js +1 -0
- package/out/_next/static/chunks/12yxvamsloafv.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 +89 -17
- package/server/routes.js +1672 -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 +12 -0
- package/templates/seeds/head.AGENTS.md +21 -6
- package/templates/seeds/re1.AGENTS.md +17 -3
- package/templates/seeds/re2.AGENTS.md +17 -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 → h8gr2UEtEQkyXBVa2J0z9}/_buildManifest.js +0 -0
- /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → h8gr2UEtEQkyXBVa2J0z9}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{vvtpLPTwziTD3klXH46MU → h8gr2UEtEQkyXBVa2J0z9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// #836: cross-platform test runner. Discovers every `*.test.js` file under
|
|
3
|
+
// `server/` via fs.readdirSync recursion (no shell glob — must work on
|
|
4
|
+
// Windows) and runs each one in its own `node <file>` child process. The
|
|
5
|
+
// per-file process isolates any residual module state (#836 root cause: the
|
|
6
|
+
// shared routes.js module pollers used to pin the loop across files in
|
|
7
|
+
// `node --test server/*.test.js`, hanging the whole suite). Aggregate exit
|
|
8
|
+
// code is non-zero if any child fails or times out.
|
|
9
|
+
//
|
|
10
|
+
// Out of scope for #836: the two Jest-style tests under server/__tests__/
|
|
11
|
+
// (bridge-auto-stop-guard, rate-limit-handling) use `describe`/`expect` and
|
|
12
|
+
// can't run under plain node yet. They're explicitly skipped here with a
|
|
13
|
+
// clear log line so a future rewrite is obvious work.
|
|
14
|
+
|
|
15
|
+
const { spawn } = require("child_process");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
|
|
19
|
+
const SERVER_DIR = __dirname;
|
|
20
|
+
const ROOT = path.resolve(SERVER_DIR, "..");
|
|
21
|
+
const PER_FILE_TIMEOUT_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
// Skip-list keyed by absolute path so the runner stays Windows-friendly.
|
|
24
|
+
const SKIP = new Set([
|
|
25
|
+
path.join(SERVER_DIR, "__tests__", "bridge-auto-stop-guard.test.js"),
|
|
26
|
+
path.join(SERVER_DIR, "__tests__", "rate-limit-handling.test.js"),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function discover(dir) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
if (entry.name === "node_modules") continue;
|
|
33
|
+
const full = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) out.push(...discover(full));
|
|
35
|
+
else if (entry.isFile() && entry.name.endsWith(".test.js")) out.push(full);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runOne(file) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
const child = spawn(process.execPath, [file], {
|
|
44
|
+
cwd: ROOT,
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
46
|
+
env: process.env,
|
|
47
|
+
});
|
|
48
|
+
let stdout = "";
|
|
49
|
+
let stderr = "";
|
|
50
|
+
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
51
|
+
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
52
|
+
const killer = setTimeout(() => {
|
|
53
|
+
timedOut = true;
|
|
54
|
+
child.kill("SIGKILL");
|
|
55
|
+
}, PER_FILE_TIMEOUT_MS);
|
|
56
|
+
child.on("close", (code, signal) => {
|
|
57
|
+
clearTimeout(killer);
|
|
58
|
+
resolve({ file, code, signal, stdout, stderr, timedOut });
|
|
59
|
+
});
|
|
60
|
+
child.on("error", (err) => {
|
|
61
|
+
clearTimeout(killer);
|
|
62
|
+
resolve({ file, code: -1, signal: null, stdout, stderr: stderr + String(err), timedOut: false });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function indent(text, marker) {
|
|
68
|
+
if (!text) return "";
|
|
69
|
+
return text.split("\n").map((l) => ` ${marker} ${l}`).join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
(async () => {
|
|
73
|
+
const allFiles = discover(SERVER_DIR).sort();
|
|
74
|
+
const runnable = allFiles.filter((f) => !SKIP.has(f));
|
|
75
|
+
const skipped = allFiles.filter((f) => SKIP.has(f));
|
|
76
|
+
|
|
77
|
+
console.log(`\nDiscovered ${allFiles.length} test files (${skipped.length} skipped, ${runnable.length} to run).\n`);
|
|
78
|
+
|
|
79
|
+
let passed = 0;
|
|
80
|
+
let failed = 0;
|
|
81
|
+
const failures = [];
|
|
82
|
+
|
|
83
|
+
for (const file of runnable) {
|
|
84
|
+
const rel = path.relative(ROOT, file);
|
|
85
|
+
process.stdout.write(`▶ ${rel} … `);
|
|
86
|
+
const r = await runOne(file);
|
|
87
|
+
if (r.timedOut) {
|
|
88
|
+
console.log(`TIMEOUT (>${PER_FILE_TIMEOUT_MS / 1000}s)`);
|
|
89
|
+
failed++;
|
|
90
|
+
failures.push({ file: rel, reason: `timed out after ${PER_FILE_TIMEOUT_MS / 1000}s`, stdout: r.stdout, stderr: r.stderr });
|
|
91
|
+
} else if (r.code === 0) {
|
|
92
|
+
console.log("PASS");
|
|
93
|
+
passed++;
|
|
94
|
+
} else {
|
|
95
|
+
console.log(`FAIL (exit ${r.code}${r.signal ? `, signal ${r.signal}` : ""})`);
|
|
96
|
+
failed++;
|
|
97
|
+
failures.push({ file: rel, reason: `exit ${r.code}${r.signal ? `, signal ${r.signal}` : ""}`, stdout: r.stdout, stderr: r.stderr });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`\n${passed} passed, ${failed} failed, ${skipped.length} skipped.`);
|
|
102
|
+
|
|
103
|
+
if (skipped.length) {
|
|
104
|
+
console.log("\nSkipped (Jest-style — out of #836 scope; rewrite separately):");
|
|
105
|
+
for (const f of skipped) console.log(` - ${path.relative(ROOT, f)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (failures.length) {
|
|
109
|
+
console.log("\nFailure details:\n");
|
|
110
|
+
for (const f of failures) {
|
|
111
|
+
console.log(`▼ ${f.file} (${f.reason})`);
|
|
112
|
+
if (f.stdout) console.log(indent(f.stdout.replace(/\n$/, ""), "|"));
|
|
113
|
+
if (f.stderr) console.log(indent(f.stderr.replace(/\n$/, ""), "!"));
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
119
|
+
})().catch((err) => {
|
|
120
|
+
console.error("test runner crashed:", err);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// #797: Self-heal detector for the Opus 4.8 thinking-block API 400 that
|
|
4
|
+
// permanently bricks a `claude` agent. Once a session hits this state every
|
|
5
|
+
// subsequent turn 400s instantly and the agent does nothing while QuadWork
|
|
6
|
+
// still shows it "online". A bricked conversation is unrecoverable, so the
|
|
7
|
+
// only safe fix is to restart the session (which the agent re-seeds from chat
|
|
8
|
+
// on its next trigger).
|
|
9
|
+
//
|
|
10
|
+
// This module is pure decision + per-agent guard state so it is unit-testable
|
|
11
|
+
// without booting the server. The actual restart / message side effects are
|
|
12
|
+
// supplied by the caller as callbacks; this file performs no I/O of its own.
|
|
13
|
+
|
|
14
|
+
const COOLDOWN_MS = 60 * 1000;
|
|
15
|
+
const CIRCUIT_WINDOW_MS = 15 * 60 * 1000;
|
|
16
|
+
const CIRCUIT_MAX = 3;
|
|
17
|
+
|
|
18
|
+
// Per-agent guard state: key -> { lastAutoRestartAt, restarts: number[], breakerNotified }
|
|
19
|
+
const _state = new Map();
|
|
20
|
+
|
|
21
|
+
// Conservative, Anthropic-API-specific signature. `codex` agents never emit
|
|
22
|
+
// it. We require BOTH substrings so normal output that merely mentions
|
|
23
|
+
// "thinking" can never trip the detector.
|
|
24
|
+
function isThinkingBlock400(data) {
|
|
25
|
+
return typeof data === "string"
|
|
26
|
+
&& data.includes("cannot be modified")
|
|
27
|
+
&& data.includes("thinking");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function breakerMessage(agent) {
|
|
31
|
+
return `${agent} repeatedly hitting the thinking-block API error — auto-recovery paused, manual attention needed`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Observe a single PTY chunk and decide whether to auto-restart. Never throws:
|
|
35
|
+
// callback failures are swallowed so a self-heal bug can never break the PTY
|
|
36
|
+
// data pipeline. Returns the action taken for logging/tests:
|
|
37
|
+
// "no-match" | "recovering" | "cooldown" | "breaker" | "restart"
|
|
38
|
+
//
|
|
39
|
+
// opts:
|
|
40
|
+
// now - current epoch ms (injected for testability)
|
|
41
|
+
// recovering - true while a restart for this agent is already in flight
|
|
42
|
+
// onRestart - (reason) => void invoked when a restart should happen
|
|
43
|
+
// onBreaker - (message) => void invoked once when the circuit breaker trips
|
|
44
|
+
function observeChunk(key, data, opts = {}) {
|
|
45
|
+
try {
|
|
46
|
+
if (!isThinkingBlock400(data)) return "no-match";
|
|
47
|
+
|
|
48
|
+
const now = opts.now;
|
|
49
|
+
const agent = key.split("/")[1] || key;
|
|
50
|
+
const st = _state.get(key) || { lastAutoRestartAt: -Infinity, restarts: [], breakerNotified: false };
|
|
51
|
+
|
|
52
|
+
// Re-entrancy: a restart for this agent is already underway. The wedge
|
|
53
|
+
// keeps printing the 400 on the dying PTY — ignore those lines.
|
|
54
|
+
if (opts.recovering) return "recovering";
|
|
55
|
+
|
|
56
|
+
// Cooldown: also absorbs the burst of repeated 400 lines from one wedge.
|
|
57
|
+
if (now - st.lastAutoRestartAt < COOLDOWN_MS) return "cooldown";
|
|
58
|
+
|
|
59
|
+
// Circuit breaker: drop restarts older than the window, then refuse if we
|
|
60
|
+
// have already restarted CIRCUIT_MAX times inside it. An infinite restart
|
|
61
|
+
// loop would only mask a deeper failure.
|
|
62
|
+
st.restarts = st.restarts.filter((t) => now - t < CIRCUIT_WINDOW_MS);
|
|
63
|
+
if (st.restarts.length >= CIRCUIT_MAX) {
|
|
64
|
+
const firstTrip = !st.breakerNotified;
|
|
65
|
+
st.breakerNotified = true;
|
|
66
|
+
_state.set(key, st);
|
|
67
|
+
if (firstTrip && typeof opts.onBreaker === "function") {
|
|
68
|
+
try { opts.onBreaker(breakerMessage(agent)); } catch {}
|
|
69
|
+
}
|
|
70
|
+
return "breaker";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
st.lastAutoRestartAt = now;
|
|
74
|
+
st.restarts.push(now);
|
|
75
|
+
_state.set(key, st);
|
|
76
|
+
if (typeof opts.onRestart === "function") {
|
|
77
|
+
try { opts.onRestart("thinking-block-400"); } catch {}
|
|
78
|
+
}
|
|
79
|
+
return "restart";
|
|
80
|
+
} catch {
|
|
81
|
+
// A detector bug must never break the PTY → xterm pipeline.
|
|
82
|
+
return "no-match";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Drop guard state for an agent (e.g. on permanent stop).
|
|
87
|
+
function clearState(key) {
|
|
88
|
+
_state.delete(key);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
observeChunk,
|
|
93
|
+
isThinkingBlock400,
|
|
94
|
+
breakerMessage,
|
|
95
|
+
clearState,
|
|
96
|
+
_state,
|
|
97
|
+
COOLDOWN_MS,
|
|
98
|
+
CIRCUIT_WINDOW_MS,
|
|
99
|
+
CIRCUIT_MAX,
|
|
100
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# {{project_name}} — GitHub State
|
|
2
|
+
|
|
3
|
+
> **Repo:** {{repo}}
|
|
4
|
+
> **Generated:** never · staleCycles: 0 · stale: true
|
|
5
|
+
>
|
|
6
|
+
> Machine-generated by QuadWork from live GitHub state — do NOT edit the
|
|
7
|
+
> sections below (they are overwritten on every sync). Edit only `## Notes`.
|
|
8
|
+
> Agents read this file for GitHub discovery instead of calling `gh` directly.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Open Issues
|
|
13
|
+
|
|
14
|
+
(awaiting first sync)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Open PRs
|
|
19
|
+
|
|
20
|
+
(awaiting first sync)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Recently Closed Issues
|
|
25
|
+
|
|
26
|
+
(awaiting first sync)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Recently Merged PRs
|
|
31
|
+
|
|
32
|
+
(awaiting first sync)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Review Detail
|
|
37
|
+
|
|
38
|
+
(awaiting first sync)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
_Advisory only. The Review Detail above is body-marker attribution (re1/re2),
|
|
45
|
+
not merge-authoritative — Head re-checks live before merging. This section is
|
|
46
|
+
human/Head-writable and is preserved across regeneration._
|
|
@@ -49,6 +49,8 @@ Butler is the cross-project operator assistant:
|
|
|
49
49
|
|
|
50
50
|
Read `~/.quadwork/config.json` for project IDs, repos, and working directories. Access any repo via `gh -R owner/repo`. Know worktree layout: `<working_dir>-{head,dev,re1,re2}`.
|
|
51
51
|
|
|
52
|
+
**Board discovery via GITHUB.md.** To see a project's issue/PR board state, read the server-authored per-project file `~/.quadwork/<project>/GITHUB.md` (or `GET http://127.0.0.1:8400/api/github-parsed?project=<project>`) instead of `gh pr list`/`gh issue list`. **Writes and correctness-critical reads stay direct, live `gh`:** `gh issue create`/`gh issue edit`, `gh release create`, and PR-review reads (`gh pr view`, `git diff`). **By-number / live fallback:** when acting on a specific number, use a single-object `gh` call directly; if GITHUB.md is absent or stale (`_stale` / older than ~2 cycles), do one direct `gh` read — never conclude "no work" from a stale or empty file.
|
|
53
|
+
|
|
52
54
|
**Critical: project context isolation.** Butler manages multiple projects simultaneously. To prevent mixing contexts:
|
|
53
55
|
- Always specify `-R owner/repo` when running `gh` commands — never rely on cwd
|
|
54
56
|
- When discussing a project, state the project name at the start of each response
|
|
@@ -50,6 +50,18 @@ The project's task queue lives at the absolute path:
|
|
|
50
50
|
|
|
51
51
|
Head owns this file — do not edit it. Read it when you need context on the batch you're working in or want to see what's coming next.
|
|
52
52
|
|
|
53
|
+
## GitHub State (discovery)
|
|
54
|
+
For board context — what issues/PRs exist and their state — read the server-authored file instead of running `gh pr list`/`gh issue list`:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
~/.quadwork/{{project_name}}/GITHUB.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
(or `GET http://127.0.0.1:8400/api/github-parsed?project={{project_name}}` for JSON). The server regenerates it from live GitHub each poll cycle.
|
|
61
|
+
|
|
62
|
+
- Reading the **assigned issue** stays a live, by-number call: `gh issue view <n>` (the file is for context, not the source of truth for the issue you're implementing).
|
|
63
|
+
- **By-number / live fallback:** Head assigns by issue number in chat — act on it directly; never gate its existence on the file. If the assigned issue is missing from GITHUB.md, or the file is stale (`_stale` true / older than ~2 cycles), just `gh issue view <n>` — **never conclude "no work" from a stale or empty file.**
|
|
64
|
+
|
|
53
65
|
## Role
|
|
54
66
|
- Implement features, fix bugs, and refactor code as assigned by Head
|
|
55
67
|
- Create feature branches, write code, and open PRs
|
|
@@ -48,11 +48,26 @@ When checking for mentions addressed to you, match your **base role name** regar
|
|
|
48
48
|
- Final guard on all merges — verify RE1/RE2 approval exists before merging
|
|
49
49
|
|
|
50
50
|
## Allowed Actions
|
|
51
|
-
- `gh issue create`, `gh issue edit`, `gh issue
|
|
52
|
-
- `gh pr merge` (only after RE1/RE2 approval)
|
|
53
|
-
- `gh pr
|
|
51
|
+
- `gh issue create`, `gh issue edit`, `gh issue view` (live, by number)
|
|
52
|
+
- `gh pr merge` (only after RE1/RE2 approval + the live approval re-read below)
|
|
53
|
+
- `gh pr view`, `gh pr checks` (live, by number)
|
|
54
|
+
- `gh pr list` / `gh issue list` — **fallback only**, when GITHUB.md is absent or stale (see GitHub State below)
|
|
54
55
|
- Read any file in the workspace
|
|
55
56
|
|
|
57
|
+
## GitHub State (discovery)
|
|
58
|
+
Discover the board — which issues/PRs exist, their state, and review status — by reading the server-authored file, NOT `gh pr list`/`gh issue list`:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
~/.quadwork/{{project_name}}/GITHUB.md
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(or `GET http://127.0.0.1:8400/api/github-parsed?project={{project_name}}` for the same data as JSON). The server regenerates it from live GitHub state every poll cycle.
|
|
65
|
+
|
|
66
|
+
**Discovery is never authoritative for an action:**
|
|
67
|
+
- **Before EVERY merge**, re-confirm approvals with a live read — `gh pr view <n> --json reviewDecision,reviews` — AND keep the chat-derived two-reviewer gate (read chat for both RE1 and RE2 approval messages for the current commit). Both are required. Never merge off the file's `## Review Detail` (it is ADVISORY only). Branch protection is the server-side backstop.
|
|
68
|
+
- **Before `gh issue create`**, run a live duplicate-issue check (`gh issue list`/search for the same scope) — never create off the file.
|
|
69
|
+
- **By-number / live fallback:** when handed a specific number, act on it directly via a single-object `gh` call — never gate its existence on the file. If expected work is missing from GITHUB.md, or the file is stale (`_stale` true / older than ~2 cycles), do a targeted `gh pr view <n>` / `gh issue view <n>` (or one `gh pr list`) — **never conclude "no work" from a stale or empty file.**
|
|
70
|
+
|
|
56
71
|
## Forbidden Actions
|
|
57
72
|
- **NO coding** — do not create, edit, or write code files
|
|
58
73
|
- **NO branch creation** — Dev creates branches
|
|
@@ -76,7 +91,7 @@ This is an **absolute path** — read it with the full path, never a relative on
|
|
|
76
91
|
|
|
77
92
|
### Operator → Head flow
|
|
78
93
|
When the operator asks you in chat to start a task or batch:
|
|
79
|
-
1. Create the GitHub issue(s) if they don't already exist (`gh issue create` with scope, acceptance, and `agent/*` labels
|
|
94
|
+
1. Create the GitHub issue(s) if they don't already exist — run a **live** duplicate check first (`gh issue list`/search; do not rely on GITHUB.md), then `gh issue create` with scope, acceptance, and `agent/*` labels.
|
|
80
95
|
2. Append the task(s) under the **Backlog** section of `OVERNIGHT-QUEUE.md`, or move them into **Active Batch** if the operator says they're ready to run.
|
|
81
96
|
|
|
82
97
|
**Batch numbering.** Each new batch you put into Active Batch gets the next sequential number. Read every `**Batch:** N` line in the file (Active Batch + Done) and use `max(N) + 1`. If no batches exist yet, start at `1`. Stamp the Active Batch section with:
|
|
@@ -116,8 +131,8 @@ When the operator asks you in chat to start a task or batch:
|
|
|
116
131
|
## Workflow
|
|
117
132
|
1. Receive task request (from the operator in chat, or as the next item in `OVERNIGHT-QUEUE.md`) → create GitHub issue if needed.
|
|
118
133
|
2. @dev to assign implementation — then **wait silently**. Do NOT route to reviewers; Dev handles that.
|
|
119
|
-
3. Wait for Dev to confirm reviewers approved. Before merging, verify by reading the chat history for **both** RE1 and RE2 approval messages for this PR. Do NOT rely solely on Dev's claim.
|
|
120
|
-
4.
|
|
134
|
+
3. Wait for Dev to confirm reviewers approved. Before merging, verify by reading the chat history for **both** RE1 and RE2 approval messages for this PR's current commit. Do NOT rely solely on Dev's claim, and do NOT rely on GITHUB.md's `## Review Detail` (advisory only).
|
|
135
|
+
4. **Immediately before merging, re-confirm approvals live**: `gh pr view <number> --json reviewDecision,reviews` (in addition to the chat two-reviewer gate in step 3). Then merge: `gh pr merge <number> --merge`.
|
|
121
136
|
5. Update `OVERNIGHT-QUEUE.md` (move the item from Active Batch to Done) and update the issue status.
|
|
122
137
|
|
|
123
138
|
## Communication
|
|
@@ -51,6 +51,19 @@ The project's task queue lives at the absolute path:
|
|
|
51
51
|
|
|
52
52
|
Head owns this file — do not edit it. Read it when you need context on the batch the PR under review belongs to.
|
|
53
53
|
|
|
54
|
+
## GitHub State (discovery)
|
|
55
|
+
Discover **which open PRs are yours to review** — those you were @mentioned on — by reading the server-authored file instead of `gh pr list`/`gh issue list`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
~/.quadwork/{{project_name}}/GITHUB.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(or `GET http://127.0.0.1:8400/api/github-parsed?project={{project_name}}` for JSON — `## Open PRs` lists open PRs). The server regenerates it from live GitHub each poll cycle.
|
|
62
|
+
|
|
63
|
+
**The review itself is ALWAYS live — never review off the file:**
|
|
64
|
+
- Read the code with live `gh pr diff <n>` / `gh pr view <n>`, and CI with live `gh pr checks <n>`. **Never APPROVE off cached CI or cached code** — the file's status can lag.
|
|
65
|
+
- **By-number fallback:** you are bound to a PR by a chat @mention carrying its number — act on that number directly; never gate its existence on the file. If the @mentioned PR is missing from GITHUB.md, or the file is stale (`_stale` true / older than ~2 cycles), just `gh pr view <n>` / `gh pr diff <n>` — **never conclude "nothing to review" from a stale or empty file.**
|
|
66
|
+
|
|
54
67
|
## Role
|
|
55
68
|
- Review pull requests for correctness, design, and code quality
|
|
56
69
|
- Post structured PR reviews via `gh pr review`
|
|
@@ -58,9 +71,10 @@ Head owns this file — do not edit it. Read it when you need context on the bat
|
|
|
58
71
|
- You have VETO authority on design decisions
|
|
59
72
|
|
|
60
73
|
## Allowed Actions
|
|
61
|
-
- `gh pr view`, `gh pr diff`, `gh pr checks`
|
|
74
|
+
- `gh pr view`, `gh pr diff`, `gh pr checks` — **always live** (review off live code + CI, never cached)
|
|
62
75
|
- `gh pr review --approve`, `gh pr review --request-changes`, `gh pr review --comment`
|
|
63
|
-
- `gh issue view
|
|
76
|
+
- `gh issue view` (live, by number)
|
|
77
|
+
- `gh issue list` — **fallback only**, when GITHUB.md is absent or stale (see GitHub State above)
|
|
64
78
|
- Read any file in the workspace
|
|
65
79
|
|
|
66
80
|
## GitHub Authentication
|
|
@@ -118,7 +132,7 @@ Reference `DESIGN-GUIDE.md` in the workspace for full details on each rule.
|
|
|
118
132
|
|
|
119
133
|
## Workflow
|
|
120
134
|
1. Receive review request from Dev with PR number
|
|
121
|
-
2. Read the PR: `gh pr view <number>`, `gh pr diff <number>`
|
|
135
|
+
2. Read the PR live: `gh pr view <number>`, `gh pr diff <number>`, and CI via `gh pr checks <number>` — review off live code + CI, never GITHUB.md's cached status
|
|
122
136
|
3. Read related issue: `gh issue view <number>`
|
|
123
137
|
4. Review code against checklist
|
|
124
138
|
5. Post review: `gh pr review <number> --approve/--request-changes --body "..."`
|
|
@@ -51,6 +51,19 @@ The project's task queue lives at the absolute path:
|
|
|
51
51
|
|
|
52
52
|
Head owns this file — do not edit it. Read it when you need context on the batch the PR under review belongs to.
|
|
53
53
|
|
|
54
|
+
## GitHub State (discovery)
|
|
55
|
+
Discover **which open PRs are yours to review** — those you were @mentioned on — by reading the server-authored file instead of `gh pr list`/`gh issue list`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
~/.quadwork/{{project_name}}/GITHUB.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(or `GET http://127.0.0.1:8400/api/github-parsed?project={{project_name}}` for JSON — `## Open PRs` lists open PRs). The server regenerates it from live GitHub each poll cycle.
|
|
62
|
+
|
|
63
|
+
**The review itself is ALWAYS live — never review off the file:**
|
|
64
|
+
- Read the code with live `gh pr diff <n>` / `gh pr view <n>`, and CI with live `gh pr checks <n>`. **Never APPROVE off cached CI or cached code** — the file's status can lag.
|
|
65
|
+
- **By-number fallback:** you are bound to a PR by a chat @mention carrying its number — act on that number directly; never gate its existence on the file. If the @mentioned PR is missing from GITHUB.md, or the file is stale (`_stale` true / older than ~2 cycles), just `gh pr view <n>` / `gh pr diff <n>` — **never conclude "nothing to review" from a stale or empty file.**
|
|
66
|
+
|
|
54
67
|
## Role
|
|
55
68
|
- Review pull requests for correctness, design, and code quality
|
|
56
69
|
- Post structured PR reviews via `gh pr review`
|
|
@@ -58,9 +71,10 @@ Head owns this file — do not edit it. Read it when you need context on the bat
|
|
|
58
71
|
- You have VETO authority on design decisions
|
|
59
72
|
|
|
60
73
|
## Allowed Actions
|
|
61
|
-
- `gh pr view`, `gh pr diff`, `gh pr checks`
|
|
74
|
+
- `gh pr view`, `gh pr diff`, `gh pr checks` — **always live** (review off live code + CI, never cached)
|
|
62
75
|
- `gh pr review --approve`, `gh pr review --request-changes`, `gh pr review --comment`
|
|
63
|
-
- `gh issue view
|
|
76
|
+
- `gh issue view` (live, by number)
|
|
77
|
+
- `gh issue list` — **fallback only**, when GITHUB.md is absent or stale (see GitHub State above)
|
|
64
78
|
- Read any file in the workspace
|
|
65
79
|
|
|
66
80
|
## GitHub Authentication
|
|
@@ -118,7 +132,7 @@ Reference `DESIGN-GUIDE.md` in the workspace for full details on each rule.
|
|
|
118
132
|
|
|
119
133
|
## Workflow
|
|
120
134
|
1. Receive review request from Dev with PR number
|
|
121
|
-
2. Read the PR: `gh pr view <number>`, `gh pr diff <number>`
|
|
135
|
+
2. Read the PR live: `gh pr view <number>`, `gh pr diff <number>`, and CI via `gh pr checks <number>` — review off live code + CI, never GITHUB.md's cached status
|
|
122
136
|
3. Read related issue: `gh issue view <number>`
|
|
123
137
|
4. Review code against checklist
|
|
124
138
|
5. Post review: `gh pr review <number> --approve/--request-changes --body "..."`
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,76234,29198,e=>{"use strict";var t=e.i(43476),a=e.i(71645);function r({children:e,position:s="below"}){let[n,l]=(0,a.useState)(!1),o=(0,a.useRef)(null);return(0,a.useEffect)(()=>{if(!n)return;let e=e=>{o.current&&!o.current.contains(e.target)&&l(!1)},t=e=>{"Escape"===e.key&&l(!1)};return document.addEventListener("mousedown",e),document.addEventListener("keydown",t),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("keydown",t)}},[n]),(0,t.jsxs)("div",{ref:o,className:"relative inline-flex items-center",children:[(0,t.jsx)("button",{type:"button",onClick:()=>l(e=>!e),className:"w-3.5 h-3.5 rounded-full border border-border text-[9px] leading-none text-text-muted hover:text-accent hover:border-accent inline-flex items-center justify-center",children:"?"}),n&&(0,t.jsx)("div",{className:`absolute left-0 z-30 w-64 p-2 text-[10px] leading-snug text-text bg-bg-surface border border-border rounded shadow-lg ${"above"===s?"bottom-5":"top-5"}`,children:e})]})}e.s(["default",0,r],29198);var s=e.i(52368);let n={en:{title:"Agent Models",loading:"Loading…",noAgents:"No agents configured.",restartRequired:"restart required",restartRequiredTooltip:"Config changed — running session is still on the old model/effort. Click Restart to apply.",default:"(default)",custom:"(custom)",restart:"Restart",restartTooltip:"Restart this agent to pick up the new model / reasoning setting",help:(0,t.jsxs)(t.Fragment,{children:["Codex reasoning effort defaults to ",(0,t.jsx)("code",{className:"text-text",children:"medium"})," for new projects. Blank model falls back to the CLI default. Click Restart to apply changes to a live session."]}),summary:(e,a)=>(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"text-text",children:e}),(0,t.jsxs)("span",{children:[": ",a]})]}),configure:"Configure →",tooltip:(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("b",{children:"Agent Models"})," — configure which LLM model and reasoning effort each agent uses. Changes require an agent restart to take effect."]})},ko:{title:"에이전트 모델",loading:"로딩 중…",noAgents:"설정된 에이전트가 없습니다.",restartRequired:"재시작 필요",restartRequiredTooltip:"설정이 변경되었습니다. 실행 중인 세션에는 이전 설정이 적용되어 있습니다. 재시작을 클릭하여 적용하세요.",default:"(기본값)",custom:"(사용자 정의)",restart:"재시작",restartTooltip:"에이전트를 재시작하여 새로운 모델/추론 설정을 적용합니다",help:(0,t.jsxs)(t.Fragment,{children:["Codex 추론 수준은 새 프로젝트의 경우 ",(0,t.jsx)("code",{className:"text-text",children:"medium"}),"으로 기본 설정됩니다. 모델을 비워두면 CLI 기본값이 사용됩니다. 변경 사항을 적용하려면 재시작을 클릭하세요."]}),summary:(e,a)=>(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"text-text",children:e}),(0,t.jsxs)("span",{children:[": ",a]})]}),configure:"설정 →",tooltip:(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("b",{children:"에이전트 모델"})," - 각 에이전트가 어떤 LLM 모델과 추론 수준을 사용할지 설정합니다. 변경 사항은 에이전트를 재시작해야 적용됩니다."]})}},l=["minimal","low","medium","high"],o={codex:[{value:"",label:"(CLI default)"},{value:"gpt-5.4",label:"gpt-5.4"},{value:"gpt-5",label:"gpt-5"},{value:"gpt-4o",label:"gpt-4o"}],claude:[{value:"",label:"(CLI default)"},{value:"claude-opus-4-7",label:"claude-opus-4-7"},{value:"claude-opus-4-6",label:"claude-opus-4-6"},{value:"claude-sonnet-4-6",label:"claude-sonnet-4-6"},{value:"claude-haiku-4-5-20251001",label:"claude-haiku-4-5"}],gemini:[{value:"",label:"(CLI default)"},{value:"gemini-2.5-pro",label:"gemini-2.5-pro"},{value:"gemini-2.5-flash",label:"gemini-2.5-flash"}]};function d(e){return o[e]||[{value:"",label:"(CLI default)"}]}function c({projectId:e,onClose:r}){let{locale:o}=(0,s.useLocale)(),i=n[o],[u,x]=(0,a.useState)(null),[p,m]=(0,a.useState)(null),[b,h]=(0,a.useState)(null),[g,f]=(0,a.useState)(new Set),v=(0,a.useCallback)(async()=>{try{let t=await fetch(`/api/project/${encodeURIComponent(e)}/agent-models`);if(!t.ok)throw Error(`${t.status}`);let a=await t.json();x(a.agents||[]),m(null)}catch(e){m(e.message)}},[e]);(0,a.useEffect)(()=>{v()},[v]),(0,a.useEffect)(()=>{let e=e=>{"Escape"===e.key&&r()};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[r]);let j=async(t,a)=>{h(t),m(null);try{let r=await fetch(`/api/project/${encodeURIComponent(e)}/agent-models/${t}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)}),s=await r.json();if(!r.ok||!1===s.ok)throw Error(s.error||`${r.status}`);f(e=>{let a=new Set(e);return a.add(t),a}),await v()}catch(e){m(e.message)}finally{h(null)}},N=async t=>{h(t),m(null);try{let a=await fetch(`/api/agents/${encodeURIComponent(e)}/${encodeURIComponent(t)}/restart`,{method:"POST"}),r=await a.json();if(!a.ok||!1===r.ok)throw Error(r.error||`${a.status}`);f(e=>{let a=new Set(e);return a.delete(t),a})}catch(e){m(e.message)}finally{h(null)}};return(0,t.jsx)("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:r,role:"dialog","aria-modal":"true","aria-labelledby":"agent-models-title",children:(0,t.jsxs)("div",{className:"relative mx-4 w-full max-w-[520px] max-h-[90vh] overflow-auto rounded-lg border border-white/10 bg-neutral-950 p-6 shadow-2xl",onClick:e=>e.stopPropagation(),children:[(0,t.jsx)("button",{type:"button",onClick:r,"aria-label":"Close",className:"absolute right-3 top-3 rounded p-1 text-neutral-400 hover:bg-white/5 hover:text-white",children:(0,t.jsx)("svg",{width:"18",height:"18",viewBox:"0 0 20 20",fill:"none",stroke:"currentColor",strokeWidth:"1.8",children:(0,t.jsx)("path",{d:"M4 4l12 12M16 4L4 16",strokeLinecap:"round"})})}),(0,t.jsxs)("div",{className:"flex items-center justify-between mb-3",children:[(0,t.jsx)("h2",{id:"agent-models-title",className:"text-base font-semibold text-white",children:i.title}),p&&(0,t.jsxs)("span",{className:"text-[10px] text-error max-w-[60%] truncate ml-2",title:p,children:["err: ",p]})]}),(0,t.jsxs)("div",{className:"flex flex-col gap-1.5",children:[!u&&(0,t.jsx)("div",{className:"text-[11px] text-text-muted",children:i.loading}),u&&0===u.length&&(0,t.jsx)("div",{className:"text-[11px] text-text-muted",children:i.noAgents}),u&&u.map(e=>(0,t.jsxs)("div",{className:"flex items-center gap-1.5 flex-wrap",children:[(0,t.jsx)("span",{className:"text-[11px] text-text font-semibold w-16 shrink-0",children:e.agent_id}),(0,t.jsx)("span",{className:"text-[10px] text-text-muted w-12 shrink-0",children:e.backend}),g.has(e.agent_id)&&(0,t.jsx)("span",{className:"text-[9px] text-[#ffcc00] border border-[#ffcc00]/40 px-1 py-[1px] shrink-0",title:i.restartRequiredTooltip,children:i.restartRequired}),(0,t.jsxs)("select",{value:e.model,disabled:b===e.agent_id,onChange:t=>j(e.agent_id,{model:t.target.value}),className:"flex-1 min-w-[140px] bg-transparent border border-border px-1 py-0.5 text-[11px] font-mono text-text outline-none focus:border-accent cursor-pointer disabled:opacity-50",children:[d(e.backend).map(e=>(0,t.jsx)("option",{value:e.value,className:"bg-bg-surface",children:e.label},e.value)),e.model&&!d(e.backend).some(t=>t.value===e.model)&&(0,t.jsxs)("option",{value:e.model,className:"bg-bg-surface",children:[e.model," ",i.custom]})]}),e.reasoning_supported?(0,t.jsxs)("select",{value:e.reasoning_effort||"",disabled:b===e.agent_id,onChange:t=>j(e.agent_id,{reasoning_effort:t.target.value}),className:"bg-transparent border border-border px-1 py-0.5 text-[11px] text-text outline-none focus:border-accent cursor-pointer disabled:opacity-50",children:[(0,t.jsx)("option",{value:"",className:"bg-bg-surface",children:i.default}),l.map(e=>(0,t.jsx)("option",{value:e,className:"bg-bg-surface",children:e},e))]}):(0,t.jsx)("span",{className:"text-[10px] text-text-muted w-16 text-center",children:"—"}),(0,t.jsx)("button",{type:"button",onClick:()=>N(e.agent_id),disabled:b===e.agent_id,title:i.restartTooltip,className:"shrink-0 px-1.5 py-0.5 text-[10px] text-text-muted border border-border hover:text-accent hover:border-accent/40 disabled:opacity-50 transition-colors",children:b===e.agent_id?"…":i.restart})]},e.agent_id)),(0,t.jsx)("p",{className:"mt-2 text-[10px] text-text-muted leading-snug",children:i.help})]})]})})}e.s(["default",0,function({projectId:e}){let{locale:l}=(0,s.useLocale)(),o=n[l],[d,i]=(0,a.useState)(!1),[u,x]=(0,a.useState)(null);return(0,a.useEffect)(()=>{let t=!1;return fetch(`/api/project/${encodeURIComponent(e)}/agent-models`).then(e=>e.ok?e.json():null).then(e=>{t||!e||x((e.agents||[]).map(e=>({id:e.agent_id,backend:e.backend})))}).catch(()=>{}),()=>{t=!0}},[e]),(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{className:"flex flex-col border border-border",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between h-7 px-3 shrink-0 border-b border-border",children:[(0,t.jsxs)("div",{className:"flex items-center gap-1.5",children:[(0,t.jsx)("span",{className:"text-[11px] text-text-muted uppercase tracking-wider",children:o.title}),(0,t.jsx)(r,{children:o.tooltip})]}),(0,t.jsx)("button",{type:"button",onClick:()=>i(!0),className:"px-2 py-0.5 text-[10px] text-text-muted border border-border hover:text-accent hover:border-accent/40 transition-colors",children:o.configure})]}),u&&u.length>0&&(0,t.jsx)("div",{className:"px-3 py-1 text-[10px] text-text-muted truncate",title:u.map(e=>`${e.id}: ${e.backend}`).join(" · "),children:u.map((e,a)=>(0,t.jsxs)("span",{children:[a>0&&(0,t.jsx)("span",{className:"text-text-muted/60",children:" · "}),o.summary(e.id,e.backend)]},e.id))})]}),d&&(0,t.jsx)(c,{projectId:e,onClose:()=>i(!1)})]})},"optionsForBackend",0,d],76234)},95773,e=>{"use strict";var t=e.i(43476),a=e.i(71645),r=e.i(18566),s=e.i(52368),n=e.i(76234);let l={head:{display_name:"Head",command:"claude",cwd:"",model:"opus",agents_md:""},re1:{display_name:"RE1",command:"claude",cwd:"",model:"sonnet",agents_md:""},re2:{display_name:"RE2",command:"claude",cwd:"",model:"sonnet",agents_md:""},dev:{display_name:"Dev",command:"claude",cwd:"",model:"sonnet",agents_md:""}},o=[{value:"claude",label:"Claude Code"},{value:"codex",label:"Codex"},{value:"gemini",label:"Gemini CLI"}],d=["opus","sonnet","haiku"];function c(e){let t=(0,n.optionsForBackend)(e).filter(e=>""!==e.value);return t.length>0?t:(0,n.optionsForBackend)("claude").filter(e=>""!==e.value)}let i={en:{loading:"Loading...",title:"Settings",save:"Save",saving:"Saving...",saved:"Saved",operatorIdentity:"Operator Identity",yourNameInChat:"Your name in chat",language:"Language",operatorHelp:"Shows next to your messages in the project chat panel. Defaults to user if blank. Allowed: 1-32 letters, digits, dash, underscore (other characters are stripped server-side). Reserved agent names like head, dev, re1, re2, and system are rejected and fall back to user.",global:"Global",dashboardPort:"QuadWork Dashboard Port",globalHelp:"The dashboard binds to the QuadWork port.",defaults:"Defaults",defaultAgentCli:"Default agent CLI",reviewerGithubUser:"Reviewer GitHub user",reviewerGithubToken:"Reviewer GitHub token",configured:"Configured",notConfigured:"Not configured",pasteNewToken:"Paste new token",defaultsHelp:"The default CLI seeds new project agents. The reviewer GitHub user/token are used by RE1/RE2 to post PR review comments without your personal token. The token is written to ~/.quadwork/reviewer-token (mode 0600) and is never returned by the API.",system:"System",keepAwake:"Keep Awake",on:"on",off:"off",stop:"Stop",start:"Start",keepAwakeHelp:"Prevents this machine from sleeping while agents are running. Machine-level (not per-project) - uses caffeinate on macOS.",cleanup:"Cleanup",cleanupIntro:"Remove legacy AgentChattr files left over from pre-v2 installs:",cleanupSingle:"To remove a single project's clone and config entry:",cleanupHelp:"Both commands prompt for confirmation. Worktrees and source repos are never touched. See npx quadwork --help or the README's Disk Usage section for details.",activeProjects:"Active Projects",projectName:"Project Name",githubRepo:"GitHub Repo",workingDirectory:"Working Directory",agents:"Agents",name:"Name",command:"Command",model:"Model",cwd:"CWD",agentsMd:"AGENTS.md",owner:"Owner",reviewer:"Reviewer",builder:"Builder",edit:"edit",oneCliInstalled:"Only one CLI installed - install the other for more options",agentsMdPlaceholder:"# AGENTS.md seed content for this agent...",restoreProject:"Restore Project",archive:"Archive",remove:"Remove",removeQuestion:"Remove?",confirm:"Confirm",cancel:"Cancel",addProject:"+ Add Project",archived:"Archived",restore:"Restore",confirmRemove:"Confirm Remove",newProject:"New Project",butlerAgent:"Butler Agent",butlerEnabled:"Enabled",butlerDisabled:"Disabled",butlerCli:"CLI",butlerModel:"Model",butlerAutoStart:"Auto-start on boot",butlerCwd:"Working directory",butlerHelp:"Butler is a cross-project operator assistant that runs in ~/docs/. It helps manage tickets, proposals, reviews, and releases across all projects.",enable:"Enable",disable:"Disable",unsavedChanges:"Unsaved changes",butlerRestartHint:"Butler is running with previous settings. Disable and re-enable to apply changes."},ko:{loading:"로딩 중...",title:"설정",save:"저장",saving:"저장 중...",saved:"저장됨",operatorIdentity:"운영자 정보",yourNameInChat:"채팅에서의 이름",language:"언어",operatorHelp:"프로젝트 채팅 패널에서 내 메시지 옆에 표시됩니다. 비워두면 기본값은 user입니다. 허용: 1-32자의 영문, 숫자, 하이픈, 언더스코어. 다른 문자는 서버에서 제거됩니다. head, dev, re1, re2, system 같은 예약 이름은 거부되고 user로 대체됩니다.",global:"전역",dashboardPort:"QuadWork 대시보드 포트",globalHelp:"대시보드는 QuadWork 포트에 바인딩됩니다.",defaults:"기본값",defaultAgentCli:"기본 에이전트 CLI",reviewerGithubUser:"리뷰어 GitHub 사용자",reviewerGithubToken:"리뷰어 GitHub 토큰",configured:"설정됨",notConfigured:"미설정",pasteNewToken:"새 토큰 붙여넣기",defaultsHelp:"기본 CLI는 새 프로젝트 에이전트의 초기값으로 사용됩니다. 리뷰어 GitHub 사용자/토큰은 개인 토큰 없이 RE1/RE2가 PR 리뷰 댓글을 남길 때 사용됩니다. 토큰은 ~/.quadwork/reviewer-token (권한 0600)에 저장되며 API로는 반환되지 않습니다.",system:"시스템",keepAwake:"절전 방지",on:"켜짐",off:"꺼짐",stop:"중지",start:"시작",keepAwakeHelp:"에이전트가 실행되는 동안 이 기기가 잠들지 않도록 합니다. 기기 전체 설정이며(프로젝트별 아님) macOS에서는 caffeinate를 사용합니다.",cleanup:"정리",cleanupIntro:"v2 이전 설치에서 남은 레거시 AgentChattr 파일을 제거합니다:",cleanupSingle:"특정 프로젝트의 클론과 설정 항목만 제거하려면:",cleanupHelp:"두 명령 모두 확인 절차가 있습니다. 워크트리와 소스 저장소는 건드리지 않습니다. 자세한 내용은 npx quadwork --help 또는 README의 Disk Usage 섹션을 참고하세요.",activeProjects:"활성 프로젝트",projectName:"프로젝트 이름",githubRepo:"GitHub 저장소",workingDirectory:"작업 디렉터리",agents:"에이전트",name:"이름",command:"명령어",model:"모델",cwd:"작업 디렉터리",agentsMd:"AGENTS.md",owner:"소유자",reviewer:"검토자",builder:"개발자",edit:"편집",oneCliInstalled:"CLI 하나만 설치됨 - 더 많은 옵션을 위해 다른 CLI를 설치하세요",agentsMdPlaceholder:"# 이 에이전트의 AGENTS.md 초기 내용...",restoreProject:"프로젝트 복원",archive:"보관",remove:"제거",removeQuestion:"제거할까요?",confirm:"확인",cancel:"취소",addProject:"+ 프로젝트 추가",archived:"보관됨",restore:"복원",confirmRemove:"제거 확인",newProject:"새 프로젝트",butlerAgent:"버틀러 에이전트",butlerEnabled:"활성",butlerDisabled:"비활성",butlerCli:"CLI",butlerModel:"모델",butlerAutoStart:"서버 시작 시 자동 실행",butlerCwd:"작업 디렉터리",butlerHelp:"버틀러는 ~/docs/에서 실행되는 크로스 프로젝트 운영자 어시스턴트입니다. 모든 프로젝트의 티켓, 제안서, 리뷰, 릴리스 관리를 지원합니다.",enable:"활성화",disable:"비활성화",unsavedChanges:"저장되지 않은 변경사항",butlerRestartHint:"버틀러가 이전 설정으로 실행 중입니다. 변경사항을 적용하려면 비활성화 후 다시 활성화하세요."}};function u({label:e,value:a,onChange:r,onBlur:s,type:n="text",placeholder:l}){return(0,t.jsxs)("div",{className:"flex flex-col gap-1",children:[(0,t.jsx)("label",{className:"text-[11px] text-text-muted uppercase tracking-wider",children:e}),(0,t.jsx)("input",{type:n,value:a,onChange:e=>r(e.target.value),onBlur:s,placeholder:l,className:"bg-transparent border border-border px-2 py-1.5 text-[12px] text-text outline-none focus:border-accent"})]})}function x({label:e,value:a,onChange:r,options:s}){return(0,t.jsxs)("div",{className:"flex flex-col gap-1",children:[(0,t.jsx)("label",{className:"text-[11px] text-text-muted uppercase tracking-wider",children:e}),(0,t.jsx)("select",{value:a,onChange:e=>r(e.target.value),className:"bg-transparent border border-border px-2 py-1.5 text-[12px] text-text outline-none focus:border-accent cursor-pointer",children:s.map(e=>(0,t.jsx)("option",{value:e.value,className:"bg-bg-surface",children:e.label},e.value))})]})}e.s(["default",0,function(){let{locale:e,setLocale:n}=(0,s.useLocale)(),p=i[e],m=(0,r.useSearchParams)(),[b,h]=(0,a.useState)(null),[g,f]=(0,a.useState)(!1),[v,j]=(0,a.useState)(!1),N=(0,a.useRef)(""),[w,k]=(0,a.useState)(null),[y,C]=(0,a.useState)({}),[S,R]=(0,a.useState)(null),[_,T]=(0,a.useState)(!1),[I,E]=(0,a.useState)(null),[P,A]=(0,a.useState)("8400"),L=(0,a.useCallback)(()=>{fetch("/api/config").then(e=>{if(!e.ok)throw Error(`${e.status}`);return e.json()}).then(e=>{A(String(e.port||8400));let t={port:e.port||8400,default_backend:e.default_backend||"claude",reviewer_github_user:e.reviewer_github_user||"",operator_name:e.operator_name||"user",projects:e.projects||[],butler:e.butler||{}};return N.current=JSON.stringify(t),h(t)}).catch(()=>{})},[]);(0,a.useEffect)(()=>{L()},[L]),(0,a.useEffect)(()=>{fetch("/api/cli-status").then(e=>e.json()).then(e=>E(e)).catch(()=>{})},[]);let[$,H]=(0,a.useState)(null),[O,D]=(0,a.useState)(""),[G,M]=(0,a.useState)(!1),[U,B]=(0,a.useState)(!1),[q,F]=(0,a.useState)(!1),[W,J]=(0,a.useState)(!1),[Q,z]=(0,a.useState)(!1),K=(0,a.useCallback)(()=>{fetch("/api/setup/reviewer-token-status").then(e=>e.ok?e.json():{exists:!1}).then(e=>H(!!e.exists)).catch(()=>H(!1))},[]),V=(0,a.useCallback)(()=>{fetch("/api/caffeinate/status").then(e=>e.ok?e.json():{active:!1}).then(e=>B(!!e.active)).catch(()=>{})},[]),Y=(0,a.useCallback)(()=>{fetch("/api/butler/status").then(e=>e.ok?e.json():{running:!1}).then(e=>{J(!!e.running),e.running&&e.command&&k({command:e.command,model:e.model||""}),e.running||k(null)}).catch(()=>{})},[]);(0,a.useEffect)(()=>{K(),V(),Y()},[K,V,Y]);let X=async()=>{if(O.trim()){M(!0);try{(await fetch("/api/setup/save-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:O.trim()})})).ok&&(D(""),K())}finally{M(!1)}}},Z=async()=>{F(!0);try{(await fetch(U?"/api/caffeinate/stop":"/api/caffeinate/start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})})).ok&&V()}finally{F(!1)}},ee=e=>{b&&h({...b,butler:{...b.butler,...e}})},et=async()=>{z(!0);try{let e=await fetch(W?"/api/butler/stop":"/api/butler/start",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});if(e.ok){let t=await e.json();(W||t.ok)&&(W&&k(null),Y(),ee({enabled:!W}))}}finally{z(!1)}};(0,a.useEffect)(()=>{b&&"true"===m.get("add")&&!_&&(T(!0),el())},[b,m,_]),(0,a.useEffect)(()=>{if(!b)return;let e=window.location.hash.replace("#","");if(e){let t=document.getElementById(e);t&&t.scrollIntoView({behavior:"smooth"})}},[b]);let ea=async()=>{if(b){f(!0);try{let e=await fetch("/api/config",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(b)});if(!e.ok)throw Error(`${e.status}`);N.current=JSON.stringify(b),j(!0),setTimeout(()=>j(!1),2e3)}catch(e){console.error(e)}f(!1)}},er=(e,t)=>{b&&h({...b,[e]:t})},es=(e,t)=>{if(!b)return;let a=[...b.projects];a[e]={...a[e],...t},h({...b,projects:a})},en=(e,t,a)=>{if(!b)return;let r=[...b.projects],s={...r[e].agents};s[t]={...s[t],...a},r[e]={...r[e],agents:s},h({...b,projects:r})},el=()=>{if(!b)return;let e=`project-${Date.now()}`,t=b.default_backend||"claude",a=I&&!1===I[t]?I&&I.claude&&!I.codex?"claude":I&&!I.claude&&I.codex?"codex":"claude":t,r={};for(let[e,t]of Object.entries(l))r[e]={...t,command:a};let s={id:e,name:p.newProject,repo:"owner/repo",working_dir:"",agents:r};h({...b,projects:[...b.projects,s]}),C({...y,[e]:!0})},eo=(0,a.useRef)({}),ed=(0,a.useRef)({}),ec=e=>{b&&es(e,{archived:!1})},ei=e=>{if(!b)return;let t=b.projects.filter((t,a)=>a!==e);h({...b,projects:t}),R(null)};if(!b)return(0,t.jsx)("div",{className:"p-6 text-text-muted text-xs",children:p.loading});let eu=""!==N.current&&JSON.stringify(b)!==N.current,ex=W&&null!=w&&((b.butler?.command||"claude")!==w.command||(b.butler?.model||"")!==w.model);return(0,t.jsxs)("div",{className:"h-full w-full overflow-y-auto p-6",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-6",children:[(0,t.jsx)("h1",{className:"text-lg font-semibold text-text tracking-tight",children:p.title}),(0,t.jsx)("button",{onClick:ea,disabled:g,className:"px-4 py-1.5 bg-accent text-bg text-[12px] font-semibold hover:bg-accent-dim transition-colors disabled:opacity-50",children:g?p.saving:v?p.saved:p.save})]}),(0,t.jsxs)("section",{className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.operatorIdentity}),(0,t.jsxs)("div",{className:"grid grid-cols-1 md:grid-cols-[minmax(0,2fr)_minmax(220px,1fr)] gap-3 items-end",children:[(0,t.jsx)(u,{label:p.yourNameInChat,value:b.operator_name||"user",onChange:e=>er("operator_name",e),placeholder:"user"}),(0,t.jsxs)("div",{className:"flex flex-col gap-1",children:[(0,t.jsx)("label",{className:"text-[11px] text-text-muted uppercase tracking-wider",children:p.language}),(0,t.jsx)("div",{className:"flex items-center gap-2 h-[35px]",children:["en","ko"].map(a=>{let r=e===a;return(0,t.jsx)("button",{type:"button",onClick:()=>n(a),className:`px-3 py-1.5 text-[12px] border transition-colors ${r?"border-accent bg-accent text-bg":"border-border text-text-muted hover:text-text hover:border-accent"}`,children:a},a)})})]})]}),(0,t.jsx)("p",{className:"mt-2 text-[10px] text-text-muted leading-snug",children:p.operatorHelp})]}),(0,t.jsxs)("section",{className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.global}),(0,t.jsx)("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-3",children:(0,t.jsx)(u,{label:p.dashboardPort,value:P,onChange:e=>A(e),onBlur:()=>{let e=parseInt(P,10),t=Number.isFinite(e)&&e>0&&e<=65535?e:8400;er("port",t),A(String(t))},type:"number"})}),(0,t.jsx)("p",{className:"mt-2 text-[10px] text-text-muted leading-snug",children:p.globalHelp})]}),(0,t.jsxs)("section",{className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.defaults}),(0,t.jsxs)("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-3 items-end",children:[(0,t.jsx)(x,{label:p.defaultAgentCli,value:b.default_backend||"claude",onChange:e=>er("default_backend",e),options:o.map(e=>({value:e.value,label:e.label+(I&&!I[e.value]?" (not installed)":"")}))}),(0,t.jsx)(u,{label:p.reviewerGithubUser,value:b.reviewer_github_user||"",onChange:e=>er("reviewer_github_user",e),placeholder:"reviewer-bot"}),(0,t.jsxs)("div",{className:"flex flex-col gap-1",children:[(0,t.jsx)("label",{className:"text-[11px] text-text-muted uppercase tracking-wider",children:p.reviewerGithubToken}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${$?"bg-accent":"bg-text-muted"}`}),(0,t.jsx)("span",{className:"text-[11px] text-text-muted",children:null===$?"…":$?p.configured:p.notConfigured})]}),(0,t.jsxs)("div",{className:"flex items-center gap-1.5 mt-1",children:[(0,t.jsx)("input",{type:"password",value:O,onChange:e=>D(e.target.value),placeholder:p.pasteNewToken,className:"flex-1 bg-transparent border border-border px-2 py-1 text-[11px] text-text outline-none focus:border-accent font-mono"}),(0,t.jsx)("button",{onClick:X,disabled:G||!O.trim(),className:"px-2 py-1 text-[11px] font-semibold text-bg bg-accent hover:bg-accent-dim disabled:opacity-50 transition-colors",children:G?p.saving:p.save})]})]})]}),(0,t.jsx)("p",{className:"mt-2 text-[10px] text-text-muted leading-snug",children:p.defaultsHelp})]}),(0,t.jsxs)("section",{className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.system}),(0,t.jsxs)("div",{className:"border border-border p-3 flex items-center gap-3",children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${U?"bg-accent":"bg-text-muted"}`}),(0,t.jsxs)("span",{className:"text-[11px] text-text",children:[p.keepAwake," - ",U?p.on:p.off]}),(0,t.jsx)("button",{onClick:Z,disabled:q,className:"px-2 py-1 text-[11px] border border-border text-text-muted hover:text-text hover:border-accent disabled:opacity-50 transition-colors",children:q?"…":U?p.stop:p.start}),(0,t.jsx)("span",{className:"text-[10px] text-text-muted",children:p.keepAwakeHelp})]})]}),(0,t.jsxs)("section",{id:"butler",className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.butlerAgent}),(0,t.jsxs)("div",{className:"border border-border p-3 space-y-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3",children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${W?"bg-accent":"bg-text-muted"}`}),(0,t.jsx)("span",{className:"text-[11px] text-text",children:W?p.butlerEnabled:p.butlerDisabled}),(0,t.jsx)("button",{onClick:et,disabled:Q,className:"px-2 py-1 text-[11px] border border-border text-text-muted hover:text-text hover:border-accent disabled:opacity-50 transition-colors",children:Q?"…":W?p.disable:p.enable})]}),(0,t.jsxs)("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-3",children:[(0,t.jsx)(x,{label:p.butlerCli,value:b.butler?.command||"claude",onChange:e=>{let t=c(e),a=b.butler?.model||"opus";ee({command:e,model:t.some(e=>e.value===a)?a:t[0].value})},options:o.map(e=>({value:e.value,label:e.label+(I&&!I[e.value]?" (not installed)":"")}))}),(0,t.jsx)(x,{label:p.butlerModel,value:b.butler?.model||"opus",onChange:e=>ee({model:e}),options:c(b.butler?.command||"claude")}),(0,t.jsx)(u,{label:p.butlerCwd,value:b.butler?.cwd||"~/docs/",onChange:e=>ee({cwd:e}),placeholder:"~/docs/"})]}),(0,t.jsxs)("label",{className:"flex items-center gap-2 cursor-pointer",children:[(0,t.jsx)("input",{type:"checkbox",checked:b.butler?.auto_start??!1,onChange:e=>ee({auto_start:e.target.checked}),className:"accent-accent"}),(0,t.jsx)("span",{className:"text-[11px] text-text",children:p.butlerAutoStart})]}),(0,t.jsx)("p",{className:"text-[10px] text-text-muted leading-snug",children:p.butlerHelp}),ex&&(0,t.jsxs)("p",{className:"text-[10px] text-yellow-500 leading-snug mt-1",children:["⚠ ",p.butlerRestartHint]})]})]}),(0,t.jsxs)("section",{className:"mb-8",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.cleanup}),(0,t.jsxs)("div",{className:"border border-border p-3 text-[11px] text-text-muted space-y-1",children:[(0,t.jsx)("p",{children:p.cleanupIntro}),(0,t.jsx)("pre",{className:"mt-1 p-2 bg-bg-surface text-text rounded font-mono text-[11px]",children:"npx quadwork cleanup --legacy"}),(0,t.jsx)("p",{className:"mt-2",children:p.cleanupSingle}),(0,t.jsx)("pre",{className:"mt-1 p-2 bg-bg-surface text-text rounded font-mono text-[11px]",children:"npx quadwork cleanup --project <id>"}),(0,t.jsx)("p",{className:"mt-2 text-text-muted/80",children:p.cleanupHelp})]})]}),(0,t.jsx)("hr",{className:"border-border mb-6"}),(0,t.jsxs)("section",{className:"mb-6",children:[(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.activeProjects}),b.projects.filter(e=>!e.archived).map(e=>{let a=b.projects.indexOf(e);return(0,t.jsxs)("div",{className:"border border-border mb-3",children:[(0,t.jsx)("div",{className:"flex items-center justify-between px-3 py-2",children:(0,t.jsx)("span",{className:"text-[12px] text-text font-semibold",children:e.name})}),(0,t.jsxs)("div",{className:"px-3 pb-3 border-t border-border",children:[(0,t.jsxs)("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-3 mt-3",children:[(0,t.jsx)(u,{label:p.projectName,value:e.name,onChange:e=>((e,t)=>{if(!b)return;let a=b.projects[e],r=`project:${a.id}`;r in eo.current||(eo.current[r]=a.name),es(e,{name:t}),ed.current[r]&&clearTimeout(ed.current[r]),ed.current[r]=setTimeout(()=>{let e=eo.current[r];e&&e!==t&&fetch("/api/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"project",projectId:a.id,oldName:e,newName:t})}).then(()=>L()).catch(()=>{}),delete eo.current[r],delete ed.current[r]},800)})(a,e)}),(0,t.jsx)(u,{label:p.githubRepo,value:e.repo,onChange:e=>es(a,{repo:e}),placeholder:"owner/repo"}),(0,t.jsx)(u,{label:p.workingDirectory,value:e.working_dir||"",onChange:e=>es(a,{working_dir:e}),placeholder:"/path/to/project"})]}),(0,t.jsxs)("div",{className:"mt-4",children:[(0,t.jsx)("h3",{className:"text-[10px] text-text-muted uppercase tracking-wider mb-2",children:p.agents}),I&&(I.claude?!I.codex:I.codex)&&(0,t.jsxs)("div",{className:"border border-accent/20 bg-accent/5 p-2 mb-2 text-[10px]",children:[(0,t.jsx)("span",{className:"text-text",children:p.oneCliInstalled}),(0,t.jsx)("code",{className:"text-accent ml-2",children:I.claude?"npm install -g codex":"npm install -g @anthropic-ai/claude-code"})]}),(0,t.jsxs)("div",{className:"border border-border",children:[(0,t.jsxs)("div",{className:"grid grid-cols-5 gap-0 px-2 py-1 border-b border-border text-[10px] text-text-muted uppercase",children:[(0,t.jsx)("span",{children:p.name}),(0,t.jsx)("span",{children:p.command}),(0,t.jsx)("span",{children:p.model}),(0,t.jsx)("span",{children:p.cwd}),(0,t.jsx)("span",{children:p.agentsMd})]}),Object.entries(e.agents||{}).map(([r,s])=>(0,t.jsxs)("div",{className:"border-b border-border/50 last:border-b-0",children:[(0,t.jsxs)("div",{className:"grid grid-cols-5 gap-0 px-2 py-1",children:[(0,t.jsxs)("div",{className:"flex flex-col gap-0.5",children:[(0,t.jsx)("input",{value:s.display_name||r.toUpperCase(),onChange:e=>((e,t,a)=>{if(!b)return;let r=b.projects[e],s=r.agents?.[t],n=`agent:${r.id}:${t}`;n in eo.current||(eo.current[n]=s?.display_name||t.toUpperCase()),en(e,t,{display_name:a}),ed.current[n]&&clearTimeout(ed.current[n]),ed.current[n]=setTimeout(()=>{let e=eo.current[n];e&&e!==a&&fetch("/api/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"agent",projectId:r.id,agentId:t,oldName:e,newName:a})}).then(()=>L()).catch(()=>{}),delete eo.current[n],delete ed.current[n]},800)})(a,r,e.target.value),className:"bg-transparent text-[11px] text-text font-semibold outline-none border border-border px-1 py-0.5 focus:border-accent"}),(0,t.jsx)("span",{className:"text-[9px] text-text-muted px-1",children:"head"===r?p.owner:r.startsWith("reviewer")?p.reviewer:p.builder})]}),(0,t.jsx)("select",{value:s.command||"claude",onChange:e=>en(a,r,{command:e.target.value}),className:"bg-transparent text-[11px] text-text outline-none border border-border px-1 py-0.5 focus:border-accent",title:I&&1===Object.values(I).filter(Boolean).length?p.oneCliInstalled:void 0,children:o.map(e=>(0,t.jsxs)("option",{value:e.value,className:"bg-bg-surface",disabled:!!I&&!I[e.value],children:[e.label,I&&!I[e.value]?" (not installed)":""]},e.value))}),(0,t.jsx)("select",{value:s.model||"sonnet",onChange:e=>en(a,r,{model:e.target.value}),className:"bg-transparent text-[11px] text-text outline-none border border-border px-1 py-0.5 focus:border-accent",children:d.map(e=>(0,t.jsx)("option",{value:e,className:"bg-bg-surface",children:e},e))}),(0,t.jsx)("input",{value:s.cwd||"",onChange:e=>en(a,r,{cwd:e.target.value}),placeholder:"/path/to/worktree",className:"bg-transparent text-[11px] text-text outline-none border border-border px-1 py-0.5 focus:border-accent"}),(0,t.jsx)("button",{onClick:()=>C({...y,[`${e.id}-${r}-md`]:!y[`${e.id}-${r}-md`]}),className:"text-[10px] text-text-muted hover:text-accent transition-colors text-left px-1",children:y[`${e.id}-${r}-md`]?`▾ ${p.edit}`:`▸ ${p.edit}`})]}),y[`${e.id}-${r}-md`]&&(0,t.jsx)("div",{className:"px-2 pb-2",children:(0,t.jsx)("textarea",{value:s.agents_md||"",onChange:e=>en(a,r,{agents_md:e.target.value}),placeholder:p.agentsMdPlaceholder,rows:8,className:"w-full bg-transparent border border-border px-2 py-1.5 text-[11px] text-text outline-none focus:border-accent resize-y"})})]},r))]})]}),(0,t.jsxs)("div",{className:"mt-4 flex justify-end gap-3",children:[e.archived?(0,t.jsx)("button",{onClick:()=>ec(a),className:"text-[11px] text-accent hover:underline",children:p.restoreProject}):(0,t.jsx)("button",{onClick:()=>{b&&es(a,{archived:!0})},className:"text-[11px] text-text-muted hover:text-text transition-colors",children:p.archive}),S===e.id?(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"text-[11px] text-error",children:p.removeQuestion}),(0,t.jsx)("button",{onClick:()=>ei(a),className:"px-2 py-1 text-[11px] bg-error text-bg font-semibold",children:p.confirm}),(0,t.jsx)("button",{onClick:()=>R(null),className:"px-2 py-1 text-[11px] text-text-muted border border-border",children:p.cancel})]}):(0,t.jsx)("button",{onClick:()=>R(e.id),className:"text-[11px] text-error hover:text-text transition-colors",children:p.remove})]})]})]},e.id)}),(0,t.jsx)("button",{onClick:el,className:"w-full border border-dashed border-border py-2 text-[12px] text-text-muted hover:text-text hover:border-text-muted transition-colors",children:p.addProject}),b.projects.some(e=>e.archived)&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("hr",{className:"border-border my-4"}),(0,t.jsx)("h2",{className:"text-[11px] text-text-muted uppercase tracking-wider mb-3",children:p.archived}),b.projects.filter(e=>e.archived).map(e=>{let a=b.projects.indexOf(e);return(0,t.jsx)("div",{className:"border border-border mb-3 opacity-60",children:(0,t.jsxs)("div",{className:"flex items-center justify-between px-3 py-2",children:[(0,t.jsx)("span",{className:"text-[12px] text-text-muted",children:e.name}),(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("button",{onClick:()=>ec(a),className:"text-[11px] text-accent hover:underline",children:p.restore}),(0,t.jsx)("button",{onClick:()=>{S===e.id?ei(a):R(e.id)},className:"text-[11px] text-error hover:underline",children:S===e.id?p.confirmRemove:p.remove})]})]})},e.id)})]})]}),(0,t.jsx)("div",{className:"flex justify-end pb-6",children:(0,t.jsx)("button",{onClick:ea,disabled:g,className:"px-4 py-1.5 bg-accent text-bg text-[12px] font-semibold hover:bg-accent-dim transition-colors disabled:opacity-50",children:g?p.saving:v?p.saved:p.save})}),eu&&(0,t.jsxs)("div",{className:"fixed bottom-0 left-0 right-0 border-t border-border bg-bg-surface px-6 py-3 flex items-center justify-between z-50",children:[(0,t.jsx)("span",{className:"text-[12px] text-text-muted",children:p.unsavedChanges}),(0,t.jsx)("button",{onClick:ea,disabled:g,className:"px-4 py-1.5 bg-accent text-bg text-[12px] font-semibold hover:bg-accent-dim transition-colors disabled:opacity-50",children:g?p.saving:p.save})]})]})}])}]);
|