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,59 @@
1
+ /**
2
+ * Phase 5 — GitHub issue watcher — public exports.
3
+ *
4
+ * The watcher polls a target label on a GitHub repo, fans each newly-seen
5
+ * issue out into a batch (splitIssues -> spawn via the existing bash
6
+ * spawner), and tracks idempotency durably in a state file so a restart
7
+ * never double-spawns. See docs/phase5-issue-watcher.md.
8
+ */
9
+
10
+ export {
11
+ watchIssues,
12
+ defaultSpawn,
13
+ DEFAULT_POLL_INTERVAL_MS,
14
+ DEFAULT_MAX_PER_SWEEP,
15
+ } from "./watch.js"
16
+ export type {
17
+ WatcherConfig,
18
+ WatcherBatchInfo,
19
+ SpawnBatch,
20
+ SpawnResult,
21
+ SweepResult,
22
+ WatchResult,
23
+ } from "./watch.js"
24
+
25
+ export {
26
+ loadWatcherState,
27
+ loadWatcherStateSync,
28
+ saveWatcherState,
29
+ saveWatcherStateSync,
30
+ markProcessed,
31
+ isProcessed,
32
+ defaultWatcherState,
33
+ } from "./state.js"
34
+ export type { WatcherIssueEntry, WatcherIssueStatus, WatcherState } from "./state.js"
35
+
36
+ export {
37
+ ghApi,
38
+ buildGhUrl,
39
+ listIssues,
40
+ getIssue,
41
+ addLabel,
42
+ createGhClient,
43
+ toIssue,
44
+ GhApiError,
45
+ DEFAULT_GH_BASE_URL,
46
+ GH_API_VERSION,
47
+ ISSUES_PAGE_SIZE,
48
+ DEFAULT_MAX_ISSUES,
49
+ } from "./github.js"
50
+ export type {
51
+ GhClient,
52
+ GhClientOptions,
53
+ GhRequestOptions,
54
+ GitHubIssue,
55
+ ListIssuesOptions,
56
+ } from "./github.js"
57
+
58
+ export { watchMain, parseWatchArgs } from "./cli.js"
59
+ export type { WatchCliOptions } from "./cli.js"
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Phase 5 — watcher durable idempotency state — read/write
3
+ * `.worktrees/.watcher-state.json` (path overridable).
4
+ *
5
+ * Schema:
6
+ *
7
+ * {
8
+ * "processed": {
9
+ * "<issueNumber>": {
10
+ * "number": 27,
11
+ * "title": "…",
12
+ * "label": "needs-agent", // the target label that triggered it
13
+ * "batch": "watcher-2026-08-01",
14
+ * "groups": [ { name, issues, taskFile } ], // splitIssues output
15
+ * "spawnedAt": ISO timestamp, // when the batch was (or will be) spawned
16
+ * "status": "spawned"|"done"|"failed"|"skipped"|"pending",
17
+ * "updatedAt": ISO timestamp,
18
+ * "error": "…" // only on failed
19
+ * }
20
+ * },
21
+ * "updated": ISO timestamp,
22
+ * "lastSweep": ISO timestamp
23
+ * }
24
+ *
25
+ * Idempotency semantics: an issue is "already processed" iff it has an entry
26
+ * with status != "pending". `pending` means "seen but deferred by the
27
+ * maxPerSweep cap" — it is picked up on a later sweep. `failed` is NOT
28
+ * retried by default (avoids spawn loops); pass retryFailed to re-attempt.
29
+ * If the label is later removed from an issue, the entry persists, so the
30
+ * watcher still treats it as processed (it will not respawn).
31
+ *
32
+ * Write-ahead contract (the watcher relies on this ordering):
33
+ * 1. markProcessed(state, entry with status "spawned") + save → durable
34
+ * BEFORE the spawn command runs;
35
+ * 2. spawn;
36
+ * 3. markProcessed(state, entry with status "done"|"failed") + save.
37
+ * A crash between spawn and the final save leaves a "spawned" entry, which
38
+ * the next restart treats as processed — a double-spawn is impossible; a
39
+ * crash between the write-ahead save and the spawn leaves a "spawned" entry
40
+ * with no worktrees (safer than the alternative, and surfaced in the state
41
+ * file for manual inspection).
42
+ *
43
+ * Validation is deliberately LOOSE (plain fs + JSON, no schema library),
44
+ * matching src/orchestrator/state.ts style: missing/malformed files yield a
45
+ * fresh default state; entries that don't validate are dropped.
46
+ */
47
+
48
+ import * as fs from "node:fs"
49
+ import * as fsp from "node:fs/promises"
50
+ import * as path from "node:path"
51
+
52
+ import type { WorktreeSpec } from "../orchestrator/split.js"
53
+
54
+ export type WatcherIssueStatus = "spawned" | "done" | "failed" | "skipped" | "pending"
55
+
56
+ /** One issue's processing record in the watcher state file. */
57
+ export interface WatcherIssueEntry {
58
+ number: number
59
+ title: string
60
+ /** The target label that triggered this entry (recorded for audit). */
61
+ label: string
62
+ /** Batch id (watcher-<date>); the batch maps 1:1 to a spawn invocation. */
63
+ batch?: string
64
+ /** Worktree groups produced by splitIssues([issue]). */
65
+ groups?: WorktreeSpec[]
66
+ spawnedAt?: string
67
+ status: WatcherIssueStatus
68
+ updatedAt: string
69
+ /** Error message when status === "failed". */
70
+ error?: string
71
+ /** Phase 4 pass-through: a QA round should follow this batch. */
72
+ qa?: boolean
73
+ /** Phase 4 pass-through: a deploy gate should follow this batch. */
74
+ deploy?: boolean
75
+ }
76
+
77
+ /** The watcher state file document. */
78
+ export interface WatcherState {
79
+ processed: Record<string, WatcherIssueEntry>
80
+ updated?: string
81
+ lastSweep?: string
82
+ }
83
+
84
+ export function defaultWatcherState(): WatcherState {
85
+ return { processed: {}, updated: new Date().toISOString() }
86
+ }
87
+
88
+ /** Loosely validate a parsed state document; drops malformed entries. */
89
+ function sanitize(raw: unknown): WatcherState {
90
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
91
+ const obj = raw as Record<string, unknown>
92
+ const processed: Record<string, WatcherIssueEntry> = {}
93
+ if (obj.processed !== null && typeof obj.processed === "object" && !Array.isArray(obj.processed)) {
94
+ for (const [key, value] of Object.entries(obj.processed)) {
95
+ if (value === null || typeof value !== "object") {
96
+ continue
97
+ }
98
+ const e = value as Record<string, unknown>
99
+ const number = Number(e.number)
100
+ if (!Number.isInteger(number) || number <= 0) {
101
+ continue
102
+ }
103
+ const entry: WatcherIssueEntry = {
104
+ number,
105
+ title: typeof e.title === "string" ? e.title : "",
106
+ label: typeof e.label === "string" ? e.label : "",
107
+ status: (typeof e.status === "string" ? e.status : "pending") as WatcherIssueStatus,
108
+ updatedAt: typeof e.updatedAt === "string" ? e.updatedAt : new Date().toISOString(),
109
+ }
110
+ if (typeof e.batch === "string") {
111
+ entry.batch = e.batch
112
+ }
113
+ if (Array.isArray(e.groups)) {
114
+ entry.groups = e.groups as WorktreeSpec[]
115
+ }
116
+ if (typeof e.spawnedAt === "string") {
117
+ entry.spawnedAt = e.spawnedAt
118
+ }
119
+ if (typeof e.error === "string") {
120
+ entry.error = e.error
121
+ }
122
+ if (typeof e.qa === "boolean") {
123
+ entry.qa = e.qa
124
+ }
125
+ if (typeof e.deploy === "boolean") {
126
+ entry.deploy = e.deploy
127
+ }
128
+ processed[key] = entry
129
+ }
130
+ }
131
+ const state: WatcherState = { processed }
132
+ if (typeof obj.updated === "string") {
133
+ state.updated = obj.updated
134
+ }
135
+ if (typeof obj.lastSweep === "string") {
136
+ state.lastSweep = obj.lastSweep
137
+ }
138
+ return state
139
+ }
140
+ return defaultWatcherState()
141
+ }
142
+
143
+ /**
144
+ * Read + parse the watcher state file. Missing or malformed files yield a
145
+ * fresh default state (never throws for missing files). Issue #78: a parse
146
+ * error (e.g. a crash truncated the file mid-write, back when writes were
147
+ * non-atomic) is NOT fatal here either — unlike the orchestrator's state
148
+ * file, watcher state is a resumable idempotency cache, not the source of
149
+ * truth for in-flight work, so falling back to a fresh default lets the
150
+ * watcher keep running (worst case: a few issues get re-evaluated) instead
151
+ * of hard-failing on every subsequent poll.
152
+ */
153
+ export async function loadWatcherState(statePath: string): Promise<WatcherState> {
154
+ let raw: string
155
+ try {
156
+ raw = await fsp.readFile(statePath, "utf-8")
157
+ } catch (error) {
158
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
159
+ return defaultWatcherState()
160
+ }
161
+ throw error
162
+ }
163
+ try {
164
+ return sanitize(JSON.parse(raw))
165
+ } catch {
166
+ return defaultWatcherState()
167
+ }
168
+ }
169
+
170
+ /** Synchronous variant; same semantics as loadWatcherState. */
171
+ export function loadWatcherStateSync(statePath: string): WatcherState {
172
+ let raw: string
173
+ try {
174
+ raw = fs.readFileSync(statePath, "utf-8")
175
+ } catch (error) {
176
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
177
+ return defaultWatcherState()
178
+ }
179
+ throw error
180
+ }
181
+ try {
182
+ return sanitize(JSON.parse(raw))
183
+ } catch {
184
+ return defaultWatcherState()
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Unique sibling temp path for an atomic state write (mirrors
190
+ * src/orchestrator/state.ts's stateTmpPath). The pid + counter suffix keeps
191
+ * concurrent saves from ever sharing a temp file, which would race their
192
+ * renames.
193
+ */
194
+ let watcherStateWriteCounter = 0
195
+ function watcherStateTmpPath(statePath: string): string {
196
+ return `${statePath}.tmp-${process.pid}-${++watcherStateWriteCounter}`
197
+ }
198
+
199
+ /**
200
+ * Write the state file (pretty-printed + trailing newline), creating dirs.
201
+ * Issue #78: atomic temp+rename write, matching the orchestrator's
202
+ * saveState (src/orchestrator/state.ts) — a crash mid-write can no longer
203
+ * truncate .watcher-state.json (rename is atomic on the same filesystem).
204
+ */
205
+ export async function saveWatcherState(statePath: string, state: WatcherState): Promise<void> {
206
+ const cleaned = sanitize(state)
207
+ cleaned.updated = new Date().toISOString()
208
+ await fsp.mkdir(path.dirname(statePath), { recursive: true })
209
+ const tmpPath = watcherStateTmpPath(statePath)
210
+ await fsp.writeFile(tmpPath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8")
211
+ await fsp.rename(tmpPath, statePath)
212
+ }
213
+
214
+ /** Synchronous variant of saveWatcherState. */
215
+ export function saveWatcherStateSync(statePath: string, state: WatcherState): void {
216
+ const cleaned = sanitize(state)
217
+ cleaned.updated = new Date().toISOString()
218
+ fs.mkdirSync(path.dirname(statePath), { recursive: true })
219
+ const tmpPath = watcherStateTmpPath(statePath)
220
+ fs.writeFileSync(tmpPath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8")
221
+ fs.renameSync(tmpPath, statePath)
222
+ }
223
+
224
+ /**
225
+ * Return a NEW state with the given entry upserted (immutable; the input
226
+ * state is not mutated). Stamps a fresh `updatedAt` on the entry.
227
+ */
228
+ export function markProcessed(state: WatcherState, entry: WatcherIssueEntry): WatcherState {
229
+ const processed = { ...state.processed }
230
+ processed[String(entry.number)] = { ...entry, updatedAt: new Date().toISOString() }
231
+ return { ...state, processed, updated: new Date().toISOString() }
232
+ }
233
+
234
+ /**
235
+ * Is this issue considered already processed for the current sweep?
236
+ * - no entry -> false (new issue, spawn)
237
+ * - status pending -> false (deferred by the cap; pick up this sweep)
238
+ * - status failed -> false ONLY when retryFailed is set (--retry-failed)
239
+ * - everything else -> true (spawned/done/skipped — do not respawn,
240
+ * regardless of whether the label is still attached)
241
+ */
242
+ export function isProcessed(state: WatcherState, number: number, options?: { retryFailed?: boolean }): boolean {
243
+ const entry = state.processed[String(number)]
244
+ if (!entry) {
245
+ return false
246
+ }
247
+ if (entry.status === "pending") {
248
+ return false
249
+ }
250
+ if (entry.status === "failed" && options?.retryFailed) {
251
+ return false
252
+ }
253
+ return true
254
+ }