headlesscode 1.0.2

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 (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. package/tsconfig.json +18 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `headlesscode trend` subcommand — a local, auto-refreshing, multi-repo
3
+ * cost-efficiency trend page. Thin dispatch to src/dashboard/trend.ts, same
4
+ * shape as dashboard/cli.ts.
5
+ *
6
+ * npx tsx src/cli.ts trend --repo <a> [--repo <b> ...] [--port 4460]
7
+ */
8
+
9
+ import { startTrendServer } from "./trend.js"
10
+
11
+ const TREND_USAGE = `headlesscode trend — local, live-updating, multi-repo cost-efficiency trend
12
+
13
+ Usage:
14
+ headlesscode trend --repo <path> [--repo <path> ...] [--port 4460]
15
+
16
+ Options:
17
+ --repo <path> Repo to include (repeatable). At least one required. Reads
18
+ that repo's central-store cost-history.jsonl /
19
+ session-cost-history.jsonl LIVE on every page load — no
20
+ separate build/index step, nothing to keep in sync.
21
+ --port <n> Port to listen on (default: 4460, or $HEADLESSCODE_TREND_PORT)
22
+ --help Show this help and exit
23
+
24
+ Binds to 127.0.0.1 only, read-only, no auth — matches the dashboard's
25
+ local-only contract.
26
+ `
27
+
28
+ interface TrendCliOptions {
29
+ repos: string[]
30
+ port: number
31
+ help: boolean
32
+ }
33
+
34
+ export function parseTrendArgs(argv: string[]): { options: TrendCliOptions; error?: string } {
35
+ const options: TrendCliOptions = {
36
+ repos: [],
37
+ port: Number(process.env.HEADLESSCODE_TREND_PORT ?? 4460),
38
+ help: false,
39
+ }
40
+
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i]
43
+ const eq = arg.indexOf("=")
44
+ const flag = eq === -1 ? arg : arg.slice(0, eq)
45
+ const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
46
+ const next = (): string | undefined => {
47
+ if (inlineValue !== undefined) {
48
+ return inlineValue
49
+ }
50
+ const v = argv[i + 1]
51
+ if (v === undefined || v.startsWith("--")) {
52
+ return undefined
53
+ }
54
+ i++
55
+ return v
56
+ }
57
+
58
+ switch (flag) {
59
+ case "--repo": {
60
+ const v = next()
61
+ if (v === undefined) {
62
+ return { options, error: "Missing value for --repo" }
63
+ }
64
+ options.repos.push(v)
65
+ break
66
+ }
67
+ case "--port": {
68
+ const v = next()
69
+ const n = v === undefined ? Number.NaN : Number(v)
70
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
71
+ return { options, error: "--port requires an integer between 1 and 65535" }
72
+ }
73
+ options.port = n
74
+ break
75
+ }
76
+ case "--help":
77
+ case "-h":
78
+ options.help = true
79
+ break
80
+ default:
81
+ return { options, error: `Unknown argument: ${arg}` }
82
+ }
83
+ }
84
+
85
+ return { options }
86
+ }
87
+
88
+ export async function trendMain(argv: string[]): Promise<number> {
89
+ const { options, error } = parseTrendArgs(argv)
90
+ if (error) {
91
+ process.stderr.write(`headlesscode trend: ${error}\n\n${TREND_USAGE}`)
92
+ return 2
93
+ }
94
+ if (options.help) {
95
+ process.stdout.write(TREND_USAGE)
96
+ return 0
97
+ }
98
+ if (options.repos.length === 0) {
99
+ process.stderr.write(`headlesscode trend: at least one --repo is required\n\n${TREND_USAGE}`)
100
+ return 2
101
+ }
102
+
103
+ let server: Awaited<ReturnType<typeof startTrendServer>>
104
+ try {
105
+ server = await startTrendServer({ port: options.port, repos: options.repos })
106
+ } catch (err) {
107
+ const message = err instanceof Error ? err.message : String(err)
108
+ if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
109
+ process.stderr.write(`headlesscode trend: port ${options.port} is already in use. Pick another with --port <n>.\n`)
110
+ return 2
111
+ }
112
+ process.stderr.write(`headlesscode trend: failed to start: ${message}\n`)
113
+ return 2
114
+ }
115
+
116
+ process.stdout.write(`[trend] listening on http://127.0.0.1:${options.port} (repos: ${options.repos.join(", ")})\n`)
117
+
118
+ return new Promise<number>((resolve) => {
119
+ let shuttingDown = false
120
+ const onSignal = (): void => {
121
+ // Only the first signal shuts down; a second Ctrl-C while the
122
+ // server is mid-close is ignored (the process exits on its own
123
+ // once the listening handle is gone).
124
+ if (shuttingDown) {
125
+ return
126
+ }
127
+ shuttingDown = true
128
+ process.stdout.write("\n[trend] shutting down\n")
129
+ process.removeListener("SIGINT", onSignal)
130
+ process.removeListener("SIGTERM", onSignal)
131
+ // Release the port: stop accepting new connections and drop
132
+ // keep-alive sockets so the process can exit. Without close the
133
+ // listening handle keeps the event loop alive and the port bound.
134
+ server.close()
135
+ server.closeAllConnections?.()
136
+ resolve(0)
137
+ }
138
+ process.on("SIGINT", onSignal)
139
+ process.on("SIGTERM", onSignal)
140
+ })
141
+ }
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Cross-repo cost-efficiency trend — `headlesscode trend --repo <a> --repo <b> ...`.
3
+ *
4
+ * The single-repo dashboard's "Cost history" section (issue #29) already
5
+ * charts one repo's rounds. This is a different question: comparing
6
+ * multiple repos side by side, and surfacing whether the harness is
7
+ * getting more or less efficient over time, computed FRESH from
8
+ * cost-history.jsonl / session-cost-history.jsonl on every request (no
9
+ * cached snapshot) so the page reflects live data as new rounds settle.
10
+ *
11
+ * Deliberately data-driven, not hardcoded to specific issue numbers: the
12
+ * insight bullets below are generic computations (wasted-session
13
+ * clustering, cost/iteration vs. round-size correlation, rework rate,
14
+ * per-repo averages) that keep making sense as new rounds accumulate,
15
+ * rather than prose written for one particular investigation.
16
+ */
17
+
18
+ import * as http from "node:http"
19
+ import * as path from "node:path"
20
+
21
+ import { readCostHistory, readSessionCostHistory, type CostHistoryRecord, type SessionCostRecord } from "../orchestrator/cost-history.js"
22
+
23
+ export interface TrendPoint {
24
+ recordedAt: string
25
+ groupName: string
26
+ issues: number[]
27
+ costUsd: number
28
+ iterations: number
29
+ /** $ per 1,000 iterations — normalizes cheap and expensive rounds onto one scale. */
30
+ rate: number
31
+ continuationCount: number
32
+ reworkCount: number
33
+ /** True when a non-success session was recorded at the same settle event (see matchWastedSessions). */
34
+ wasted: boolean
35
+ wastedNote?: string
36
+ }
37
+
38
+ export interface RepoTrend {
39
+ name: string
40
+ points: TrendPoint[]
41
+ avgRate: number
42
+ totalCost: number
43
+ wastedCost: number
44
+ wastedCount: number
45
+ }
46
+
47
+ export interface TrendData {
48
+ repos: RepoTrend[]
49
+ insights: string[]
50
+ generatedAt: string
51
+ }
52
+
53
+ /** Pearson correlation coefficient; NaN-safe (returns 0 for degenerate input). */
54
+ function correlation(xs: number[], ys: number[]): number {
55
+ const n = xs.length
56
+ if (n < 2) {
57
+ return 0
58
+ }
59
+ const meanX = xs.reduce((s, v) => s + v, 0) / n
60
+ const meanY = ys.reduce((s, v) => s + v, 0) / n
61
+ let num = 0
62
+ let denX = 0
63
+ let denY = 0
64
+ for (let i = 0; i < n; i++) {
65
+ const dx = xs[i] - meanX
66
+ const dy = ys[i] - meanY
67
+ num += dx * dy
68
+ denX += dx * dx
69
+ denY += dy * dy
70
+ }
71
+ const den = Math.sqrt(denX * denY)
72
+ return den === 0 ? 0 : num / den
73
+ }
74
+
75
+ /** Group and session records at the same settle event land within this window of each other. */
76
+ const SETTLE_EVENT_WINDOW_MS = 30_000
77
+
78
+ /**
79
+ * A group record is "wasted-adjacent" when a non-success session was
80
+ * recorded at the SAME settle event. recordGroupCost + recordAllSessionCosts
81
+ * both flush at group-settle time (see cost-history.ts) but stamp their OWN
82
+ * `new Date().toISOString()` a few ms apart — never identical — so pairing
83
+ * requires a small tolerance window, not an exact string match. Each group
84
+ * record claims the closest same-groupName non-success session(s) within
85
+ * that window; a session matched to one group record is not reused for
86
+ * another.
87
+ */
88
+ function buildTrendPoints(groups: CostHistoryRecord[], sessions: SessionCostRecord[]): TrendPoint[] {
89
+ const wastedSessions = sessions.filter((s) => s.status !== "success")
90
+ const claimed = new Set<SessionCostRecord>()
91
+ return [...groups]
92
+ .sort((a, b) => (a.recordedAt < b.recordedAt ? -1 : a.recordedAt > b.recordedAt ? 1 : 0))
93
+ .map((g) => {
94
+ const groupMs = Date.parse(g.recordedAt)
95
+ const wasted = wastedSessions.filter(
96
+ (s) =>
97
+ !claimed.has(s) &&
98
+ s.groupName === g.groupName &&
99
+ Math.abs(Date.parse(s.recordedAt) - groupMs) <= SETTLE_EVENT_WINDOW_MS,
100
+ )
101
+ wasted.forEach((s) => claimed.add(s))
102
+ return {
103
+ recordedAt: g.recordedAt,
104
+ groupName: g.groupName,
105
+ issues: g.issues,
106
+ costUsd: g.costUsd,
107
+ iterations: g.iterations,
108
+ rate: g.iterations > 0 ? (g.costUsd / g.iterations) * 1000 : 0,
109
+ continuationCount: g.continuationCount,
110
+ reworkCount: g.reworkCount,
111
+ wasted: wasted.length > 0,
112
+ wastedNote:
113
+ wasted.length > 0
114
+ ? wasted.map((w) => `${w.mode} ${w.status} ($${w.costUsd.toFixed(4)}, ${w.iterations} iter)`).join("; ")
115
+ : undefined,
116
+ }
117
+ })
118
+ }
119
+
120
+ export function computeTrendData(repos: Array<{ name: string; groups: CostHistoryRecord[]; sessions: SessionCostRecord[] }>): TrendData {
121
+ const repoTrends: RepoTrend[] = repos.map(({ name, groups, sessions }) => {
122
+ const points = buildTrendPoints(groups, sessions)
123
+ const totalCost = points.reduce((s, p) => s + p.costUsd, 0)
124
+ const avgRate = points.length > 0 ? points.reduce((s, p) => s + p.rate, 0) / points.length : 0
125
+ const wastedSessions = sessions.filter((s) => s.status !== "success")
126
+ return {
127
+ name,
128
+ points,
129
+ avgRate,
130
+ totalCost,
131
+ wastedCost: wastedSessions.reduce((s, w) => s + w.costUsd, 0),
132
+ wastedCount: wastedSessions.length,
133
+ }
134
+ })
135
+
136
+ const insights: string[] = []
137
+
138
+ // Wasted-session clustering: are they all in the past, or ongoing?
139
+ const allWasted = repoTrends.flatMap((r) =>
140
+ r.points.filter((p) => p.wasted).map((p) => ({ repo: r.name, ...p })),
141
+ )
142
+ if (allWasted.length > 0) {
143
+ const totalWastedCost = repoTrends.reduce((s, r) => s + r.wastedCost, 0)
144
+ const totalWastedCount = repoTrends.reduce((s, r) => s + r.wastedCount, 0)
145
+ const lastWastedAt = allWasted.reduce((max, p) => (p.recordedAt > max ? p.recordedAt : max), allWasted[0].recordedAt)
146
+ const roundsSince = repoTrends.reduce(
147
+ (s, r) => s + r.points.filter((p) => p.recordedAt > lastWastedAt).length,
148
+ 0,
149
+ )
150
+ if (roundsSince > 0) {
151
+ insights.push(
152
+ `${totalWastedCount} wasted session(s) ($${totalWastedCost.toFixed(4)}) recorded, all before ${lastWastedAt} — zero wasted sessions in the ${roundsSince} round(s) recorded since. If that holds as more rounds land, whatever fix landed around then is working.`,
153
+ )
154
+ } else {
155
+ insights.push(
156
+ `${totalWastedCount} wasted session(s) ($${totalWastedCost.toFixed(4)}) recorded, most recently at ${lastWastedAt} — still an active source of waste; worth checking what those sessions have in common.`,
157
+ )
158
+ }
159
+ } else {
160
+ insights.push("No wasted sessions (session-level error/killed/budget outcomes) recorded in this data.")
161
+ }
162
+
163
+ // Cost/iteration vs. round size: does a bigger round cost MORE per iteration, not just more iterations?
164
+ const allPoints = repoTrends.flatMap((r) => r.points)
165
+ if (allPoints.length >= 4) {
166
+ const corr = correlation(allPoints.map((p) => p.iterations), allPoints.map((p) => p.rate))
167
+ if (corr > 0.35) {
168
+ insights.push(
169
+ `Cost per iteration correlates positively with round size (r=${corr.toFixed(2)} across ${allPoints.length} rounds) — larger rounds aren't just longer, each iteration gets more expensive too. Splitting oversized issues before dispatch likely helps more than tuning iteration count alone.`,
170
+ )
171
+ } else if (corr < -0.35) {
172
+ insights.push(
173
+ `Cost per iteration correlates negatively with round size (r=${corr.toFixed(2)}) — larger rounds are actually getting cheaper per iteration, possibly from cache-hit growth outpacing complexity.`,
174
+ )
175
+ }
176
+ }
177
+
178
+ // Rework rate.
179
+ if (allPoints.length > 0) {
180
+ const reworked = allPoints.filter((p) => p.reworkCount > 0).length
181
+ insights.push(
182
+ `${reworked}/${allPoints.length} round(s) (${((reworked / allPoints.length) * 100).toFixed(0)}%) needed at least one rework cycle.`,
183
+ )
184
+ }
185
+
186
+ // Per-repo average comparison.
187
+ if (repoTrends.length >= 2) {
188
+ const sorted = [...repoTrends].filter((r) => r.points.length > 0).sort((a, b) => b.avgRate - a.avgRate)
189
+ if (sorted.length >= 2) {
190
+ const [highest, lowest] = [sorted[0], sorted[sorted.length - 1]]
191
+ if (highest.name !== lowest.name && lowest.avgRate > 0) {
192
+ const ratio = highest.avgRate / lowest.avgRate
193
+ insights.push(
194
+ `${highest.name} averages $${highest.avgRate.toFixed(3)}/1k-iter vs. ${lowest.name}'s $${lowest.avgRate.toFixed(3)}/1k-iter — ${ratio.toFixed(1)}x higher, consistent with ${highest.name} handling larger/less-familiar tasks.`,
195
+ )
196
+ }
197
+ }
198
+ }
199
+
200
+ return { repos: repoTrends, insights, generatedAt: new Date().toISOString() }
201
+ }
202
+
203
+ export interface TrendServerOptions {
204
+ port: number
205
+ repos: string[]
206
+ }
207
+
208
+ async function loadTrendData(repoPaths: string[]): Promise<TrendData> {
209
+ const repos = await Promise.all(
210
+ repoPaths.map(async (repoPath) => {
211
+ const [groups, sessions] = await Promise.all([readCostHistory(repoPath), readSessionCostHistory(repoPath)])
212
+ return { name: path.basename(repoPath.replace(/\/$/, "")), groups, sessions }
213
+ }),
214
+ )
215
+ return computeTrendData(repos)
216
+ }
217
+
218
+ export function startTrendServer(options: TrendServerOptions): Promise<http.Server> {
219
+ return new Promise((resolve, reject) => {
220
+ const server = http.createServer((req, res) => {
221
+ const url = new URL(req.url ?? "/", "http://localhost")
222
+ if (url.pathname === "/api/trend") {
223
+ loadTrendData(options.repos)
224
+ .then((data) => {
225
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" })
226
+ res.end(JSON.stringify(data))
227
+ })
228
+ .catch((err) => {
229
+ res.writeHead(500, { "content-type": "application/json; charset=utf-8" })
230
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
231
+ })
232
+ return
233
+ }
234
+ if (url.pathname === "/") {
235
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" })
236
+ res.end(renderTrendPage(options.repos))
237
+ return
238
+ }
239
+ res.writeHead(404, { "content-type": "text/plain" })
240
+ res.end("not found")
241
+ })
242
+ server.on("error", reject)
243
+ server.listen(options.port, "127.0.0.1", () => resolve(server))
244
+ })
245
+ }
246
+
247
+ function renderTrendPage(repoPaths: string[]): string {
248
+ return `<!doctype html>
249
+ <html>
250
+ <head>
251
+ <meta charset="utf-8">
252
+ <title>headlesscode: cost-efficiency trend</title>
253
+ <style>
254
+ :root {
255
+ color-scheme: light;
256
+ --surface-1: #fcfcfb; --page: #f9f9f7; --text-primary: #0b0b0b; --text-secondary: #52514e;
257
+ --text-muted: #898781; --grid: #e1e0d9; --baseline: #c3c2b7; --border: rgba(11,11,11,0.10);
258
+ --series-1: #2a78d6; --series-2: #eb6834; --series-3: #1baf7a; --series-4: #eda100;
259
+ --status-critical: #d03b3b; --status-good: #0ca30c;
260
+ }
261
+ @media (prefers-color-scheme: dark) {
262
+ :root {
263
+ color-scheme: dark;
264
+ --surface-1: #1a1a19; --page: #0d0d0d; --text-primary: #ffffff; --text-secondary: #c3c2b7;
265
+ --text-muted: #898781; --grid: #2c2c2a; --baseline: #383835; --border: rgba(255,255,255,0.10);
266
+ --series-1: #3987e5; --series-2: #d95926; --series-3: #199e70; --series-4: #c98500;
267
+ --status-critical: #e66767; --status-good: #0ca30c;
268
+ }
269
+ }
270
+ body { background: var(--page); color: var(--text-primary); margin: 0; font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; }
271
+ .wrap { max-width: 980px; margin: 0 auto; padding: 32px 20px 64px; }
272
+ h1 { font-size: 22px; margin: 0 0 4px; }
273
+ .sub { color: var(--text-secondary); font-size: 14px; margin: 0 0 8px; }
274
+ .stamp { color: var(--text-muted); font-size: 12px; margin: 0 0 28px; }
275
+ .card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 20px; }
276
+ .card h2 { font-size: 15px; margin: 0 0 12px; }
277
+ svg { display: block; overflow: visible; }
278
+ .axis-label { fill: var(--text-muted); font-size: 10px; }
279
+ .gridline { stroke: var(--grid); stroke-width: 1; }
280
+ .baseline { stroke: var(--baseline); stroke-width: 1; }
281
+ .legend { display: flex; gap: 16px; font-size: 12px; color: var(--text-secondary); margin: 10px 0 2px; flex-wrap: wrap; }
282
+ .legend .dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
283
+ .tooltip { position: fixed; pointer-events: none; z-index: 50; background: var(--text-primary); color: var(--surface-1);
284
+ font-size: 12px; padding: 6px 9px; border-radius: 6px; line-height: 1.4; opacity: 0; transition: opacity 80ms; max-width: 260px; }
285
+ ul.insights { margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.6; }
286
+ ul.insights li { margin-bottom: 10px; }
287
+ .refresh-note { color: var(--text-muted); font-size: 12px; }
288
+ </style>
289
+ </head>
290
+ <body>
291
+ <div class="wrap">
292
+ <h1>headlesscode: cost-efficiency trend</h1>
293
+ <p class="sub">Live from <code>cost-history.jsonl</code> across ${repoPaths.length} repo(s): ${repoPaths.map((p) => path.basename(p)).join(", ")}. Refreshes every 30s.</p>
294
+ <p class="stamp" id="stamp">loading…</p>
295
+
296
+ <div class="card">
297
+ <h2>Cost per 1,000 iterations, chronological (all repos, own timeline each)</h2>
298
+ <div id="chart-lines"></div>
299
+ <div class="legend" id="legend-lines"></div>
300
+ </div>
301
+
302
+ <div class="card">
303
+ <h2>Repo averages</h2>
304
+ <div id="chart-bars"></div>
305
+ </div>
306
+
307
+ <div class="card">
308
+ <h2>What the data says</h2>
309
+ <ul class="insights" id="insights"></ul>
310
+ </div>
311
+ </div>
312
+ <div class="tooltip" id="tooltip"></div>
313
+ <script>
314
+ const SERIES = ["var(--series-1)","var(--series-2)","var(--series-3)","var(--series-4)"];
315
+ const tooltip = document.getElementById("tooltip");
316
+ function showTip(evt, html) {
317
+ tooltip.innerHTML = html;
318
+ tooltip.style.opacity = 1;
319
+ tooltip.style.left = (evt.clientX + 14) + "px";
320
+ tooltip.style.top = (evt.clientY + 14) + "px";
321
+ }
322
+ function hideTip() { tooltip.style.opacity = 0; }
323
+ window.__tip = showTip;
324
+ window.__hidetip = hideTip;
325
+
326
+ function lineChart(el, legendEl, repos) {
327
+ const W = 900, H = 240, padL = 44, padR = 12, padT = 14, padB = 26;
328
+ const plotW = W - padL - padR, plotH = H - padT - padB;
329
+ const allRates = repos.flatMap(r => r.points.map(p => p.rate));
330
+ const maxY = (Math.max(1e-6, ...allRates)) * 1.15;
331
+ const maxLen = Math.max(1, ...repos.map(r => r.points.length));
332
+ const x = i => padL + (maxLen <= 1 ? 0 : (i / (maxLen - 1)) * plotW);
333
+ const y = v => padT + plotH - (v / maxY) * plotH;
334
+
335
+ let gridlines = "";
336
+ for (let s = 0; s <= 4; s++) {
337
+ const val = (maxY/4)*s, yy = y(val);
338
+ gridlines += \`<line class="gridline" x1="\${padL}" x2="\${W-padR}" y1="\${yy}" y2="\${yy}"/>
339
+ <text class="axis-label" x="\${padL-6}" y="\${yy+3}" text-anchor="end">\${val.toFixed(2)}</text>\`;
340
+ }
341
+
342
+ let body = "";
343
+ repos.forEach((r, ri) => {
344
+ const color = SERIES[ri % SERIES.length];
345
+ if (r.points.length === 0) return;
346
+ const pathD = r.points.map((p,i) => \`\${i===0?"M":"L"}\${x(i).toFixed(1)},\${y(p.rate).toFixed(1)}\`).join(" ");
347
+ body += \`<path d="\${pathD}" fill="none" stroke="\${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\`;
348
+ body += r.points.map((p,i) => {
349
+ const cx = x(i), cy = y(p.rate);
350
+ const rad = p.wasted ? 5 : 3.5;
351
+ const fill = p.wasted ? "var(--status-critical)" : color;
352
+ const stroke = p.wasted ? \`stroke="var(--surface-1)" stroke-width="2"\` : "";
353
+ const tip = \`<b>\${r.name} · \${p.groupName} · #\${p.issues.join(",")}</b><br>$\${p.costUsd.toFixed(4)} · \${p.iterations} iter · \${p.rate.toFixed(3)} $/1k-iter\${p.wasted ? "<br>wasted: " + p.wastedNote : ""}\`;
354
+ return \`<circle cx="\${cx.toFixed(1)}" cy="\${cy.toFixed(1)}" r="\${rad}" fill="\${fill}" \${stroke}
355
+ onmousemove='window.__tip(event, \${JSON.stringify(tip)})' onmouseleave='window.__hidetip()' style="cursor:pointer"/>\`;
356
+ }).join("");
357
+ });
358
+
359
+ el.innerHTML = \`<svg viewBox="0 0 \${W} \${H}" width="100%" height="\${H}">
360
+ \${gridlines}
361
+ <line class="baseline" x1="\${padL}" x2="\${W-padR}" y1="\${padT+plotH}" y2="\${padT+plotH}"/>
362
+ \${body}
363
+ </svg>\`;
364
+
365
+ legendEl.innerHTML = repos.map((r, ri) => \`<span><span class="dot" style="background:\${SERIES[ri % SERIES.length]}"></span>\${r.name}</span>\`).join("")
366
+ + \`<span><span class="dot" style="background:var(--status-critical)"></span>wasted session</span>\`;
367
+ }
368
+
369
+ function barChart(el, repos) {
370
+ const W = 500, H = 180, padL = 44, padR = 12, padT = 14, padB = 30;
371
+ const plotW = W - padL - padR, plotH = H - padT - padB;
372
+ const maxY = Math.max(1e-6, ...repos.map(r => r.avgRate)) * 1.2;
373
+ const y = v => padT + plotH - (v / maxY) * plotH;
374
+ const bw = plotW / Math.max(1, repos.length) * 0.4;
375
+ let gridlines = "";
376
+ for (let s = 0; s <= 4; s++) {
377
+ const val = (maxY/4)*s, yy = y(val);
378
+ gridlines += \`<line class="gridline" x1="\${padL}" x2="\${W-padR}" y1="\${yy}" y2="\${yy}"/>
379
+ <text class="axis-label" x="\${padL-6}" y="\${yy+3}" text-anchor="end">\${val.toFixed(2)}</text>\`;
380
+ }
381
+ const bars = repos.map((r,i) => {
382
+ const cx = padL + (i+0.5)/repos.length*plotW;
383
+ const barY = y(r.avgRate), barH = (padT+plotH)-barY;
384
+ const color = SERIES[i % SERIES.length];
385
+ return \`<rect x="\${(cx-bw/2).toFixed(1)}" y="\${barY.toFixed(1)}" width="\${bw.toFixed(1)}" height="\${barH.toFixed(1)}" rx="4" fill="\${color}"/>
386
+ <text class="axis-label" x="\${cx.toFixed(1)}" y="\${padT+plotH+16}" text-anchor="middle">\${r.name}</text>
387
+ <text x="\${cx.toFixed(1)}" y="\${(barY-6).toFixed(1)}" text-anchor="middle" font-size="12" fill="var(--text-primary)" font-weight="600">\${r.avgRate.toFixed(3)}</text>\`;
388
+ }).join("");
389
+ el.innerHTML = \`<svg viewBox="0 0 \${W} \${H}" width="100%" height="\${H}">
390
+ \${gridlines}
391
+ <line class="baseline" x1="\${padL}" x2="\${W-padR}" y1="\${padT+plotH}" y2="\${padT+plotH}"/>
392
+ \${bars}
393
+ </svg>\`;
394
+ }
395
+
396
+ async function refresh() {
397
+ try {
398
+ const res = await fetch("/api/trend");
399
+ const data = await res.json();
400
+ document.getElementById("stamp").textContent = "last updated " + new Date(data.generatedAt).toLocaleString();
401
+ lineChart(document.getElementById("chart-lines"), document.getElementById("legend-lines"), data.repos);
402
+ barChart(document.getElementById("chart-bars"), data.repos);
403
+ document.getElementById("insights").innerHTML = data.insights.map(t => "<li>" + t + "</li>").join("");
404
+ } catch (err) {
405
+ document.getElementById("stamp").textContent = "failed to load: " + err;
406
+ }
407
+ }
408
+ refresh();
409
+ setInterval(refresh, 30000);
410
+ </script>
411
+ </body>
412
+ </html>`
413
+ }