quadwork 1.4.0 → 1.5.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.
Files changed (91) hide show
  1. package/bin/quadwork.js +82 -0
  2. package/out/404.html +1 -1
  3. package/out/__next.__PAGE__.txt +3 -3
  4. package/out/__next._full.txt +12 -12
  5. package/out/__next._head.txt +4 -4
  6. package/out/__next._index.txt +6 -6
  7. package/out/__next._tree.txt +2 -2
  8. package/out/_next/static/chunks/{18cmux34jwe.p.js → 0-y13tz~pmpno.js} +1 -1
  9. package/out/_next/static/chunks/{0zqyw6q.jp~1i.js → 0.9m84as-sc_r.js} +13 -13
  10. package/out/_next/static/chunks/05.po0c1knrbu.css +2 -0
  11. package/out/_next/static/chunks/084lff9v4p_vh.js +1 -0
  12. package/out/_next/static/chunks/0e.ktwt1nyj...js +1 -0
  13. package/out/_next/static/chunks/{05ok82hwk0x-c.js → 0za4cvk8.n0-y.js} +1 -1
  14. package/out/_not-found/__next._full.txt +11 -11
  15. package/out/_not-found/__next._head.txt +4 -4
  16. package/out/_not-found/__next._index.txt +6 -6
  17. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  18. package/out/_not-found/__next._not-found.txt +3 -3
  19. package/out/_not-found/__next._tree.txt +2 -2
  20. package/out/_not-found.html +1 -1
  21. package/out/_not-found.txt +11 -11
  22. package/out/app-shell/__next._full.txt +11 -11
  23. package/out/app-shell/__next._head.txt +4 -4
  24. package/out/app-shell/__next._index.txt +6 -6
  25. package/out/app-shell/__next._tree.txt +2 -2
  26. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  27. package/out/app-shell/__next.app-shell.txt +3 -3
  28. package/out/app-shell.html +1 -1
  29. package/out/app-shell.txt +11 -11
  30. package/out/index.html +1 -1
  31. package/out/index.txt +12 -12
  32. package/out/project/_/__next._full.txt +12 -12
  33. package/out/project/_/__next._head.txt +4 -4
  34. package/out/project/_/__next._index.txt +6 -6
  35. package/out/project/_/__next._tree.txt +2 -2
  36. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  37. package/out/project/_/__next.project.$d$id.txt +3 -3
  38. package/out/project/_/__next.project.txt +3 -3
  39. package/out/project/_/memory/__next._full.txt +12 -12
  40. package/out/project/_/memory/__next._head.txt +4 -4
  41. package/out/project/_/memory/__next._index.txt +6 -6
  42. package/out/project/_/memory/__next._tree.txt +2 -2
  43. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +3 -3
  44. package/out/project/_/memory/__next.project.$d$id.memory.txt +3 -3
  45. package/out/project/_/memory/__next.project.$d$id.txt +3 -3
  46. package/out/project/_/memory/__next.project.txt +3 -3
  47. package/out/project/_/memory.html +1 -1
  48. package/out/project/_/memory.txt +12 -12
  49. package/out/project/_/queue/__next._full.txt +12 -12
  50. package/out/project/_/queue/__next._head.txt +4 -4
  51. package/out/project/_/queue/__next._index.txt +6 -6
  52. package/out/project/_/queue/__next._tree.txt +2 -2
  53. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  54. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  55. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  56. package/out/project/_/queue/__next.project.txt +3 -3
  57. package/out/project/_/queue.html +1 -1
  58. package/out/project/_/queue.txt +12 -12
  59. package/out/project/_.html +1 -1
  60. package/out/project/_.txt +12 -12
  61. package/out/settings/__next._full.txt +12 -12
  62. package/out/settings/__next._head.txt +4 -4
  63. package/out/settings/__next._index.txt +6 -6
  64. package/out/settings/__next._tree.txt +2 -2
  65. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  66. package/out/settings/__next.settings.txt +3 -3
  67. package/out/settings.html +1 -1
  68. package/out/settings.txt +12 -12
  69. package/out/setup/__next._full.txt +12 -12
  70. package/out/setup/__next._head.txt +4 -4
  71. package/out/setup/__next._index.txt +6 -6
  72. package/out/setup/__next._tree.txt +2 -2
  73. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  74. package/out/setup/__next.setup.txt +3 -3
  75. package/out/setup.html +1 -1
  76. package/out/setup.txt +12 -12
  77. package/package.json +1 -1
  78. package/server/index.js +26 -0
  79. package/server/queue-watcher.js +47 -10
  80. package/server/queue-watcher.test.js +64 -0
  81. package/server/routes.batchProgress.test.js +94 -0
  82. package/server/routes.js +388 -23
  83. package/server/routes.parseActiveBatch.test.js +88 -0
  84. package/server/routes.telegramBridge.test.js +70 -0
  85. package/templates/CLAUDE.md +0 -1
  86. package/out/_next/static/chunks/006g3lco-9xqf.js +0 -1
  87. package/out/_next/static/chunks/035rt-n0oid7d.js +0 -1
  88. package/out/_next/static/chunks/0u~7e4fgf-u06.css +0 -2
  89. /package/out/_next/static/{6uvV3nUfwr_t_JKrZJSP8 → OzDK1Fplm2eUu23bzILlU}/_buildManifest.js +0 -0
  90. /package/out/_next/static/{6uvV3nUfwr_t_JKrZJSP8 → OzDK1Fplm2eUu23bzILlU}/_clientMiddlewareManifest.js +0 -0
  91. /package/out/_next/static/{6uvV3nUfwr_t_JKrZJSP8 → OzDK1Fplm2eUu23bzILlU}/_ssgManifest.js +0 -0
