panopticon-cli 0.4.32 → 0.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 (142) hide show
  1. package/README.md +96 -210
  2. package/dist/{agents-BDFHF4T3.js → agents-E43Y3HNU.js} +10 -7
  3. package/dist/chunk-7SN4L4PH.js +150 -0
  4. package/dist/chunk-7SN4L4PH.js.map +1 -0
  5. package/dist/{chunk-2NIAOCIC.js → chunk-AAFQANKW.js} +358 -97
  6. package/dist/chunk-AAFQANKW.js.map +1 -0
  7. package/dist/chunk-AQXETQHW.js +113 -0
  8. package/dist/chunk-AQXETQHW.js.map +1 -0
  9. package/dist/chunk-B3PF6JPQ.js +212 -0
  10. package/dist/chunk-B3PF6JPQ.js.map +1 -0
  11. package/dist/chunk-CFCUOV3Q.js +669 -0
  12. package/dist/chunk-CFCUOV3Q.js.map +1 -0
  13. package/dist/chunk-CWELWPWQ.js +32 -0
  14. package/dist/chunk-CWELWPWQ.js.map +1 -0
  15. package/dist/chunk-DI7ABPNQ.js +352 -0
  16. package/dist/chunk-DI7ABPNQ.js.map +1 -0
  17. package/dist/{chunk-VU4FLXV5.js → chunk-FQ66DECN.js} +31 -4
  18. package/dist/chunk-FQ66DECN.js.map +1 -0
  19. package/dist/{chunk-VIWUCJ4V.js → chunk-FTCPTHIJ.js} +57 -432
  20. package/dist/chunk-FTCPTHIJ.js.map +1 -0
  21. package/dist/{review-status-GWQYY77L.js → chunk-GFP3PIPB.js} +14 -7
  22. package/dist/chunk-GFP3PIPB.js.map +1 -0
  23. package/dist/chunk-GR6ZZMCX.js +816 -0
  24. package/dist/chunk-GR6ZZMCX.js.map +1 -0
  25. package/dist/chunk-HJSM6E6U.js +1038 -0
  26. package/dist/chunk-HJSM6E6U.js.map +1 -0
  27. package/dist/{chunk-XP2DXWYP.js → chunk-HZT2AOPN.js} +164 -39
  28. package/dist/chunk-HZT2AOPN.js.map +1 -0
  29. package/dist/chunk-JQBV3Q2W.js +29 -0
  30. package/dist/chunk-JQBV3Q2W.js.map +1 -0
  31. package/dist/{chunk-BWGFN44T.js → chunk-JT4O4YVM.js} +28 -16
  32. package/dist/chunk-JT4O4YVM.js.map +1 -0
  33. package/dist/chunk-NTO3EDB3.js +600 -0
  34. package/dist/chunk-NTO3EDB3.js.map +1 -0
  35. package/dist/{chunk-JY7R7V4G.js → chunk-OMNXYPXC.js} +2 -2
  36. package/dist/chunk-OMNXYPXC.js.map +1 -0
  37. package/dist/chunk-PELXV435.js +215 -0
  38. package/dist/chunk-PELXV435.js.map +1 -0
  39. package/dist/chunk-PPRFKTVC.js +154 -0
  40. package/dist/chunk-PPRFKTVC.js.map +1 -0
  41. package/dist/chunk-WQG2TYCB.js +677 -0
  42. package/dist/chunk-WQG2TYCB.js.map +1 -0
  43. package/dist/{chunk-HCTJFIJJ.js → chunk-YLPSQAM2.js} +2 -2
  44. package/dist/{chunk-HCTJFIJJ.js.map → chunk-YLPSQAM2.js.map} +1 -1
  45. package/dist/{chunk-6HXKTOD7.js → chunk-ZTFNYOC7.js} +53 -38
  46. package/dist/chunk-ZTFNYOC7.js.map +1 -0
  47. package/dist/cli/index.js +5103 -3165
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/{config-BOAMSKTF.js → config-4CJNUE3O.js} +7 -3
  50. package/dist/dashboard/prompts/merge-agent.md +217 -0
  51. package/dist/dashboard/prompts/review-agent.md +409 -0
  52. package/dist/dashboard/prompts/sync-main.md +84 -0
  53. package/dist/dashboard/prompts/test-agent.md +283 -0
  54. package/dist/dashboard/prompts/work-agent.md +249 -0
  55. package/dist/dashboard/public/assets/index-BxpjweAL.css +32 -0
  56. package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
  57. package/dist/dashboard/public/index.html +2 -2
  58. package/dist/dashboard/server.js +17619 -4044
  59. package/dist/{dns-L3L2BB27.js → dns-7BDJSD3E.js} +4 -2
  60. package/dist/{feedback-writer-AAKF5BTK.js → feedback-writer-LVZ5TFYZ.js} +8 -4
  61. package/dist/feedback-writer-LVZ5TFYZ.js.map +1 -0
  62. package/dist/hume-WMAUBBV2.js +13 -0
  63. package/dist/index.d.ts +162 -40
  64. package/dist/index.js +67 -23
  65. package/dist/index.js.map +1 -1
  66. package/dist/{projects-VXRUCMLM.js → projects-JEIVIYC6.js} +3 -3
  67. package/dist/rally-RKFSWC7E.js +10 -0
  68. package/dist/{remote-agents-Z3R2A5BN.js → remote-agents-TFSMW7GN.js} +2 -2
  69. package/dist/{remote-workspace-2G6V2KNP.js → remote-workspace-AHVHQEES.js} +8 -8
  70. package/dist/review-status-EPFG4XM7.js +19 -0
  71. package/dist/shadow-state-5MDP6YXH.js +30 -0
  72. package/dist/shadow-state-5MDP6YXH.js.map +1 -0
  73. package/dist/{specialist-context-N32QBNNQ.js → specialist-context-ZC6A4M3I.js} +8 -7
  74. package/dist/{specialist-context-N32QBNNQ.js.map → specialist-context-ZC6A4M3I.js.map} +1 -1
  75. package/dist/{specialist-logs-GF3YV4KL.js → specialist-logs-KLGJCEUL.js} +7 -6
  76. package/dist/specialist-logs-KLGJCEUL.js.map +1 -0
  77. package/dist/{specialists-JBIW6MP4.js → specialists-O4HWDJL5.js} +7 -6
  78. package/dist/specialists-O4HWDJL5.js.map +1 -0
  79. package/dist/tldr-daemon-T3THOUGT.js +21 -0
  80. package/dist/tldr-daemon-T3THOUGT.js.map +1 -0
  81. package/dist/traefik-QN7R5I6V.js +19 -0
  82. package/dist/traefik-QN7R5I6V.js.map +1 -0
  83. package/dist/tunnel-W2GZBLEV.js +13 -0
  84. package/dist/tunnel-W2GZBLEV.js.map +1 -0
  85. package/dist/workspace-manager-IE4JL2JP.js +22 -0
  86. package/dist/workspace-manager-IE4JL2JP.js.map +1 -0
  87. package/package.json +2 -2
  88. package/scripts/heartbeat-hook +37 -10
  89. package/scripts/patches/llm-tldr-tsx-support.py +109 -0
  90. package/scripts/pre-tool-hook +26 -15
  91. package/scripts/record-cost-event.js +177 -43
  92. package/scripts/record-cost-event.ts +87 -3
  93. package/scripts/statusline.sh +169 -0
  94. package/scripts/stop-hook +21 -11
  95. package/scripts/tldr-post-edit +72 -0
  96. package/scripts/tldr-read-enforcer +275 -0
  97. package/scripts/work-agent-stop-hook +137 -0
  98. package/skills/check-merged/SKILL.md +143 -0
  99. package/skills/crash-investigation/SKILL.md +301 -0
  100. package/skills/github-cli/SKILL.md +185 -0
  101. package/skills/myn-standards/SKILL.md +351 -0
  102. package/skills/pan-reopen/SKILL.md +65 -0
  103. package/skills/pan-sync-main/SKILL.md +87 -0
  104. package/skills/pan-tldr/SKILL.md +149 -0
  105. package/skills/react-best-practices/SKILL.md +125 -0
  106. package/skills/spec-readiness/REPORT-TEMPLATE.md +158 -0
  107. package/skills/spec-readiness/SCORING-REFERENCE.md +369 -0
  108. package/skills/spec-readiness/SKILL.md +400 -0
  109. package/skills/spec-readiness-setup/SKILL.md +361 -0
  110. package/skills/workspace-status/SKILL.md +56 -0
  111. package/skills/write-spec/SKILL.md +138 -0
  112. package/templates/traefik/dynamic/panopticon.yml.template +0 -5
  113. package/templates/traefik/traefik.yml +0 -8
  114. package/dist/chunk-2NIAOCIC.js.map +0 -1
  115. package/dist/chunk-3XAB4IXF.js +0 -51
  116. package/dist/chunk-3XAB4IXF.js.map +0 -1
  117. package/dist/chunk-6HXKTOD7.js.map +0 -1
  118. package/dist/chunk-BBCUK6N2.js +0 -241
  119. package/dist/chunk-BBCUK6N2.js.map +0 -1
  120. package/dist/chunk-BWGFN44T.js.map +0 -1
  121. package/dist/chunk-ELK6Q7QI.js +0 -545
  122. package/dist/chunk-ELK6Q7QI.js.map +0 -1
  123. package/dist/chunk-JY7R7V4G.js.map +0 -1
  124. package/dist/chunk-LYSBSZYV.js +0 -1523
  125. package/dist/chunk-LYSBSZYV.js.map +0 -1
  126. package/dist/chunk-VIWUCJ4V.js.map +0 -1
  127. package/dist/chunk-VU4FLXV5.js.map +0 -1
  128. package/dist/chunk-XP2DXWYP.js.map +0 -1
  129. package/dist/dashboard/public/assets/index-C7X6LP5Z.css +0 -32
  130. package/dist/dashboard/public/assets/index-ClYqpcAJ.js +0 -645
  131. package/dist/feedback-writer-AAKF5BTK.js.map +0 -1
  132. package/dist/review-status-GWQYY77L.js.map +0 -1
  133. package/dist/traefik-CUJM6K5Z.js +0 -12
  134. /package/dist/{agents-BDFHF4T3.js.map → agents-E43Y3HNU.js.map} +0 -0
  135. /package/dist/{config-BOAMSKTF.js.map → config-4CJNUE3O.js.map} +0 -0
  136. /package/dist/{dns-L3L2BB27.js.map → dns-7BDJSD3E.js.map} +0 -0
  137. /package/dist/{projects-VXRUCMLM.js.map → hume-WMAUBBV2.js.map} +0 -0
  138. /package/dist/{remote-agents-Z3R2A5BN.js.map → projects-JEIVIYC6.js.map} +0 -0
  139. /package/dist/{specialist-logs-GF3YV4KL.js.map → rally-RKFSWC7E.js.map} +0 -0
  140. /package/dist/{specialists-JBIW6MP4.js.map → remote-agents-TFSMW7GN.js.map} +0 -0
  141. /package/dist/{remote-workspace-2G6V2KNP.js.map → remote-workspace-AHVHQEES.js.map} +0 -0
  142. /package/dist/{traefik-CUJM6K5Z.js.map → review-status-EPFG4XM7.js.map} +0 -0
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // scripts/record-cost-event.ts
4
- import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, openSync, readSync, fstatSync, closeSync } from "fs";
5
- import { join as join4 } from "path";
4
+ import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, openSync, readSync, fstatSync, closeSync } from "fs";
5
+ import { execFileSync } from "child_process";
6
+ import { join as join5 } from "path";
6
7
  import { homedir as homedir3 } from "os";
