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,147 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * gate-cli.ts — tiny CLI wrapper around the deploy-gate decision logic
4
+ * (src/deploy/gate.ts). scripts/deploy-gate.sh calls this to decide approval,
5
+ * keeping the pure decision logic in TypeScript (unit-tested) and the bash
6
+ * wrapper thin (UI + path safety + invoking the deploy script).
7
+ *
8
+ * Usage:
9
+ * node --import tsx src/deploy/gate-cli.ts \
10
+ * --repo <path> [--batch <name>] [--notes <file>] [--state <file>] \
11
+ * [--token-file <path>] [--approval-file <path>] \
12
+ * [--interactive] [--interactive-input <text>]
13
+ *
14
+ * Environment:
15
+ * DEPLOY_APPROVAL_TOKEN the token to match against the token file
16
+ * DEPLOY_APPROVAL_FILE fallback for --approval-file
17
+ *
18
+ * Output (parsed by the bash wrapper):
19
+ * DEPLOY_SUMMARY_BEGIN
20
+ * <multi-line deployment summary>
21
+ * DEPLOY_SUMMARY_END
22
+ * DEPLOY_APPROVED=yes|no
23
+ * DEPLOY_REASON=<reason>
24
+ *
25
+ * Exit code: 0 when approved, 3 when denied (the gate's "hard stop" code).
26
+ */
27
+
28
+ import * as path from "node:path"
29
+
30
+ import { buildDeploySummary, evaluateApproval } from "./gate.js"
31
+
32
+ interface CliArgs {
33
+ repo: string
34
+ batch?: string
35
+ notes?: string
36
+ state?: string
37
+ tokenFile?: string
38
+ approvalFile?: string
39
+ interactive: boolean
40
+ interactiveInput?: string
41
+ }
42
+
43
+ function parseArgs(argv: string[]): CliArgs {
44
+ const args: CliArgs = { repo: "", interactive: false }
45
+ for (let i = 0; i < argv.length; i++) {
46
+ const arg = argv[i]
47
+ const next = (): string | undefined => {
48
+ const v = argv[i + 1]
49
+ if (v === undefined || v.startsWith("--")) {
50
+ return undefined
51
+ }
52
+ i++
53
+ return v
54
+ }
55
+ switch (arg) {
56
+ case "--repo":
57
+ case "--batch":
58
+ case "--notes":
59
+ case "--state":
60
+ case "--token-file":
61
+ case "--approval-file":
62
+ case "--interactive-input": {
63
+ const v = next()
64
+ if (v === undefined) {
65
+ throw new Error(`Missing value for ${arg}`)
66
+ }
67
+ switch (arg) {
68
+ case "--repo":
69
+ args.repo = v
70
+ break
71
+ case "--batch":
72
+ args.batch = v
73
+ break
74
+ case "--notes":
75
+ args.notes = v
76
+ break
77
+ case "--state":
78
+ args.state = v
79
+ break
80
+ case "--token-file":
81
+ args.tokenFile = v
82
+ break
83
+ case "--approval-file":
84
+ args.approvalFile = v
85
+ break
86
+ case "--interactive-input":
87
+ args.interactiveInput = v
88
+ break
89
+ }
90
+ break
91
+ }
92
+ case "--interactive":
93
+ args.interactive = true
94
+ break
95
+ case "--help":
96
+ case "-h":
97
+ console.error(
98
+ "gate-cli.ts --repo <path> [--batch <name>] [--notes <file>] [--state <file>] [--token-file <path>] [--approval-file <path>] [--interactive] [--interactive-input <text>]",
99
+ )
100
+ process.exit(0)
101
+ break
102
+ default:
103
+ throw new Error(`Unknown argument: ${arg}`)
104
+ }
105
+ }
106
+ return args
107
+ }
108
+
109
+ function main(): void {
110
+ const args = parseArgs(process.argv.slice(2))
111
+ if (!args.repo) {
112
+ throw new Error("--repo <path> is required")
113
+ }
114
+
115
+ // Default artifact paths inside the target repo (mirror scripts/deploy-gate.sh).
116
+ const tokenFile = args.tokenFile ?? path.join(args.repo, ".deploy-approval")
117
+ const batch = args.batch ?? "round"
118
+ const approvalFile =
119
+ args.approvalFile ??
120
+ process.env.DEPLOY_APPROVAL_FILE ??
121
+ path.join(args.repo, ".worktrees", `.deploy-approved-${batch}`)
122
+ const statePath = args.state ?? path.join(args.repo, ".worktrees", ".orchestrator-state.json")
123
+
124
+ // 1. Deployment summary (what's being deployed).
125
+ const summary = buildDeploySummary({
126
+ repo: args.repo,
127
+ batch,
128
+ notesPath: args.notes,
129
+ statePath,
130
+ })
131
+ process.stdout.write(`DEPLOY_SUMMARY_BEGIN\n${summary}\nDEPLOY_SUMMARY_END\n`)
132
+
133
+ // 2. Approval decision (pure logic; file reads happen in evaluateApproval).
134
+ const result = evaluateApproval({
135
+ interactive: args.interactive,
136
+ interactiveInput: args.interactiveInput,
137
+ approvalToken: process.env.DEPLOY_APPROVAL_TOKEN,
138
+ tokenFile,
139
+ approvalFile,
140
+ })
141
+
142
+ process.stdout.write(`DEPLOY_APPROVED=${result.approved ? "yes" : "no"}\n`)
143
+ process.stdout.write(`DEPLOY_REASON=${result.reason.replace(/\n/g, " ")}\n`)
144
+ process.exitCode = result.approved ? 0 : 3
145
+ }
146
+
147
+ main()
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Human-approval deploy gate — pure decision logic (Phase 4).
3
+ *
4
+ * The gate exists because deploy-production.sh pushes to production (GitHub
5
+ * Actions triggers / direct SSH deploys to Hetzner). That must never happen
6
+ * without an explicit human decision — NOT an automated "approved because
7
+ * everything passed" rule. The orchestrator may only run the repo's deploy
8
+ * script AFTER this gate returns approved.
9
+ *
10
+ * Two approval modes (documented in docs/phase4-deploy-gate.md):
11
+ *
12
+ * - INTERACTIVE: a human answers "Approve deploy? [y/N]" on a TTY. Any
13
+ * explicit yes (y/yes, case-insensitive) approves; everything else
14
+ * (including empty input) denies.
15
+ * - NON-INTERACTIVE (token/file): exactly ONE of:
16
+ * (a) a one-time approval FILE created by a human, e.g.
17
+ * `touch <repo>/.worktrees/.deploy-approved-<batch>` — its mere
18
+ * presence is the human's explicit action (it does not exist until
19
+ * a human creates it);
20
+ * (b) an env token `DEPLOY_APPROVAL_TOKEN` that strictly matches the
21
+ * token stored in a file like `<repo>/.deploy-approval`.
22
+ * If neither is present, or the token does not match, approval is DENIED.
23
+ *
24
+ * Never auto-approve: with no interactive input, no approval file, and no
25
+ * matching token, the answer is always denied.
26
+ *
27
+ * The decision itself lives in `decideApproval` (pure, unit-tested). The
28
+ * I/O wrapper `evaluateApproval` reads the token/approval files and delegates
29
+ * to it — this is what the bash wrapper (scripts/deploy-gate.sh) calls via
30
+ * the tiny CLI (src/deploy/gate-cli.ts), keeping the logic testable without
31
+ * a network or git.
32
+ */
33
+
34
+ import * as fs from "node:fs"
35
+ import * as path from "node:path"
36
+
37
+ /** Everything `decideApproval` needs — resolved values, no file I/O. */
38
+ export interface ApprovalInputs {
39
+ /** True when the caller wants interactive (TTY) approval. */
40
+ interactive: boolean
41
+ /** The line a human typed at the prompt (undefined when not interactive). */
42
+ interactiveInput?: string
43
+ /** Value of the DEPLOY_APPROVAL_TOKEN env var (undefined when unset). */
44
+ approvalToken?: string
45
+ /** Contents of the token file (e.g. <repo>/.deploy-approval), trimmed. */
46
+ tokenFileContent?: string
47
+ /** True when a human-created one-time approval file exists. */
48
+ approvalFileExists: boolean
49
+ }
50
+
51
+ export interface ApprovalResult {
52
+ approved: boolean
53
+ /** Human-readable reason, safe to print to the operator. */
54
+ reason: string
55
+ }
56
+
57
+ /** Interactive input counts as approval only for an explicit yes. */
58
+ function isExplicitYes(input: string): boolean {
59
+ return /^\s*y(?:es)?\s*$/i.test(input)
60
+ }
61
+
62
+ /**
63
+ * PURE decision function — unit-tested without any file system.
64
+ *
65
+ * Rules (fail-closed):
66
+ * 1. Interactive + explicit yes → approved.
67
+ * 2. Interactive + anything else → denied (empty, "n", "maybe", …).
68
+ * 3. Non-interactive + one-time approval FILE present → approved.
69
+ * 4. Non-interactive + approvalToken AND tokenFileContent set AND they
70
+ * match exactly (trimmed) → approved.
71
+ * 5. Everything else → denied with a reason explaining what was missing.
72
+ *
73
+ * Interactive mode takes precedence when both interactive and a token are
74
+ * configured (the operator is at the keyboard — their answer is what counts).
75
+ */
76
+ export function decideApproval(inputs: ApprovalInputs): ApprovalResult {
77
+ if (inputs.interactive) {
78
+ if (inputs.interactiveInput !== undefined && isExplicitYes(inputs.interactiveInput)) {
79
+ return { approved: true, reason: "approved interactively (explicit yes at the prompt)" }
80
+ }
81
+ return {
82
+ approved: false,
83
+ reason:
84
+ inputs.interactiveInput === undefined
85
+ ? "DENIED: interactive approval requires an explicit yes ('y'/'yes'); no answer was received"
86
+ : `DENIED: interactive approval requires an explicit yes ('y'/'yes'); got '${inputs.interactiveInput.trim() || "(empty)"}'`,
87
+ }
88
+ }
89
+
90
+ if (inputs.approvalFileExists) {
91
+ return { approved: true, reason: "approved by a human-created one-time approval file" }
92
+ }
93
+
94
+ if (inputs.approvalToken !== undefined && inputs.tokenFileContent !== undefined) {
95
+ if (inputs.approvalToken.trim() !== "" && inputs.approvalToken.trim() === inputs.tokenFileContent.trim()) {
96
+ return { approved: true, reason: "approved: DEPLOY_APPROVAL_TOKEN matched the token file" }
97
+ }
98
+ if (inputs.approvalToken.trim() === "") {
99
+ return {
100
+ approved: false,
101
+ reason: "DENIED: DEPLOY_APPROVAL_TOKEN is set but empty — no token to match",
102
+ }
103
+ }
104
+ return {
105
+ approved: false,
106
+ reason: "DENIED: DEPLOY_APPROVAL_TOKEN does not match the token in the approval file",
107
+ }
108
+ }
109
+
110
+ if (inputs.approvalToken !== undefined && inputs.tokenFileContent === undefined) {
111
+ return {
112
+ approved: false,
113
+ reason: "DENIED: DEPLOY_APPROVAL_TOKEN is set but no token file was found to match against",
114
+ }
115
+ }
116
+ if (inputs.approvalToken === undefined && inputs.tokenFileContent !== undefined) {
117
+ return {
118
+ approved: false,
119
+ reason: "DENIED: a token file exists but DEPLOY_APPROVAL_TOKEN is not set",
120
+ }
121
+ }
122
+ return {
123
+ approved: false,
124
+ reason:
125
+ "DENIED: no interactive input, no one-time approval file, and no matching DEPLOY_APPROVAL_TOKEN — a deploy gate can never auto-approve",
126
+ }
127
+ }
128
+
129
+ export interface EvaluateApprovalOptions {
130
+ interactive: boolean
131
+ interactiveInput?: string
132
+ approvalToken?: string
133
+ /** Path to the token file (e.g. <repo>/.deploy-approval). */
134
+ tokenFile?: string
135
+ /** Path to the one-time approval file (e.g. <repo>/.worktrees/.deploy-approved-<batch>). */
136
+ approvalFile?: string
137
+ }
138
+
139
+ /** Read token/approval files (if any) and delegate to the pure decision. */
140
+ export function evaluateApproval(options: EvaluateApprovalOptions): ApprovalResult {
141
+ let tokenFileContent: string | undefined
142
+ if (options.tokenFile) {
143
+ try {
144
+ tokenFileContent = fs.readFileSync(options.tokenFile, "utf-8").trim()
145
+ } catch {
146
+ tokenFileContent = undefined // missing file = no token to match
147
+ }
148
+ }
149
+
150
+ let approvalFileExists = false
151
+ if (options.approvalFile) {
152
+ try {
153
+ approvalFileExists = fs.statSync(options.approvalFile).isFile()
154
+ } catch {
155
+ approvalFileExists = false
156
+ }
157
+ }
158
+
159
+ return decideApproval({
160
+ interactive: options.interactive,
161
+ interactiveInput: options.interactiveInput,
162
+ approvalToken: options.approvalToken,
163
+ tokenFileContent,
164
+ approvalFileExists,
165
+ })
166
+ }
167
+
168
+ // ─── Deployment summary (what would be deployed) ────────────────────────────
169
+
170
+ export interface DeploySummaryOptions {
171
+ /** Target repo root (shown in the summary header). */
172
+ repo: string
173
+ /** Batch id (default: derived from the state file or "unnamed"). */
174
+ batch?: string
175
+ /** Optional deployment notes file; shown verbatim when present. */
176
+ notesPath?: string
177
+ /** Orchestrator state file (`.worktrees/.orchestrator-state.json`). */
178
+ statePath?: string
179
+ }
180
+
181
+ /**
182
+ * Build the human-readable deployment summary printed by the gate BEFORE
183
+ * asking for approval. Sources, in priority order:
184
+ * 1. A provided/generated notes file (verbatim).
185
+ * 2. The orchestrator state file's batch + per-group summary.
186
+ * 3. A generic line (nothing to summarize).
187
+ */
188
+ export function buildDeploySummary(options: DeploySummaryOptions): string {
189
+ const lines: string[] = []
190
+ lines.push(`Target repo: ${options.repo}`)
191
+
192
+ if (options.notesPath) {
193
+ try {
194
+ const notes = fs.readFileSync(options.notesPath, "utf-8").trim()
195
+ if (notes) {
196
+ lines.push("")
197
+ lines.push(notes)
198
+ return lines.join("\n")
199
+ }
200
+ } catch {
201
+ lines.push(`(notes file not readable: ${options.notesPath})`)
202
+ }
203
+ }
204
+
205
+ if (options.statePath) {
206
+ try {
207
+ const raw = fs.readFileSync(options.statePath, "utf-8")
208
+ const state = JSON.parse(raw) as {
209
+ batch?: string
210
+ groups?: Array<{
211
+ name?: string
212
+ status?: string
213
+ issues?: number[]
214
+ review_verdict?: string
215
+ qa?: { verdict?: string }
216
+ }>
217
+ }
218
+ const batch = options.batch ?? state.batch ?? "unnamed"
219
+ lines.push(`Batch: ${batch}`)
220
+ const groups = Array.isArray(state.groups) ? state.groups : []
221
+ if (groups.length === 0) {
222
+ lines.push("(no groups recorded in the orchestrator state file)")
223
+ } else {
224
+ lines.push(`Groups (${groups.length}):`)
225
+ for (const g of groups) {
226
+ const issues = Array.isArray(g.issues) && g.issues.length > 0 ? ` issues ${g.issues.join(",")}` : ""
227
+ const review = g.review_verdict ? ` review=${g.review_verdict}` : ""
228
+ const qa = g.qa?.verdict ? ` qa=${g.qa.verdict}` : ""
229
+ lines.push(` - ${g.name ?? "(unnamed)"}: status=${g.status ?? "?"}${issues}${review}${qa}`)
230
+ }
231
+ }
232
+ return lines.join("\n")
233
+ } catch {
234
+ // fall through to generic
235
+ }
236
+ }
237
+
238
+ lines.push(options.batch ? `Batch: ${options.batch}` : "Batch: (not provided)")
239
+ lines.push("(no deployment notes and no orchestrator state file available — nothing to summarize)")
240
+ return lines.join("\n")
241
+ }
242
+
243
+ /**
244
+ * Default paths for the gate artifacts inside a target repo.
245
+ * - token file: <repo>/.deploy-approval
246
+ * - approval: <repo>/.worktrees/.deploy-approved-<batch>
247
+ */
248
+ export function defaultTokenFile(repo: string): string {
249
+ return path.join(repo, ".deploy-approval")
250
+ }
251
+
252
+ export function defaultApprovalFile(repo: string, batch: string): string {
253
+ return path.join(repo, ".worktrees", `.deploy-approved-${batch}`)
254
+ }