package/out/setup.txt CHANGED
@@ -1,20 +1,20 @@
1
1
  1:"$Sreact.fragment"
2
- 2:I[34852,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
3
- 3:I[86081,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
4
- 4:I[12527,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
5
- 5:I[59763,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
6
- 6:I[64618,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js","/_next/static/chunks/035rt-n0oid7d.js"],"default"]
7
- 7:I[11717,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"OutletBoundary"]
2
+ 2:I[34852,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
3
+ 3:I[86081,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
4
+ 4:I[12527,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
5
+ 5:I[59763,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default"]
6
+ 6:I[64618,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js","/_next/static/chunks/084lff9v4p_vh.js"],"default"]
7
+ 7:I[11717,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"OutletBoundary"]
8
8
  8:"$Sreact.suspense"
9
- b:I[11717,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"ViewportBoundary"]
10
- d:I[11717,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"MetadataBoundary"]
11
- f:I[92243,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default",1]
12
- :HL["/_next/static/chunks/0u~7e4fgf-u06.css","style"]
9
+ b:I[11717,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"ViewportBoundary"]
10
+ d:I[11717,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"MetadataBoundary"]
11
+ f:I[92243,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"default",1]
12
+ :HL["/_next/static/chunks/05.po0c1knrbu.css","style"]
13
13
  :HL["/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
14
- 0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0u~7e4fgf-u06.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/05ok82hwk0x-c.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/035rt-n0oid7d.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0u~7e4fgf-u06.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"6uvV3nUfwr_t_JKrZJSP8"}
14
+ 0:{"P":null,"c":["","setup"],"q":"","i":false,"f":[[["",{"children":["setup",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/05.po0c1knrbu.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/0za4cvk8.n0-y.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/0ox7p_szjhn69.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_mono_8d43a2aa-module__8Li5zG__variable h-full","children":["$","body",null,{"className":"h-full flex flex-col","children":[["$","$L2",null,{}],["$","div",null,{"className":"flex flex-1 min-h-0","children":[["$","$L3",null,{}],["$","main",null,{"className":"flex-1 min-w-0 overflow-auto","children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]]}]]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L6",null,{}],[["$","script","script-0",{"src":"/_next/static/chunks/084lff9v4p_vh.js","async":true,"nonce":"$undefined"}]],["$","$L7",null,{"children":["$","$8",null,{"name":"Next.MetadataOutlet","children":"$@9"}]}]]}],{},null,false,null]},null,false,"$@a"]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$8",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/05.po0c1knrbu.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"OzDK1Fplm2eUu23bzILlU"}
15
15
  10:[]
16
16
  a:"$W10"
17
17
  c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
18
- 11:I[80070,["/_next/static/chunks/05ok82hwk0x-c.js","/_next/static/chunks/0ox7p_szjhn69.js"],"IconMark"]
18
+ 11:I[80070,["/_next/static/chunks/0za4cvk8.n0-y.js","/_next/static/chunks/0ox7p_szjhn69.js"],"IconMark"]
19
19
  9:null
20
20
  e:[["$","title","0",{"children":"QuadWork"}],["$","meta","1",{"name":"description","content":"Unified dashboard for multi-agent coding teams"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L11","3",{}]]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quadwork",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Unified dashboard for multi-agent coding teams — 4 AI agents, one terminal",
5
5
  "bin": {
6
6
  "quadwork": "./bin/quadwork.js"
package/server/index.js CHANGED
@@ -312,6 +312,32 @@ async function buildAgentArgs(projectId, agentId) {
312
312
  if (flags) args.push(...flags);
313
313
  }
314
314
 
315
+ // #343: per-agent model + reasoning effort overrides. Persist in
316
+ // project.agents[agentId].{model,reasoning_effort} via the
317
+ // dashboard Agent Models widget. When unset, fall back to the
318
+ // CLI's own default so existing projects without overrides keep
319
+ // their current behavior.
320
+ //
321
+ // Codex: -c model="<slug>" / -c model_reasoning_effort="<level>"
322
+ // reasoning levels: minimal | low | medium | high (xhigh is
323
+ // deliberately NOT offered — it's the capacity-failure hot
324
+ // spot #343 was filed for).
325
+ // Claude: --model <slug>
326
+ // reasoning_effort is not wired for Claude — Anthropic's CLI
327
+ // doesn't expose an equivalent flag.
328
+ if (cliBase === "codex") {
329
+ if (agentCfg.model && typeof agentCfg.model === "string") {
330
+ args.push("-c", `model="${agentCfg.model}"`);
331
+ }
332
+ if (agentCfg.reasoning_effort && typeof agentCfg.reasoning_effort === "string") {
333
+ args.push("-c", `model_reasoning_effort="${agentCfg.reasoning_effort}"`);
334
+ }
335
+ } else if (cliBase === "claude") {
336
+ if (agentCfg.model && typeof agentCfg.model === "string") {
337
+ args.push("--model", agentCfg.model);
338
+ }
339
+ }
340
+
315
341
  // MCP config injection
316
342
  const mcpHttpPort = project.mcp_http_port;
317
343
  const token = project.agentchattr_token;
@@ -11,8 +11,19 @@
11
11
  * Reference: /Users/cho/Projects/agentchattr/wrapper.py lines 438-541
12
12
  * (`_queue_watcher`). Polling (not fs.watch) is intentional: matches
13
13
  * wrapper.py's behavior and avoids the cross-platform fs.watch
14
- * footguns. The role/rules/identity-hint additions from wrapper.py
15
- * lines 501-528 are intentionally out of scope for v1 per the issue.
14
+ * footguns.
15
+ *
16
+ * #342 / quadwork#342: the v1 prompt intentionally omitted the
17
+ * identity hints from wrapper.py lines 501-528, which broke
18
+ * Claude Code agent sessions. Claude's default self-concept is
19
+ * `@claude`, so a bare `mcp read #general - you were mentioned`
20
+ * causes chat_read(sender: "claude") and a filter on `@claude`
21
+ * mentions — both wrong when the agent is actually @dev /
22
+ * @reviewerN. Codex doesn't trip the same way because its init
23
+ * path already claims identity. The fix here is to scope the
24
+ * wrapper.py additions to identity only: the injected prompt
25
+ * now explicitly names the agent slug and tells the agent which
26
+ * sender to use on chat_read and which mentions to look for.
16
27
  */
17
28
 
18
29
  const fs = require("fs");
@@ -20,6 +31,38 @@ const path = require("path");
20
31
 
21
32
  const POLL_INTERVAL_MS = 1000;
22
33
 
34
+ /**
35
+ * Pure helper: build the injected prompt text for a given agent
36
+ * slug + trigger shape. Exported so it can be unit-tested without
37
+ * a PTY or a filesystem queue. Priority matches the tick() call
38
+ * site below: customPrompt > jobId > channel.
39
+ *
40
+ * agentName is expected to be the registered agent slug such as
41
+ * `dev`, `head`, `reviewer1`, `reviewer2`. The helper does not
42
+ * validate — upstream already controls who may register.
43
+ */
44
+ function buildInjectionPrompt(agentName, { channel, jobId, customPrompt } = {}) {
45
+ if (customPrompt && typeof customPrompt === "string" && customPrompt.trim()) {
46
+ // Operator-supplied prompts already control the identity
47
+ // wording; leave them alone.
48
+ return customPrompt.trim();
49
+ }
50
+ if (jobId) {
51
+ return (
52
+ `You are @${agentName} in this AgentChattr instance. ` +
53
+ `mcp read job_id=${jobId} with sender: "${agentName}" — ` +
54
+ `you (@${agentName}) were mentioned in a job thread, take appropriate action.`
55
+ );
56
+ }
57
+ const ch = channel || "general";
58
+ return (
59
+ `You are @${agentName} in this AgentChattr instance. ` +
60
+ `mcp read #${ch} with sender: "${agentName}" — ` +
61
+ `look for @${agentName} mentions (NOT @claude). ` +
62
+ `You were mentioned, take appropriate action.`
63
+ );
64
+ }
65
+
23
66
  /**
24
67
  * Start polling `{dataDir}/{agentName}_queue.jsonl`. When non-empty,
25
68
  * read all lines, truncate the file (atomic-ish claim — same race the
@@ -78,14 +121,7 @@ function startQueueWatcher(dataDir, agentName, ptyTerm) {
78
121
  }
79
122
  if (!hasTrigger) return;
80
123
 
81
- let prompt;
82
- if (customPrompt) {
83
- prompt = customPrompt;
84
- } else if (jobId) {
85
- prompt = `mcp read job_id=${jobId} - you were mentioned in a job thread, take appropriate action`;
86
- } else {
87
- prompt = `mcp read #${channel} - you were mentioned, take appropriate action`;
88
- }
124
+ const prompt = buildInjectionPrompt(agentName, { channel, jobId, customPrompt });
89
125
 
90
126
  // Flatten newlines: multi-line writes trigger paste detection in
91
127
  // Claude Code (shows "[Pasted text +N]") and can break injection
@@ -122,4 +158,5 @@ function stopQueueWatcher(handle) {
122
158
  module.exports = {
123
159
  startQueueWatcher,
124
160
  stopQueueWatcher,
161
+ buildInjectionPrompt,
125
162
  };
@@ -0,0 +1,64 @@
1
+ // #342 / quadwork#342: unit test for the identity-aware prompt
2
+ // builder. No test runner is wired up in this repo, so this file
3
+ // is a plain node:assert script — run it with `node server/queue-watcher.test.js`
4
+ // and it exits non-zero on any failure.
5
+
6
+ const assert = require("node:assert/strict");
7
+ const { buildInjectionPrompt } = require("./queue-watcher");
8
+
9
+ const DEFAULT_AGENT_SLUGS = ["dev", "head", "reviewer1", "reviewer2"];
10
+
11
+ // 1) Channel prompt — each of the 4 default slugs must:
12
+ // - name the agent with @<slug>
13
+ // - pass sender: "<slug>" explicitly
14
+ // - tell the agent to look for @<slug> mentions (NOT @claude)
15
+ for (const slug of DEFAULT_AGENT_SLUGS) {
16
+ const p = buildInjectionPrompt(slug, { channel: "general" });
17
+ assert.match(p, new RegExp(`You are @${slug} `), `channel: names @${slug}`);
18
+ assert.match(p, new RegExp(`sender: "${slug}"`), `channel: sender string for ${slug}`);
19
+ assert.match(p, new RegExp(`@${slug} mentions`), `channel: @${slug} mention filter`);
20
+ assert.match(p, /NOT @claude/, `channel: explicit NOT @claude guard for ${slug}`);
21
+ assert.match(p, /#general/, `channel: channel name for ${slug}`);
22
+ }
23
+
24
+ // 2) Channel defaults to "general" when not provided.
25
+ {
26
+ const p = buildInjectionPrompt("dev", {});
27
+ assert.match(p, /#general/, "channel defaults to general");
28
+ }
29
+
30
+ // 3) Non-default channel is passed through.
31
+ {
32
+ const p = buildInjectionPrompt("dev", { channel: "batch-33" });
33
+ assert.match(p, /#batch-33/, "custom channel is used");
34
+ }
35
+
36
+ // 4) Job-thread prompt — each slug must:
37
+ // - name the agent with @<slug>
38
+ // - reference the job_id
39
+ // - pass sender: "<slug>" explicitly
40
+ for (const slug of DEFAULT_AGENT_SLUGS) {
41
+ const p = buildInjectionPrompt(slug, { jobId: "42" });
42
+ assert.match(p, new RegExp(`You are @${slug} `), `job: names @${slug}`);
43
+ assert.match(p, /job_id=42/, `job: job_id for ${slug}`);
44
+ assert.match(p, new RegExp(`sender: "${slug}"`), `job: sender string for ${slug}`);
45
+ }
46
+
47
+ // 5) customPrompt wins over channel and jobId and is returned as-is.
48
+ {
49
+ const p = buildInjectionPrompt("dev", {
50
+ channel: "general",
51
+ jobId: "99",
52
+ customPrompt: " do the thing ",
53
+ });
54
+ assert.equal(p, "do the thing", "customPrompt overrides + trims");
55
+ }
56
+
57
+ // 6) Blank customPrompt is ignored (falls through to channel/job path).
58
+ {
59
+ const p = buildInjectionPrompt("reviewer2", { customPrompt: " ", channel: "general" });
60
+ assert.match(p, /You are @reviewer2 /);
61
+ assert.match(p, /#general/);
62
+ }
63
+
64
+ console.log(`queue-watcher.test.js: all assertions passed (${DEFAULT_AGENT_SLUGS.length * 2 + 4} cases)`);
@@ -0,0 +1,94 @@
1
+ // #350 / quadwork#350: batch-progress no-linked-PR row builder +
2
+ // summarizer tests. Plain node:assert script — run with
3
+ // `node server/routes.batchProgress.test.js`.
4
+
5
+ const assert = require("node:assert/strict");
6
+ const { buildNoPrRow, summarizeItems } = require("./routes");
7
+
8
+ // 1) #350 regression fixture: CLOSED issue with no linked PR
9
+ // must render as 100% complete, not 0% queued.
10
+ {
11
+ const issue = {
12
+ number: 336,
13
+ title: "superseded by #338",
14
+ state: "CLOSED",
15
+ url: "https://github.com/realproject7/quadwork/issues/336",
16
+ };
17
+ const row = buildNoPrRow(issue);
18
+ assert.equal(row.status, "closed", "CLOSED with no PR → status=closed");
19
+ assert.equal(row.progress, 100, "CLOSED with no PR → 100%");
20
+ assert.match(row.label, /Closed.*✓/, "label has Closed and ✓ marker");
21
+ assert.equal(row.issue_number, 336);
22
+ assert.equal(row.url, issue.url);
23
+ }
24
+
25
+ // 2) OPEN issue with no linked PR still renders as queued.
26
+ {
27
+ const issue = {
28
+ number: 400,
29
+ title: "still open",
30
+ state: "OPEN",
31
+ url: "https://github.com/realproject7/quadwork/issues/400",
32
+ };
33
+ const row = buildNoPrRow(issue);
34
+ assert.equal(row.status, "queued", "OPEN with no PR → queued");
35
+ assert.equal(row.progress, 0);
36
+ assert.equal(row.label, "Issue · queued");
37
+ }
38
+
39
+ // 3) summarizeItems with a mix of merged and closed-without-PR:
40
+ // should count both toward the complete total, label "complete"
41
+ // when closed > 0.
42
+ {
43
+ const items = [
44
+ { status: "merged" },
45
+ { status: "merged" },
46
+ { status: "merged" },
47
+ { status: "merged" },
48
+ { status: "merged" },
49
+ { status: "closed" },
50
+ { status: "closed" },
51
+ ];
52
+ const out = summarizeItems(items);
53
+ assert.equal(out, "7/7 complete", "mixed merged+closed → X/N complete");
54
+ }
55
+
56
+ // 4) summarizeItems with only merged items keeps the classic
57
+ // "X/N merged" wording (no behavior change for PR-only batches).
58
+ {
59
+ const items = [
60
+ { status: "merged" },
61
+ { status: "merged" },
62
+ { status: "merged" },
63
+ ];
64
+ assert.equal(summarizeItems(items), "3/3 merged");
65
+ }
66
+
67
+ // 5) summarizeItems with a queued + closed mix: done count is
68
+ // closed only, queued surfaces in the detail tail.
69
+ {
70
+ const items = [
71
+ { status: "closed" },
72
+ { status: "queued" },
73
+ { status: "queued" },
74
+ ];
75
+ assert.equal(summarizeItems(items), "1/3 complete · 2 queued");
76
+ }
77
+
78
+ // 6) summarizeItems with in-flight PR states still tallies them
79
+ // in the detail tail and keeps the done count at merged-only.
80
+ {
81
+ const items = [
82
+ { status: "merged" },
83
+ { status: "ready" },
84
+ { status: "approved1" },
85
+ { status: "in_review" },
86
+ { status: "queued" },
87
+ ];
88
+ assert.equal(
89
+ summarizeItems(items),
90
+ "1/5 merged · 1 ready to merge · 1 needs 2nd approval · 1 in review · 1 queued",
91
+ );
92
+ }
93
+
94
+ console.log("routes.batchProgress.test.js: all assertions passed (6 cases)");