7
8
 
8
9
  // src/lib/cost.ts
@@ -22,6 +23,7 @@ var BIN_DIR = join(PANOPTICON_HOME, "bin");
22
23
  var BACKUPS_DIR = join(PANOPTICON_HOME, "backups");
23
24
  var COSTS_DIR = join(PANOPTICON_HOME, "costs");
24
25
  var HEARTBEATS_DIR = join(PANOPTICON_HOME, "heartbeats");
26
+ var ARCHIVES_DIR = join(PANOPTICON_HOME, "archives");
25
27
  var TRAEFIK_DIR = join(PANOPTICON_HOME, "traefik");
26
28
  var TRAEFIK_DYNAMIC_DIR = join(TRAEFIK_DIR, "dynamic");
27
29
  var TRAEFIK_CERTS_DIR = join(TRAEFIK_DIR, "certs");
@@ -29,36 +31,16 @@ var CERTS_DIR = join(PANOPTICON_HOME, "certs");
29
31
  var CONFIG_FILE = join(CONFIG_DIR, "config.toml");
30
32
  var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
31
33
  var CLAUDE_DIR = join(homedir(), ".claude");
32
- var CODEX_DIR = join(homedir(), ".codex");
33
- var CURSOR_DIR = join(homedir(), ".cursor");
34
- var GEMINI_DIR = join(homedir(), ".gemini");
35
- var OPENCODE_DIR = join(homedir(), ".opencode");
36
- var SYNC_TARGETS = {
37
- claude: {
38
- skills: join(CLAUDE_DIR, "skills"),
39
- commands: join(CLAUDE_DIR, "commands"),
40
- agents: join(CLAUDE_DIR, "agents")
41
- },
42
- codex: {
43
- skills: join(CODEX_DIR, "skills"),
44
- commands: join(CODEX_DIR, "commands"),
45
- agents: join(CODEX_DIR, "agents")
46
- },
47
- cursor: {
48
- skills: join(CURSOR_DIR, "skills"),
49
- commands: join(CURSOR_DIR, "commands"),
50
- agents: join(CURSOR_DIR, "agents")
51
- },
52
- gemini: {
53
- skills: join(GEMINI_DIR, "skills"),
54
- commands: join(GEMINI_DIR, "commands"),
55
- agents: join(GEMINI_DIR, "agents")
56
- },
57
- opencode: {
58
- skills: join(OPENCODE_DIR, "skills"),
59
- commands: join(OPENCODE_DIR, "commands"),
60
- agents: join(OPENCODE_DIR, "agents")
61
- }
34
+ var LEGACY_RUNTIME_DIRS = {
35
+ codex: join(homedir(), ".codex"),
36
+ cursor: join(homedir(), ".cursor"),
37
+ gemini: join(homedir(), ".gemini"),
38
+ opencode: join(homedir(), ".opencode")
39
+ };
40
+ var SYNC_TARGET = {
41
+ skills: join(CLAUDE_DIR, "skills"),
42
+ commands: join(CLAUDE_DIR, "commands"),
43
+ agents: join(CLAUDE_DIR, "agents")
62
44
  };
63
45
  var TEMPLATES_DIR = join(PANOPTICON_HOME, "templates");
64
46
  var CLAUDE_MD_TEMPLATES = join(TEMPLATES_DIR, "claude-md", "sections");
@@ -75,6 +57,15 @@ var SOURCE_TRAEFIK_TEMPLATES = join(SOURCE_TEMPLATES_DIR, "traefik");
75
57
  var SOURCE_SCRIPTS_DIR = join(packageRoot, "scripts");
76
58
  var SOURCE_SKILLS_DIR = join(packageRoot, "skills");
77
59
  var SOURCE_DEV_SKILLS_DIR = join(packageRoot, "dev-skills");
60
+ var SOURCE_AGENTS_DIR = join(packageRoot, "agents");
61
+ var SOURCE_RULES_DIR = join(packageRoot, "rules");
62
+ var CACHE_AGENTS_DIR = join(PANOPTICON_HOME, "agent-definitions");
63
+ var CACHE_RULES_DIR = join(PANOPTICON_HOME, "rules");
64
+ var CACHE_MANIFEST = join(PANOPTICON_HOME, ".manifest.json");
65
+ var DOCS_DIR = join(PANOPTICON_HOME, "docs");
66
+ var PRDS_DIR = join(DOCS_DIR, "prds");
67
+ var PRD_DRAFTS_DIR = join(PRDS_DIR, "drafts");
68
+ var PRD_PUBLISHED_DIR = join(PRDS_DIR, "published");
78
69
 
79
70
  // src/lib/cost.ts
80
71
  var DEFAULT_PRICING = [
@@ -162,26 +153,118 @@ function appendCostEvent(event2) {
162
153
  appendFileSync(getEventsFile(), line, "utf-8");
163
154
  }
164
155
 
156
+ // src/lib/tldr-daemon.ts
157
+ import { exec } from "child_process";
158
+ import { promisify } from "util";
159
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2, unlinkSync } from "fs";
160
+ import { join as join4 } from "path";
161
+ function getTldrMetrics(workspacePath, sinceCheckpoint = false) {
162
+ const tldrDir = join4(workspacePath, ".tldr");
163
+ const interceptionsLog = join4(tldrDir, "interceptions.log");
164
+ const bypassesLog = join4(tldrDir, "bypasses.log");
165
+ const checkpointFile = join4(tldrDir, "metrics-checkpoint.json");
166
+ let interceptionsStartLine = 0;
167
+ let bypassesStartLine = 0;
168
+ if (sinceCheckpoint && existsSync2(checkpointFile)) {
169
+ try {
170
+ const checkpoint = JSON.parse(readFileSync2(checkpointFile, "utf-8"));
171
+ interceptionsStartLine = checkpoint.interceptionsLine || 0;
172
+ bypassesStartLine = checkpoint.bypassesLine || 0;
173
+ } catch {
174
+ }
175
+ }
176
+ const allInterceptionLines = existsSync2(interceptionsLog) ? readFileSync2(interceptionsLog, "utf-8").split("\n").filter((l) => l.trim()) : [];
177
+ const newInterceptions = allInterceptionLines.slice(interceptionsStartLine);
178
+ let estimatedTokensSaved = 0;
179
+ const filesAnalyzed = [];
180
+ for (const line of newInterceptions) {
181
+ const parts = line.trim().split(" ");
182
+ if (parts.length >= 3) {
183
+ const fileSizeBytes = parseInt(parts[1], 10) || 0;
184
+ const relPath = parts.slice(2).join(" ");
185
+ const fullTokens = Math.round(fileSizeBytes / 4);
186
+ estimatedTokensSaved += Math.max(0, fullTokens - 1e3);
187
+ if (relPath && !filesAnalyzed.includes(relPath)) {
188
+ filesAnalyzed.push(relPath);
189
+ }
190
+ }
191
+ }
192
+ const allBypassLines = existsSync2(bypassesLog) ? readFileSync2(bypassesLog, "utf-8").split("\n").filter((l) => l.trim()) : [];
193
+ const newBypasses = allBypassLines.slice(bypassesStartLine);
194
+ const bypassReasons = {};
195
+ for (const line of newBypasses) {
196
+ const parts = line.trim().split(" ");
197
+ if (parts.length >= 2) {
198
+ const reason = parts[1];
199
+ bypassReasons[reason] = (bypassReasons[reason] || 0) + 1;
200
+ }
201
+ }
202
+ return {
203
+ interceptions: newInterceptions.length,
204
+ bypasses: newBypasses.length,
205
+ estimatedTokensSaved,
206
+ filesAnalyzed,
207
+ bypassReasons
208
+ };
209
+ }
210
+ function captureTldrMetrics(workspacePath) {
211
+ const tldrDir = join4(workspacePath, ".tldr");
212
+ if (!existsSync2(tldrDir)) {
213
+ return null;
214
+ }
215
+ const metrics = getTldrMetrics(workspacePath, true);
216
+ const interceptionsLog = join4(tldrDir, "interceptions.log");
217
+ const bypassesLog = join4(tldrDir, "bypasses.log");
218
+ const checkpointFile = join4(tldrDir, "metrics-checkpoint.json");
219
+ const interceptionsTotal = existsSync2(interceptionsLog) ? readFileSync2(interceptionsLog, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
220
+ const bypassesTotal = existsSync2(bypassesLog) ? readFileSync2(bypassesLog, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
221
+ const checkpoint = {
222
+ interceptionsLine: interceptionsTotal,
223
+ bypassesLine: bypassesTotal,
224
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
225
+ };
226
+ try {
227
+ writeFileSync2(checkpointFile, JSON.stringify(checkpoint, null, 2), "utf-8");
228
+ } catch {
229
+ }
230
+ return metrics;
231
+ }
232
+ var execAsync = promisify(exec);
233
+ var TLDR_STATE_DIR = join4(PANOPTICON_HOME, "tldr");
234
+
165
235
  // scripts/record-cost-event.ts
166
236
  var event;
167
237
  try {
168
- const input = readFileSync2(0, "utf-8");
238
+ const input = readFileSync3(0, "utf-8");
169
239
  event = JSON.parse(input);
170
240
  } catch {
171
241
  process.exit(0);
172
242
  }
173
243
  var transcriptPath = event?.transcript_path;
174
- if (!transcriptPath || !existsSync2(transcriptPath)) {
244
+ if (!transcriptPath || !existsSync3(transcriptPath)) {
175
245
  process.exit(0);
176
246
  }
177
247
  var sessionId = event?.session_id || "unknown";
178
- var stateDir = join4(process.env.HOME || homedir3(), ".panopticon", "costs", "state");
179
- mkdirSync2(stateDir, { recursive: true });
180
- var stateFile = join4(stateDir, `${sessionId}.offset`);
248
+ var stateDir = join5(process.env.HOME || homedir3(), ".panopticon", "costs", "state");
249
+ mkdirSync3(stateDir, { recursive: true });
250
+ var stateFile = join5(stateDir, `${sessionId}.offset`);
181
251
  var lastOffset = 0;
182
- if (existsSync2(stateFile)) {
252
+ if (existsSync3(stateFile)) {
183
253
  try {
184
- lastOffset = parseInt(readFileSync2(stateFile, "utf-8").trim(), 10) || 0;
254
+ lastOffset = parseInt(readFileSync3(stateFile, "utf-8").trim(), 10) || 0;
255
+ } catch {
256
+ }
257
+ }
258
+ var seenFile = join5(stateDir, `${sessionId}.seen`);
259
+ var seenRequestIds = /* @__PURE__ */ new Set();
260
+ if (existsSync3(seenFile)) {
261
+ try {
262
+ const seenContent = readFileSync3(seenFile, "utf-8").trim();
263
+ if (seenContent) {
264
+ for (const id of seenContent.split("\n")) {
265
+ if (id.trim()) seenRequestIds.add(id.trim());
266
+ }
267
+ }
185
268
  } catch {
186
269
  }
187
270
  }
@@ -194,7 +277,7 @@ try {
194
277
  var stat = fstatSync(fd);
195
278
  if (stat.size <= lastOffset) {
196
279
  closeSync(fd);
197
- writeFileSync2(stateFile, String(stat.size), "utf-8");
280
+ writeFileSync3(stateFile, String(stat.size), "utf-8");
198
281
  process.exit(0);
199
282
  }
200
283
  var bytesToRead = stat.size - lastOffset;
@@ -204,8 +287,38 @@ closeSync(fd);
204
287
  var newContent = buffer.toString("utf-8");
205
288
  var lines = newContent.split("\n");
206
289
  var agentId = process.env.PANOPTICON_AGENT_ID || "unattributed";
207
- var issueId = process.env.PANOPTICON_ISSUE_ID || "UNKNOWN";
290
+ var issueId = process.env.PANOPTICON_ISSUE_ID || "";
208
291
  var sessionType = process.env.PANOPTICON_SESSION_TYPE || "implementation";
292
+ if (!issueId || issueId === "UNKNOWN") {
293
+ try {
294
+ const branch = execFileSync("git", ["branch", "--show-current"], {
295
+ encoding: "utf-8",
296
+ timeout: 2e3,
297
+ stdio: ["pipe", "pipe", "pipe"]
298
+ }).trim();
299
+ const branchMatch = branch.match(/(pan|min|aud)[-](\d+)/i);
300
+ if (branchMatch) {
301
+ issueId = `${branchMatch[1].toUpperCase()}-${branchMatch[2]}`;
302
+ }
303
+ } catch {
304
+ }
305
+ }
306
+ if (!issueId) {
307
+ issueId = "UNKNOWN";
308
+ }
309
+ var tldrMetrics = null;
310
+ try {
311
+ const workspaceRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
312
+ encoding: "utf-8",
313
+ timeout: 2e3,
314
+ stdio: ["pipe", "pipe", "pipe"]
315
+ }).trim();
316
+ if (workspaceRoot) {
317
+ tldrMetrics = captureTldrMetrics(workspaceRoot);
318
+ }
319
+ } catch {
320
+ }
321
+ var tldrAttachedToFirstEvent = false;
209
322
  for (const line of lines) {
210
323
  if (!line.trim()) continue;
211
324
  try {
@@ -213,6 +326,13 @@ for (const line of lines) {
213
326
  if (entry.type !== "assistant" || !entry.message?.usage) {
214
327
  continue;
215
328
  }
329
+ const requestId = entry.requestId;
330
+ if (requestId) {
331
+ if (seenRequestIds.has(requestId)) {
332
+ continue;
333
+ }
334
+ seenRequestIds.add(requestId);
335
+ }
216
336
  const usage = entry.message.usage;
217
337
  const model = entry.message.model || "claude-sonnet-4";
218
338
  const inputTokens = usage.input_tokens || 0;
@@ -237,6 +357,15 @@ for (const line of lines) {
237
357
  cacheWriteTokens,
238
358
  cacheTTL: "5m"
239
359
  }, pricing);
360
+ const tldrFields = tldrMetrics && !tldrAttachedToFirstEvent && tldrMetrics.interceptions + tldrMetrics.bypasses > 0 ? {
361
+ tldrInterceptions: tldrMetrics.interceptions,
362
+ tldrBypasses: tldrMetrics.bypasses,
363
+ tldrTokensSaved: tldrMetrics.estimatedTokensSaved,
364
+ tldrBypassReasons: Object.keys(tldrMetrics.bypassReasons).length > 0 ? tldrMetrics.bypassReasons : void 0
365
+ } : {};
366
+ if (tldrMetrics && !tldrAttachedToFirstEvent) {
367
+ tldrAttachedToFirstEvent = true;
368
+ }
240
369
  appendCostEvent({
241
370
  ts: (/* @__PURE__ */ new Date()).toISOString(),
242
371
  type: "cost",
@@ -249,10 +378,15 @@ for (const line of lines) {
249
378
  output: outputTokens,
250
379
  cacheRead: cacheReadTokens,
251
380
  cacheWrite: cacheWriteTokens,
252
- cost
381
+ cost,
382
+ ...requestId ? { requestId } : {},
383
+ ...tldrFields
253
384
  });
254
385
  } catch {
255
386
  }
256
387
  }
257
- writeFileSync2(stateFile, String(stat.size), "utf-8");
388
+ writeFileSync3(stateFile, String(stat.size), "utf-8");
389
+ if (seenRequestIds.size > 0) {
390
+ writeFileSync3(seenFile, Array.from(seenRequestIds).join("\n") + "\n", "utf-8");
391
+ }
258
392
  process.exit(0);
@@ -12,10 +12,12 @@
12
12
  */
13
13
 
14
14
  import { readFileSync, existsSync, writeFileSync, mkdirSync, openSync, readSync, fstatSync, closeSync } from 'fs';
15
+ import { execFileSync } from 'child_process';
15
16
  import { join } from 'path';
16
17
  import { homedir } from 'os';
17
18
  import { calculateCost, getPricing, AIProvider } from '../src/lib/cost.js';
18
19
  import { appendCostEvent } from '../src/lib/costs/events.js';
20
+ import { captureTldrMetrics, type TldrSessionMetrics } from '../src/lib/tldr-daemon.js';
19
21
 
20
22
  // ============== Types ==============
21
23
 
@@ -73,6 +75,21 @@ if (existsSync(stateFile)) {
73
75
  } catch { /* start from 0 */ }
74
76
  }
75
77
 
78
+ // Load persisted seen requestIds to guard against crash-before-write duplicates (PAN-238)
79
+ // Claude Code's transcript can have multiple entries per requestId — we emit exactly one event per requestId.
80
+ const seenFile = join(stateDir, `${sessionId}.seen`);
81
+ const seenRequestIds = new Set<string>();
82
+ if (existsSync(seenFile)) {
83
+ try {
84
+ const seenContent = readFileSync(seenFile, 'utf-8').trim();
85
+ if (seenContent) {
86
+ for (const id of seenContent.split('\n')) {
87
+ if (id.trim()) seenRequestIds.add(id.trim());
88
+ }
89
+ }
90
+ } catch { /* start fresh */ }
91
+ }
92
+
76
93
  // Read only NEW content from the transcript (efficient for large files)
77
94
  let fd: number;
78
95
  try {
@@ -97,12 +114,49 @@ closeSync(fd);
97
114
  const newContent = buffer.toString('utf-8');
98
115
  const lines = newContent.split('\n');
99
116
 
100
- // Get agent/issue context from environment
117
+ // Get agent/issue context from environment, with git branch fallback
101
118
  const agentId: string = process.env.PANOPTICON_AGENT_ID || 'unattributed';
102
- const issueId: string = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
119
+ let issueId: string = process.env.PANOPTICON_ISSUE_ID || '';
103
120
  const sessionType: string = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
104
121
 
122
+ // Infer issue ID from git branch if not set (covers ad-hoc Claude sessions)
123
+ if (!issueId || issueId === 'UNKNOWN') {
124
+ try {
125
+ const branch = execFileSync('git', ['branch', '--show-current'], {
126
+ encoding: 'utf-8',
127
+ timeout: 2000,
128
+ stdio: ['pipe', 'pipe', 'pipe'],
129
+ }).trim();
130
+ const branchMatch = branch.match(/(pan|min|aud)[-](\d+)/i);
131
+ if (branchMatch) {
132
+ issueId = `${branchMatch[1].toUpperCase()}-${branchMatch[2]}`;
133
+ }
134
+ } catch {
135
+ // Git not available or not in a repo — that's fine
136
+ }
137
+ }
138
+
139
+ // Final fallback
140
+ if (!issueId) {
141
+ issueId = 'UNKNOWN';
142
+ }
143
+
144
+ // Capture TLDR metrics for this batch (PAN-236)
145
+ // Find workspace root via git (same process already used for branch detection above)
146
+ let tldrMetrics: TldrSessionMetrics | null = null;
147
+ try {
148
+ const workspaceRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
149
+ encoding: 'utf-8',
150
+ timeout: 2000,
151
+ stdio: ['pipe', 'pipe', 'pipe'],
152
+ }).trim();
153
+ if (workspaceRoot) {
154
+ tldrMetrics = captureTldrMetrics(workspaceRoot);
155
+ }
156
+ } catch { /* git not available or no workspace — skip TLDR metrics */ }
157
+
105
158
  // Process new transcript lines looking for assistant messages with usage
159
+ let tldrAttachedToFirstEvent = false;
106
160
  for (const line of lines) {
107
161
  if (!line.trim()) continue;
108
162
 
@@ -114,6 +168,15 @@ for (const line of lines) {
114
168
  continue;
115
169
  }
116
170
 
171
+ // Skip already-seen requestIds — transcript has multiple entries per API request (PAN-238)
172
+ const requestId = entry.requestId;
173
+ if (requestId) {
174
+ if (seenRequestIds.has(requestId)) {
175
+ continue; // Duplicate entry for this request — already emitted a cost event
176
+ }
177
+ seenRequestIds.add(requestId);
178
+ }
179
+
117
180
  const usage = entry.message.usage;
118
181
  const model: string = entry.message.model || 'claude-sonnet-4';
119
182
 
@@ -147,6 +210,22 @@ for (const line of lines) {
147
210
  cacheTTL: '5m',
148
211
  }, pricing);
149
212
 
213
+ // Attach TLDR metrics to the first event in each batch (delta since last batch)
214
+ const tldrFields = tldrMetrics && !tldrAttachedToFirstEvent && tldrMetrics.interceptions + tldrMetrics.bypasses > 0
215
+ ? {
216
+ tldrInterceptions: tldrMetrics.interceptions,
217
+ tldrBypasses: tldrMetrics.bypasses,
218
+ tldrTokensSaved: tldrMetrics.estimatedTokensSaved,
219
+ tldrBypassReasons: Object.keys(tldrMetrics.bypassReasons).length > 0
220
+ ? tldrMetrics.bypassReasons
221
+ : undefined,
222
+ }
223
+ : {};
224
+
225
+ if (tldrMetrics && !tldrAttachedToFirstEvent) {
226
+ tldrAttachedToFirstEvent = true;
227
+ }
228
+
150
229
  // Record the cost event
151
230
  appendCostEvent({
152
231
  ts: new Date().toISOString(),
@@ -161,13 +240,18 @@ for (const line of lines) {
161
240
  cacheRead: cacheReadTokens,
162
241
  cacheWrite: cacheWriteTokens,
163
242
  cost,
243
+ ...(requestId ? { requestId } : {}),
244
+ ...tldrFields,
164
245
  });
165
246
  } catch {
166
247
  // Skip malformed lines silently
167
248
  }
168
249
  }
169
250
 
170
- // Save new byte offset for next invocation
251
+ // Save new byte offset and seen requestIds for next invocation
171
252
  writeFileSync(stateFile, String(stat.size), 'utf-8');
253
+ if (seenRequestIds.size > 0) {
254
+ writeFileSync(seenFile, Array.from(seenRequestIds).join('\n') + '\n', 'utf-8');
255
+ }
172
256
 
173
257
  process.exit(0);
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code status line — all available info + plan usage limits
3
+ # JSON is piped via stdin on each update
4
+
5
+ input=$(cat)
6
+
7
+ # Single jq call to extract all fields at once
8
+ eval "$(echo "$input" | jq -r '
9
+ @sh "model=\(.model.display_name // "")",
10
+ @sh "model_id=\(.model.id // "")",
11
+ @sh "current_dir=\(.workspace.current_dir // "")",
12
+ @sh "project_dir=\(.workspace.project_dir // "")",
13
+ @sh "cost=\(.cost.total_cost_usd // 0)",
14
+ @sh "lines_added=\(.cost.total_lines_added // 0)",
15
+ @sh "lines_removed=\(.cost.total_lines_removed // 0)",
16
+ @sh "ctx_used_pct=\(.context_window.used_percentage // 0)",
17
+ @sh "ctx_size=\(.context_window.context_window_size // 0)",
18
+ @sh "ctx_in=\(.context_window.current_usage.input_tokens // 0)",
19
+ @sh "ctx_out=\(.context_window.current_usage.output_tokens // 0)"
20
+ ' 2>/dev/null)"
21
+
22
+ # ANSI colors
23
+ RST='\033[0m'; DIM='\033[2m'
24
+ CYN='\033[36m'; GRN='\033[32m'; YLW='\033[33m'
25
+ MAG='\033[35m'; RED='\033[31m'; WHT='\033[37m'
26
+
27
+ # Helper: format token count
28
+ fmt() {
29
+ local n=${1:-0}
30
+ if (( n >= 1000000 )); then printf "%.1fM" "$(echo "scale=1;$n/1000000" | bc)"
31
+ elif (( n >= 1000 )); then printf "%.1fk" "$(echo "scale=1;$n/1000" | bc)"
32
+ else echo "$n"; fi
33
+ }
34
+
35
+ # Helper: color a percentage (green < 50, yellow < 80, red >= 80)
36
+ pct_color() {
37
+ local pct_int=${1%.*}
38
+ if (( ${pct_int:-0} >= 80 )); then echo "$RED"
39
+ elif (( ${pct_int:-0} >= 50 )); then echo "$YLW"
40
+ else echo "$GRN"; fi
41
+ }
42
+
43
+ # Helper: format time remaining from ISO timestamp
44
+ time_remaining() {
45
+ local reset_at="$1"
46
+ [ -z "$reset_at" ] || [ "$reset_at" = "null" ] && return
47
+ local reset_epoch now_epoch diff_s hours mins
48
+ reset_epoch=$(date -d "$reset_at" +%s 2>/dev/null) || return
49
+ now_epoch=$(date +%s)
50
+ diff_s=$(( reset_epoch - now_epoch ))
51
+ (( diff_s <= 0 )) && { echo "now"; return; }
52
+ hours=$(( diff_s / 3600 ))
53
+ mins=$(( (diff_s % 3600) / 60 ))
54
+ if (( hours > 0 )); then echo "${hours}h${mins}m"
55
+ else echo "${mins}m"; fi
56
+ }
57
+
58
+ # --- Usage limits (cached for 60s) ---
59
+ CACHE_FILE="/tmp/.claude-usage-cache-$(id -u)"
60
+ CACHE_TTL=60
61
+ usage_5h="" usage_7d="" reset_5h="" reset_7d=""
62
+
63
+ fetch_usage() {
64
+ local creds_file="$HOME/.claude/.credentials.json"
65
+ [ -f "$creds_file" ] || return
66
+ local token
67
+ token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
68
+ [ -z "$token" ] && return
69
+ local response
70
+ response=$(curl -sf --max-time 3 \
71
+ -H "Accept: application/json" \
72
+ -H "Content-Type: application/json" \
73
+ -H "Authorization: Bearer $token" \
74
+ -H "anthropic-beta: oauth-2025-04-20" \
75
+ "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) || return
76
+ echo "$response" > "$CACHE_FILE"
77
+ }
78
+
79
+ # Use cache if fresh, otherwise fetch in background
80
+ if [ -f "$CACHE_FILE" ]; then
81
+ cache_age=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) ))
82
+ if (( cache_age > CACHE_TTL )); then
83
+ # Fetch in background so we don't block the statusline
84
+ fetch_usage &
85
+ fi
86
+ else
87
+ # First run — fetch synchronously (one-time cost)
88
+ fetch_usage
89
+ fi
90
+
91
+ # Read cached data
92
+ if [ -f "$CACHE_FILE" ]; then
93
+ eval "$(jq -r '
94
+ @sh "usage_5h=\(.five_hour.utilization // "")",
95
+ @sh "reset_5h=\(.five_hour.resets_at // "")",
96
+ @sh "usage_7d=\(.seven_day.utilization // "")",
97
+ @sh "reset_7d=\(.seven_day.resets_at // "")"
98
+ ' "$CACHE_FILE" 2>/dev/null)"
99
+ fi
100
+
101
+ # Git branch (fast — reads file directly, no subprocess)
102
+ git_branch=""
103
+ dir="${current_dir:-.}"
104
+ while [ "$dir" != "/" ]; do
105
+ if [ -f "$dir/.git/HEAD" ]; then
106
+ ref=$(< "$dir/.git/HEAD")
107
+ git_branch="${ref#ref: refs/heads/}"
108
+ break
109
+ fi
110
+ dir=$(dirname "$dir")
111
+ done
112
+
113
+ # Context % color
114
+ ctx_color=$(pct_color "$ctx_used_pct")
115
+
116
+ # Cost formatting
117
+ cost_fmt=$(printf '$%.4f' "${cost:-0}")
118
+
119
+ # Line 1: model | dir | git branch
120
+ line1=""
121
+ [ -n "$model" ] && line1+=$(printf "%b%s%b" "$MAG" "$model" "$RST")
122
+ [ -n "$model_id" ] && line1+=$(printf " %b(%s)%b" "$DIM" "$model_id" "$RST")
123
+ if [ -n "$current_dir" ]; then
124
+ short_dir="${current_dir/#$HOME/~}"
125
+ line1+=$(printf " %b%s%b" "$CYN" "$short_dir" "$RST")
126
+ fi
127
+ [ -n "$git_branch" ] && line1+=$(printf " %b%b%s%b" "$DIM" "$GRN" "$git_branch" "$RST")
128
+
129
+ # Line 2: context usage | cost | lines changed
130
+ line2=""
131
+ line2+=$(printf "%bctx%b %b%.0f%%%b" "$DIM" "$RST" "$ctx_color" "$ctx_used_pct" "$RST")
132
+ line2+=$(printf " %b%s%b/%b%s%b" "$WHT" "$(fmt "$ctx_in")" "$RST" "$DIM" "$(fmt "$ctx_size")" "$RST")
133
+ line2+=$(printf " %bout%b %s" "$DIM" "$RST" "$(fmt "$ctx_out")")
134
+ line2+=$(printf " %bcost%b %b%s%b" "$DIM" "$RST" "$YLW" "$cost_fmt" "$RST")
135
+ if (( lines_added > 0 || lines_removed > 0 )); then
136
+ line2+=$(printf " %b+%d%b/%b-%d%b" "$GRN" "$lines_added" "$RST" "$RED" "$lines_removed" "$RST")
137
+ fi
138
+
139
+ # Line 3: plan usage limits (5h + 7d)
140
+ line3=""
141
+ if [ -n "$usage_5h" ]; then
142
+ u5_color=$(pct_color "$usage_5h")
143
+ u5_reset=$(time_remaining "$reset_5h")
144
+ line3+=$(printf "%b5h%b %b%.0f%%%b" "$DIM" "$RST" "$u5_color" "$usage_5h" "$RST")
145
+ [ -n "$u5_reset" ] && line3+=$(printf " %b(%s)%b" "$DIM" "$u5_reset" "$RST")
146
+ fi
147
+ if [ -n "$usage_7d" ]; then
148
+ u7_color=$(pct_color "$usage_7d")
149
+ u7_reset=$(time_remaining "$reset_7d")
150
+ [ -n "$line3" ] && line3+=" "
151
+ line3+=$(printf "%b7d%b %b%.0f%%%b" "$DIM" "$RST" "$u7_color" "$usage_7d" "$RST")
152
+ [ -n "$u7_reset" ] && line3+=$(printf " %b(%s)%b" "$DIM" "$u7_reset" "$RST")
153
+ fi
154
+
155
+ # Write context % to agent dir for dashboard monitoring (non-blocking)
156
+ if [ -n "$PANOPTICON_AGENT_ID" ] && [ -n "$ctx_used_pct" ]; then
157
+ CTX_DIR="$HOME/.panopticon/agents/$PANOPTICON_AGENT_ID"
158
+ if [ -d "$CTX_DIR" ]; then
159
+ printf '%.0f' "$ctx_used_pct" > "$CTX_DIR/context-pct" 2>/dev/null || true
160
+ # Capture initial context % (first time only)
161
+ if [ ! -f "$CTX_DIR/initial-context-pct" ]; then
162
+ printf '%.0f' "$ctx_used_pct" > "$CTX_DIR/initial-context-pct" 2>/dev/null || true
163
+ fi
164
+ fi
165
+ fi
166
+
167
+ printf "%b\n" "$line1"
168
+ printf "%b\n" "$line2"
169
+ [ -n "$line3" ] && printf "%b\n" "$line3"
package/scripts/stop-hook CHANGED
@@ -26,17 +26,20 @@ fi
26
26
  STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
27
27
  mkdir -p "$STATE_DIR"
28
28
 
29
- # Write state to file (atomic write via temp file)
30
- TEMP_FILE="$STATE_DIR/state.json.tmp"
31
- jq -n \
32
- --arg timestamp "$(date -Iseconds)" \
33
- --arg state "idle" \
34
- '{
35
- state: $state,
36
- lastActivity: $timestamp
37
- }' > "$TEMP_FILE" 2>/dev/null || true
38
-
39
- mv "$TEMP_FILE" "$STATE_DIR/state.json" 2>/dev/null || true
29
+ # Update runtime state in runtime.json (NOT state.json state.json holds AgentState config
30
+ # like workspace, runtime, model which must not be overwritten by hooks)
31
+ RUNTIME_FILE="$STATE_DIR/runtime.json"
32
+ TEMP_FILE="$RUNTIME_FILE.tmp"
33
+ if [ -f "$RUNTIME_FILE" ]; then
34
+ # Merge with existing runtime state (preserves contextPercent, sessionId, etc.)
35
+ jq --arg ts "$(date -Iseconds)" \
36
+ '.state = "idle" | .lastActivity = $ts | del(.currentTool)' \
37
+ "$RUNTIME_FILE" > "$TEMP_FILE" 2>/dev/null && mv "$TEMP_FILE" "$RUNTIME_FILE" 2>/dev/null || true
38
+ else
39
+ # Create initial runtime state
40
+ jq -n --arg ts "$(date -Iseconds)" \
41
+ '{state: "idle", lastActivity: $ts}' > "$TEMP_FILE" 2>/dev/null && mv "$TEMP_FILE" "$RUNTIME_FILE" 2>/dev/null || true
42
+ fi
40
43
 
41
44
  # Optionally send heartbeat to API server (non-blocking)
42
45
  # Only if dashboard is running
@@ -54,5 +57,12 @@ if [ -x "$SPECIALIST_HOOK" ]; then
54
57
  "$SPECIALIST_HOOK" &
55
58
  fi
56
59
 
60
+ # Chain to work-agent completion detection hook
61
+ # Detects when a work agent forgot to call "pan work done" and nudges it
62
+ WORK_AGENT_HOOK="$HOME/.panopticon/bin/work-agent-stop-hook"
63
+ if [ -x "$WORK_AGENT_HOOK" ]; then
64
+ "$WORK_AGENT_HOOK" &
65
+ fi
66
+
57
67
  # Always exit successfully
58
68
  exit 0