ultra-builder-pro-cli 0.25.0 → 0.26.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 (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +49 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1687
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,1014 +0,0 @@
1
- 'use strict';
2
-
3
- const childProcess = require('node:child_process');
4
- const { randomUUID } = require('node:crypto');
5
- const fs = require('node:fs');
6
- const path = require('node:path');
7
-
8
- const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
9
-
10
- const JOURNAL_VERSION = 2;
11
- const LEGACY_JOURNAL_VERSION = 1;
12
- const MAX_JOURNAL_BYTES = 32 * 1024;
13
- const MAX_LOCK_BYTES = 4 * 1024;
14
- const LOCK_VERSION = 2;
15
- const WORKER_FILENAME = 'session-close-journal-worker.cjs';
16
- const TERMINAL_STATUSES = new Set(['completed', 'crashed']);
17
- const JOURNAL_PHASES = new Set([
18
- 'prepared',
19
- 'worker_running',
20
- 'worktree_removed',
21
- 'removal_failed',
22
- 'recovery_failed',
23
- ]);
24
- const PHASE_TRANSITIONS = Object.freeze({
25
- prepared: new Set([
26
- 'worker_running',
27
- 'worktree_removed',
28
- 'removal_failed',
29
- 'recovery_failed',
30
- ]),
31
- worker_running: new Set([
32
- 'worktree_removed',
33
- 'removal_failed',
34
- 'recovery_failed',
35
- ]),
36
- removal_failed: new Set([
37
- 'worktree_removed',
38
- 'recovery_failed',
39
- ]),
40
- worktree_removed: new Set(['recovery_failed']),
41
- recovery_failed: new Set(),
42
- });
43
- const V1_KEYS = Object.freeze([
44
- 'created_at',
45
- 'error',
46
- 'phase',
47
- 'requested_status',
48
- 'sid',
49
- 'task_id',
50
- 'updated_at',
51
- 'version',
52
- 'worktree_path',
53
- ]);
54
- const V2_KEYS = Object.freeze([...V1_KEYS, 'generation'].sort());
55
-
56
- class SessionCloseJournalError extends Error {
57
- constructor(code, message, { cause, details } = {}) {
58
- super(message);
59
- this.name = 'SessionCloseJournalError';
60
- this.code = code;
61
- if (cause) this.cause = cause;
62
- if (details) this.details = details;
63
- }
64
- }
65
-
66
- function assertSid(sid) {
67
- if (typeof sid !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(sid)) {
68
- throw new SessionCloseJournalError(
69
- 'SESSION_CLOSE_JOURNAL_INVALID',
70
- `invalid session id for close journal: ${String(sid)}`,
71
- );
72
- }
73
- return sid;
74
- }
75
-
76
- function assertTerminalStatus(status) {
77
- if (!TERMINAL_STATUSES.has(status)) {
78
- throw new SessionCloseJournalError(
79
- 'SESSION_CLOSE_JOURNAL_INVALID',
80
- `session close target must be completed or crashed, got ${String(status)}`,
81
- );
82
- }
83
- return status;
84
- }
85
-
86
- function journalRoot(repoRoot) {
87
- return path.join(
88
- runtimePaths.pathsFor(repoRoot).runtimeDir,
89
- 'recovery',
90
- 'session-close',
91
- );
92
- }
93
-
94
- function journalPath(repoRoot, sid) {
95
- return path.join(journalRoot(repoRoot), `${assertSid(sid)}.json`);
96
- }
97
-
98
- function statIdentity(stat) {
99
- return {
100
- dev: String(stat.dev),
101
- ino: String(stat.ino),
102
- };
103
- }
104
-
105
- function identitiesEqual(left, right) {
106
- return String(left?.dev) === String(right?.dev)
107
- && String(left?.ino) === String(right?.ino);
108
- }
109
-
110
- function assertRuntimeRoot(repoRoot) {
111
- runtimePaths.validateProjectLayout(repoRoot, {
112
- env: {},
113
- validateRuntimeTree: true,
114
- });
115
- const runtimeDir = runtimePaths.pathsFor(repoRoot).runtimeDir;
116
- const stat = fs.lstatSync(runtimeDir, {
117
- bigint: true,
118
- throwIfNoEntry: false,
119
- });
120
- if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
121
- throw new SessionCloseJournalError(
122
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
123
- `session close runtime root must be a real directory: ${runtimeDir}`,
124
- );
125
- }
126
- return {
127
- runtimeDir,
128
- identity: statIdentity(stat),
129
- };
130
- }
131
-
132
- function decodeHelperResult(result) {
133
- let payload;
134
- try {
135
- payload = JSON.parse(String(result.stdout || '').trim());
136
- } catch (error) {
137
- throw new SessionCloseJournalError(
138
- 'SESSION_CLOSE_JOURNAL_HELPER_FAILED',
139
- `session close journal helper returned invalid output: ${String(result.stderr || '').trim()}`,
140
- { cause: result.error || error },
141
- );
142
- }
143
- if (!payload.ok) {
144
- throw new SessionCloseJournalError(
145
- payload.error?.code || 'SESSION_CLOSE_JOURNAL_HELPER_FAILED',
146
- payload.error?.message || 'session close journal helper failed',
147
- { details: payload.error?.details },
148
- );
149
- }
150
- return payload.value;
151
- }
152
-
153
- function workerPath() {
154
- const runtimeRoot = process.env.UBP_RUNTIME_ROOT
155
- ? path.resolve(process.env.UBP_RUNTIME_ROOT)
156
- : null;
157
- const candidate = runtimeRoot
158
- ? path.join(runtimeRoot, 'runtime', WORKER_FILENAME)
159
- : path.join(__dirname, WORKER_FILENAME);
160
- const stat = fs.lstatSync(candidate, { throwIfNoEntry: false });
161
- if (!stat || stat.isSymbolicLink() || !stat.isFile()) {
162
- throw new SessionCloseJournalError(
163
- 'SESSION_CLOSE_JOURNAL_HELPER_FAILED',
164
- `session close journal worker is missing or unsafe: ${candidate}`,
165
- );
166
- }
167
- return candidate;
168
- }
169
-
170
- function runHelper(repoRoot, request) {
171
- const { runtimeDir, identity } = assertRuntimeRoot(repoRoot);
172
- const result = childProcess.spawnSync(
173
- process.execPath,
174
- [workerPath()],
175
- {
176
- cwd: runtimeDir,
177
- input: JSON.stringify({ ...request, runtime_identity: identity }),
178
- encoding: 'utf8',
179
- maxBuffer: 1024 * 1024,
180
- windowsHide: true,
181
- },
182
- );
183
- if (result.error) {
184
- throw new SessionCloseJournalError(
185
- 'SESSION_CLOSE_JOURNAL_HELPER_FAILED',
186
- `cannot start session close journal helper: ${result.error.message}`,
187
- { cause: result.error },
188
- );
189
- }
190
- return decodeHelperResult(result);
191
- }
192
-
193
- function prepare(repoRoot, {
194
- sid,
195
- task_id,
196
- requested_status,
197
- worktree_path,
198
- }) {
199
- assertSid(sid);
200
- assertTerminalStatus(requested_status);
201
- assertBoundedString(task_id, 'task_id', { max: 256 });
202
- assertBoundedString(worktree_path, 'worktree_path', { max: 4096 });
203
- return {
204
- intent: runHelper(repoRoot, {
205
- op: 'prepare',
206
- sid,
207
- task_id,
208
- requested_status,
209
- worktree_path: path.resolve(worktree_path),
210
- }),
211
- path: journalPath(repoRoot, sid),
212
- };
213
- }
214
-
215
- function read(repoRoot, sid) {
216
- return runHelper(repoRoot, {
217
- op: 'read',
218
- sid: assertSid(sid),
219
- });
220
- }
221
-
222
- function update(repoRoot, sid, patch, {
223
- expected_generation = null,
224
- } = {}) {
225
- assertSid(sid);
226
- return runHelper(repoRoot, {
227
- op: 'update',
228
- sid,
229
- patch,
230
- expected_generation,
231
- });
232
- }
233
-
234
- function discard(repoRoot, sid, {
235
- expected_generation = null,
236
- } = {}) {
237
- assertSid(sid);
238
- return runHelper(repoRoot, {
239
- op: 'discard',
240
- sid,
241
- expected_generation,
242
- });
243
- }
244
-
245
- function list(repoRoot) {
246
- return runHelper(repoRoot, { op: 'list' });
247
- }
248
-
249
- function findForTask(repoRoot, taskId) {
250
- return list(repoRoot).find(
251
- (intent) => intent && intent.task_id === taskId,
252
- ) || null;
253
- }
254
-
255
- function lstatOrNull(candidate) {
256
- try {
257
- return fs.lstatSync(candidate, { bigint: true });
258
- } catch (error) {
259
- if (error.code === 'ENOENT') return null;
260
- throw error;
261
- }
262
- }
263
-
264
- function assertPinnedRuntimeRoot(expected) {
265
- const actual = statIdentity(fs.statSync('.', { bigint: true }));
266
- if (!identitiesEqual(actual, expected)) {
267
- throw new SessionCloseJournalError(
268
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
269
- 'session close runtime root changed before it could be pinned',
270
- { details: { expected, actual } },
271
- );
272
- }
273
- }
274
-
275
- function ensureAndEnterDirectory(name) {
276
- let stat = lstatOrNull(name);
277
- if (!stat) {
278
- try {
279
- fs.mkdirSync(name, { mode: 0o700 });
280
- } catch (error) {
281
- if (error.code !== 'EEXIST') throw error;
282
- }
283
- stat = lstatOrNull(name);
284
- }
285
- if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
286
- throw new SessionCloseJournalError(
287
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
288
- `session close journal ancestor must be a real directory: ${name}`,
289
- );
290
- }
291
- const expected = statIdentity(stat);
292
- process.chdir(name);
293
- const actual = statIdentity(fs.statSync('.', { bigint: true }));
294
- if (!identitiesEqual(actual, expected)) {
295
- throw new SessionCloseJournalError(
296
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
297
- `session close journal ancestor changed while entering it: ${name}`,
298
- { details: { expected, actual } },
299
- );
300
- }
301
- }
302
-
303
- function enterPinnedJournalRoot(expectedRuntimeIdentity) {
304
- assertPinnedRuntimeRoot(expectedRuntimeIdentity);
305
- ensureAndEnterDirectory('recovery');
306
- ensureAndEnterDirectory('session-close');
307
- }
308
-
309
- function assertBoundedString(value, label, {
310
- max,
311
- allowEmpty = false,
312
- } = {}) {
313
- if (typeof value !== 'string'
314
- || (!allowEmpty && value.length === 0)
315
- || value.length > max
316
- || value.includes('\0')) {
317
- throw new SessionCloseJournalError(
318
- 'SESSION_CLOSE_JOURNAL_INVALID',
319
- `session close journal ${label} is invalid`,
320
- );
321
- }
322
- return value;
323
- }
324
-
325
- function assertTimestamp(value, label) {
326
- assertBoundedString(value, label, { max: 64 });
327
- if (!Number.isFinite(Date.parse(value))) {
328
- throw new SessionCloseJournalError(
329
- 'SESSION_CLOSE_JOURNAL_INVALID',
330
- `session close journal ${label} is not a timestamp`,
331
- );
332
- }
333
- return value;
334
- }
335
-
336
- function validateIntent(value, file, expectedSid) {
337
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
338
- throw new SessionCloseJournalError(
339
- 'SESSION_CLOSE_JOURNAL_INVALID',
340
- `session close journal must be an object: ${file}`,
341
- );
342
- }
343
- const version = value.version;
344
- const expectedKeys = version === JOURNAL_VERSION
345
- ? V2_KEYS
346
- : version === LEGACY_JOURNAL_VERSION
347
- ? V1_KEYS
348
- : null;
349
- if (!expectedKeys
350
- || JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expectedKeys)) {
351
- throw new SessionCloseJournalError(
352
- 'SESSION_CLOSE_JOURNAL_INVALID',
353
- `session close journal has an unsupported schema: ${file}`,
354
- );
355
- }
356
- assertSid(value.sid);
357
- if (expectedSid !== undefined && value.sid !== expectedSid) {
358
- throw new SessionCloseJournalError(
359
- 'SESSION_CLOSE_JOURNAL_INVALID',
360
- `session close journal filename does not match payload SID: ${file}`,
361
- );
362
- }
363
- assertTerminalStatus(value.requested_status);
364
- assertBoundedString(value.task_id, 'task_id', { max: 256 });
365
- assertBoundedString(value.worktree_path, 'worktree_path', { max: 4096 });
366
- if (!path.isAbsolute(value.worktree_path)
367
- || path.resolve(value.worktree_path) !== value.worktree_path) {
368
- throw new SessionCloseJournalError(
369
- 'SESSION_CLOSE_JOURNAL_INVALID',
370
- `session close journal worktree_path must be canonical and absolute: ${file}`,
371
- );
372
- }
373
- if (!JOURNAL_PHASES.has(value.phase)) {
374
- throw new SessionCloseJournalError(
375
- 'SESSION_CLOSE_JOURNAL_INVALID',
376
- `session close journal phase is invalid: ${file}`,
377
- );
378
- }
379
- if (value.error !== null) {
380
- assertBoundedString(value.error, 'error', { max: 8192, allowEmpty: true });
381
- }
382
- assertTimestamp(value.created_at, 'created_at');
383
- assertTimestamp(value.updated_at, 'updated_at');
384
- const generation = version === LEGACY_JOURNAL_VERSION ? 0 : value.generation;
385
- if (!Number.isSafeInteger(generation) || generation < 0) {
386
- throw new SessionCloseJournalError(
387
- 'SESSION_CLOSE_JOURNAL_INVALID',
388
- `session close journal generation is invalid: ${file}`,
389
- );
390
- }
391
- return version === LEGACY_JOURNAL_VERSION
392
- ? { ...value, generation }
393
- : value;
394
- }
395
-
396
- function intentFilename(sid) {
397
- return `${assertSid(sid)}.json`;
398
- }
399
-
400
- function lockFilename(sid) {
401
- return `${assertSid(sid)}.lock`;
402
- }
403
-
404
- function assertIdentity(value, label) {
405
- if (!value || typeof value !== 'object' || Array.isArray(value)
406
- || JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(['dev', 'ino'])
407
- || !/^\d{1,32}$/u.test(String(value.dev))
408
- || !/^\d{1,32}$/u.test(String(value.ino))) {
409
- throw new SessionCloseJournalError(
410
- 'SESSION_CLOSE_JOURNAL_INVALID',
411
- `session close journal ${label} identity is invalid`,
412
- );
413
- }
414
- return {
415
- dev: String(value.dev),
416
- ino: String(value.ino),
417
- };
418
- }
419
-
420
- function processStartMarker(pid) {
421
- if (!Number.isSafeInteger(pid) || pid <= 0) return null;
422
- if (process.platform === 'linux') {
423
- try {
424
- const value = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
425
- const close = value.lastIndexOf(')');
426
- if (close < 0) return null;
427
- const fields = value.slice(close + 2).trim().split(/\s+/u);
428
- const startTime = fields[19];
429
- return startTime ? `linux:${startTime}` : null;
430
- } catch {
431
- return null;
432
- }
433
- }
434
- if (process.platform === 'win32') {
435
- const result = childProcess.spawnSync(
436
- 'powershell.exe',
437
- [
438
- '-NoProfile',
439
- '-NonInteractive',
440
- '-Command',
441
- `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
442
- ],
443
- { encoding: 'utf8', windowsHide: true, timeout: 2000 },
444
- );
445
- const value = String(result.stdout || '').trim();
446
- return result.status === 0 && /^\d+$/u.test(value) ? `win32:${value}` : null;
447
- }
448
- const result = childProcess.spawnSync(
449
- 'ps',
450
- ['-o', 'lstart=', '-p', String(pid)],
451
- { encoding: 'utf8', windowsHide: true, timeout: 2000 },
452
- );
453
- const value = String(result.stdout || '').trim().replace(/\s+/gu, ' ');
454
- return result.status === 0 && value ? `${process.platform}:${value}` : null;
455
- }
456
-
457
- function pidIsAlive(pid) {
458
- try {
459
- process.kill(pid, 0);
460
- return true;
461
- } catch (error) {
462
- return error?.code === 'EPERM';
463
- }
464
- }
465
-
466
- function validateLockMetadata(value, file, actualFileIdentity = null) {
467
- const expectedKeys = [
468
- 'created_at',
469
- 'directory',
470
- 'file',
471
- 'pid',
472
- 'process_start',
473
- 'token',
474
- 'version',
475
- ];
476
- if (!value || typeof value !== 'object' || Array.isArray(value)
477
- || JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(expectedKeys)
478
- || value.version !== LOCK_VERSION
479
- || !Number.isSafeInteger(value.pid)
480
- || value.pid <= 0) {
481
- throw new SessionCloseJournalError(
482
- 'SESSION_CLOSE_JOURNAL_INVALID',
483
- `session close journal lock has an unsupported schema: ${file}`,
484
- );
485
- }
486
- assertBoundedString(value.process_start, 'lock process_start', { max: 256 });
487
- assertBoundedString(value.token, 'lock token', { max: 128 });
488
- assertTimestamp(value.created_at, 'lock created_at');
489
- const directory = assertIdentity(value.directory, 'lock directory');
490
- const lockFile = assertIdentity(value.file, 'lock file');
491
- if (actualFileIdentity && !identitiesEqual(lockFile, actualFileIdentity)) {
492
- throw new SessionCloseJournalError(
493
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
494
- `session close journal lock inode does not match its metadata: ${file}`,
495
- { details: { expected: lockFile, actual: actualFileIdentity } },
496
- );
497
- }
498
- return {
499
- ...value,
500
- directory,
501
- file: lockFile,
502
- };
503
- }
504
-
505
- function readLockFile(sid) {
506
- const file = lockFilename(sid);
507
- let fd;
508
- try {
509
- fd = fs.openSync(
510
- file,
511
- fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0),
512
- );
513
- } catch (error) {
514
- if (error.code === 'ENOENT') return null;
515
- throw new SessionCloseJournalError(
516
- 'SESSION_CLOSE_JOURNAL_INVALID',
517
- `cannot open session close journal lock ${file}: ${error.message}`,
518
- { cause: error },
519
- );
520
- }
521
- try {
522
- const stat = fs.fstatSync(fd, { bigint: true });
523
- if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_LOCK_BYTES) {
524
- throw new SessionCloseJournalError(
525
- 'SESSION_CLOSE_JOURNAL_INVALID',
526
- `session close journal lock is not a bounded regular file: ${file}`,
527
- );
528
- }
529
- let value;
530
- try {
531
- value = JSON.parse(fs.readFileSync(fd, 'utf8'));
532
- } catch (error) {
533
- throw new SessionCloseJournalError(
534
- 'SESSION_CLOSE_JOURNAL_INVALID',
535
- `cannot parse session close journal lock ${file}: ${error.message}`,
536
- { cause: error },
537
- );
538
- }
539
- const identity = statIdentity(stat);
540
- return {
541
- metadata: validateLockMetadata(value, file, identity),
542
- identity,
543
- };
544
- } finally {
545
- fs.closeSync(fd);
546
- }
547
- }
548
-
549
- function reclaimStaleLock(sid) {
550
- const existing = readLockFile(sid);
551
- if (!existing) return true;
552
- const { metadata, identity } = existing;
553
- const currentDirectory = statIdentity(fs.statSync('.', { bigint: true }));
554
- if (!identitiesEqual(metadata.directory, currentDirectory)) {
555
- throw new SessionCloseJournalError(
556
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
557
- `session ${sid} close journal lock belongs to a different directory`,
558
- );
559
- }
560
- if (pidIsAlive(metadata.pid)) {
561
- const actualStart = processStartMarker(metadata.pid);
562
- if (!actualStart || actualStart === metadata.process_start) return false;
563
- }
564
- const current = lstatOrNull(lockFilename(sid));
565
- if (!current || current.isSymbolicLink() || !current.isFile()
566
- || !identitiesEqual(statIdentity(current), identity)) {
567
- throw new SessionCloseJournalError(
568
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
569
- `session ${sid} close journal lock changed before stale-owner recovery`,
570
- );
571
- }
572
- fs.unlinkSync(lockFilename(sid));
573
- syncCurrentDirectory();
574
- return true;
575
- }
576
-
577
- function syncCurrentDirectory() {
578
- let fd;
579
- try {
580
- fd = fs.openSync('.', fs.constants.O_RDONLY);
581
- fs.fsyncSync(fd);
582
- } finally {
583
- if (fd !== undefined) fs.closeSync(fd);
584
- }
585
- }
586
-
587
- function readIntentFile(sid) {
588
- const file = intentFilename(sid);
589
- let fd;
590
- try {
591
- fd = fs.openSync(
592
- file,
593
- fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0),
594
- );
595
- } catch (error) {
596
- if (error.code === 'ENOENT') return null;
597
- throw new SessionCloseJournalError(
598
- 'SESSION_CLOSE_JOURNAL_INVALID',
599
- `cannot open session close journal ${file}: ${error.message}`,
600
- { cause: error },
601
- );
602
- }
603
- try {
604
- const stat = fs.fstatSync(fd);
605
- if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_JOURNAL_BYTES) {
606
- throw new SessionCloseJournalError(
607
- 'SESSION_CLOSE_JOURNAL_INVALID',
608
- `session close journal is not a bounded regular file: ${file}`,
609
- );
610
- }
611
- let value;
612
- try {
613
- value = JSON.parse(fs.readFileSync(fd, 'utf8'));
614
- } catch (error) {
615
- throw new SessionCloseJournalError(
616
- 'SESSION_CLOSE_JOURNAL_INVALID',
617
- `cannot parse session close journal ${file}: ${error.message}`,
618
- { cause: error },
619
- );
620
- }
621
- return validateIntent(value, file, sid);
622
- } finally {
623
- fs.closeSync(fd);
624
- }
625
- }
626
-
627
- function writeTemp(intent, sid) {
628
- const temporary = `${assertSid(sid)}.${process.pid}.${randomUUID()}.tmp`;
629
- let fd;
630
- try {
631
- fd = fs.openSync(
632
- temporary,
633
- fs.constants.O_WRONLY
634
- | fs.constants.O_CREAT
635
- | fs.constants.O_EXCL
636
- | (fs.constants.O_NOFOLLOW || 0),
637
- 0o600,
638
- );
639
- fs.writeFileSync(fd, `${JSON.stringify(intent, null, 2)}\n`, 'utf8');
640
- fs.fsyncSync(fd);
641
- fs.closeSync(fd);
642
- fd = undefined;
643
- return temporary;
644
- } catch (error) {
645
- if (fd !== undefined) {
646
- try { fs.closeSync(fd); } catch { /* best effort */ }
647
- }
648
- try { fs.rmSync(temporary, { force: true }); } catch { /* best effort */ }
649
- throw error;
650
- }
651
- }
652
-
653
- function writeReplacement(intent, sid) {
654
- const temporary = writeTemp(intent, sid);
655
- try {
656
- fs.renameSync(temporary, intentFilename(sid));
657
- syncCurrentDirectory();
658
- } finally {
659
- try { fs.rmSync(temporary, { force: true }); } catch { /* best effort */ }
660
- }
661
- }
662
-
663
- function sameCloseIdentity(left, right) {
664
- return left.sid === right.sid
665
- && left.task_id === right.task_id
666
- && left.requested_status === right.requested_status
667
- && left.worktree_path === right.worktree_path;
668
- }
669
-
670
- function helperPrepare(request) {
671
- assertSid(request.sid);
672
- assertTerminalStatus(request.requested_status);
673
- assertBoundedString(request.task_id, 'task_id', { max: 256 });
674
- assertBoundedString(request.worktree_path, 'worktree_path', { max: 4096 });
675
- const now = new Date().toISOString();
676
- const candidate = validateIntent({
677
- version: JOURNAL_VERSION,
678
- generation: 0,
679
- sid: request.sid,
680
- task_id: request.task_id,
681
- requested_status: request.requested_status,
682
- worktree_path: request.worktree_path,
683
- phase: 'prepared',
684
- error: null,
685
- created_at: now,
686
- updated_at: now,
687
- }, intentFilename(request.sid), request.sid);
688
- const temporary = writeTemp(candidate, request.sid);
689
- try {
690
- try {
691
- fs.linkSync(temporary, intentFilename(request.sid));
692
- syncCurrentDirectory();
693
- return candidate;
694
- } catch (error) {
695
- if (error.code !== 'EEXIST') throw error;
696
- const existing = readIntentFile(request.sid);
697
- if (existing && sameCloseIdentity(existing, candidate)) return existing;
698
- throw new SessionCloseJournalError(
699
- 'SESSION_CLOSE_CONFLICT',
700
- `session ${request.sid} already has a different close intent`,
701
- );
702
- }
703
- } finally {
704
- try { fs.rmSync(temporary, { force: true }); } catch { /* best effort */ }
705
- }
706
- }
707
-
708
- function acquireUpdateLock(sid) {
709
- const name = lockFilename(sid);
710
- for (let attempt = 0; attempt < 3; attempt += 1) {
711
- const temporary = `${name}.${process.pid}.${randomUUID()}.tmp`;
712
- let fd;
713
- try {
714
- fd = fs.openSync(
715
- temporary,
716
- fs.constants.O_WRONLY
717
- | fs.constants.O_CREAT
718
- | fs.constants.O_EXCL
719
- | (fs.constants.O_NOFOLLOW || 0),
720
- 0o600,
721
- );
722
- const processStart = processStartMarker(process.pid);
723
- if (!processStart) {
724
- throw new SessionCloseJournalError(
725
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
726
- 'cannot establish the session close journal worker process-start marker',
727
- );
728
- }
729
- const fileIdentity = statIdentity(fs.fstatSync(fd, { bigint: true }));
730
- const directoryIdentity = statIdentity(fs.statSync('.', { bigint: true }));
731
- const metadata = validateLockMetadata({
732
- version: LOCK_VERSION,
733
- pid: process.pid,
734
- process_start: processStart,
735
- token: randomUUID(),
736
- created_at: new Date().toISOString(),
737
- directory: directoryIdentity,
738
- file: fileIdentity,
739
- }, temporary, fileIdentity);
740
- fs.writeFileSync(fd, `${JSON.stringify(metadata)}\n`, 'utf8');
741
- fs.fsyncSync(fd);
742
- fs.closeSync(fd);
743
- fd = undefined;
744
- fs.linkSync(temporary, name);
745
- syncCurrentDirectory();
746
- fs.unlinkSync(temporary);
747
- return {
748
- name,
749
- identity: fileIdentity,
750
- token: metadata.token,
751
- };
752
- } catch (error) {
753
- if (fd !== undefined) {
754
- try { fs.closeSync(fd); } catch { /* best effort */ }
755
- }
756
- try { fs.rmSync(temporary, { force: true }); } catch { /* best effort */ }
757
- if (error.code !== 'EEXIST') {
758
- throw error;
759
- }
760
- if (!reclaimStaleLock(sid)) {
761
- throw new SessionCloseJournalError(
762
- 'SESSION_CLOSE_CONFLICT',
763
- `session ${sid} close journal is being updated concurrently`,
764
- );
765
- }
766
- }
767
- }
768
- throw new SessionCloseJournalError(
769
- 'SESSION_CLOSE_CONFLICT',
770
- `session ${sid} close journal lock changed repeatedly during recovery`,
771
- );
772
- }
773
-
774
- function releaseUpdateLock(lock) {
775
- const current = readLockFile(lock.name.slice(0, -'.lock'.length));
776
- if (!current) return;
777
- if (!identitiesEqual(current.identity, lock.identity)
778
- || current.metadata.token !== lock.token) {
779
- throw new SessionCloseJournalError(
780
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
781
- `session close journal lock ownership changed before release: ${lock.name}`,
782
- );
783
- }
784
- const stat = lstatOrNull(lock.name);
785
- if (!stat || stat.isSymbolicLink() || !stat.isFile()
786
- || !identitiesEqual(statIdentity(stat), lock.identity)) {
787
- throw new SessionCloseJournalError(
788
- 'SESSION_CLOSE_JOURNAL_UNSAFE',
789
- `session close journal lock inode changed before release: ${lock.name}`,
790
- );
791
- }
792
- fs.unlinkSync(lock.name);
793
- syncCurrentDirectory();
794
- }
795
-
796
- function withUpdateLock(sid, fn) {
797
- const lock = acquireUpdateLock(sid);
798
- let value;
799
- let operationError = null;
800
- try {
801
- value = fn();
802
- } catch (error) {
803
- operationError = error;
804
- }
805
- try {
806
- releaseUpdateLock(lock);
807
- } catch (releaseError) {
808
- if (!operationError) throw releaseError;
809
- }
810
- if (operationError) throw operationError;
811
- return value;
812
- }
813
-
814
- function validateUpdatePatch(patch) {
815
- if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
816
- throw new SessionCloseJournalError(
817
- 'SESSION_CLOSE_JOURNAL_INVALID',
818
- 'session close journal update patch must be an object',
819
- );
820
- }
821
- const keys = Object.keys(patch).sort();
822
- if (keys.length === 0
823
- || keys.some((key) => !['error', 'phase'].includes(key))
824
- || !Object.hasOwn(patch, 'phase')) {
825
- throw new SessionCloseJournalError(
826
- 'SESSION_CLOSE_JOURNAL_INVALID',
827
- 'session close journal updates may only set phase and error',
828
- );
829
- }
830
- if (!JOURNAL_PHASES.has(patch.phase)) {
831
- throw new SessionCloseJournalError(
832
- 'SESSION_CLOSE_JOURNAL_INVALID',
833
- `unsupported session close journal phase: ${String(patch.phase)}`,
834
- );
835
- }
836
- if (Object.hasOwn(patch, 'error') && patch.error !== null) {
837
- assertBoundedString(patch.error, 'error', { max: 8192, allowEmpty: true });
838
- }
839
- return patch;
840
- }
841
-
842
- function helperUpdate(request) {
843
- assertSid(request.sid);
844
- validateUpdatePatch(request.patch);
845
- if (request.expected_generation !== null
846
- && (!Number.isSafeInteger(request.expected_generation)
847
- || request.expected_generation < 0)) {
848
- throw new SessionCloseJournalError(
849
- 'SESSION_CLOSE_JOURNAL_INVALID',
850
- 'expected_generation must be a non-negative safe integer',
851
- );
852
- }
853
- return withUpdateLock(request.sid, () => {
854
- const current = readIntentFile(request.sid);
855
- if (!current) {
856
- throw new SessionCloseJournalError(
857
- 'SESSION_CLOSE_JOURNAL_MISSING',
858
- `session ${request.sid} has no close journal`,
859
- );
860
- }
861
- if (request.expected_generation !== null
862
- && current.generation !== request.expected_generation) {
863
- throw new SessionCloseJournalError(
864
- 'SESSION_CLOSE_CONFLICT',
865
- `session ${request.sid} close journal changed concurrently`,
866
- {
867
- details: {
868
- expected_generation: request.expected_generation,
869
- actual_generation: current.generation,
870
- },
871
- },
872
- );
873
- }
874
- const nextError = Object.hasOwn(request.patch, 'error')
875
- ? request.patch.error
876
- : current.error;
877
- if (request.patch.phase === current.phase) {
878
- if (nextError === current.error) return current;
879
- throw new SessionCloseJournalError(
880
- 'SESSION_CLOSE_CONFLICT',
881
- `session ${request.sid} close journal phase evidence cannot be overwritten`,
882
- );
883
- }
884
- if (!PHASE_TRANSITIONS[current.phase]?.has(request.patch.phase)) {
885
- throw new SessionCloseJournalError(
886
- 'SESSION_CLOSE_CONFLICT',
887
- `session ${request.sid} close journal cannot transition from ${current.phase} to ${request.patch.phase}`,
888
- );
889
- }
890
- const next = validateIntent({
891
- ...current,
892
- version: JOURNAL_VERSION,
893
- generation: current.generation + 1,
894
- phase: request.patch.phase,
895
- error: nextError,
896
- updated_at: new Date().toISOString(),
897
- }, intentFilename(request.sid), request.sid);
898
- writeReplacement(next, request.sid);
899
- return next;
900
- });
901
- }
902
-
903
- function helperDiscard(request) {
904
- assertSid(request.sid);
905
- if (request.expected_generation !== null
906
- && (!Number.isSafeInteger(request.expected_generation)
907
- || request.expected_generation < 0)) {
908
- throw new SessionCloseJournalError(
909
- 'SESSION_CLOSE_JOURNAL_INVALID',
910
- 'expected_generation must be a non-negative safe integer',
911
- );
912
- }
913
- return withUpdateLock(request.sid, () => {
914
- const current = readIntentFile(request.sid);
915
- if (!current) return false;
916
- if (request.expected_generation !== null
917
- && current.generation !== request.expected_generation) {
918
- throw new SessionCloseJournalError(
919
- 'SESSION_CLOSE_CONFLICT',
920
- `session ${request.sid} close journal changed before discard`,
921
- );
922
- }
923
- fs.unlinkSync(intentFilename(request.sid));
924
- syncCurrentDirectory();
925
- return true;
926
- });
927
- }
928
-
929
- function helperList() {
930
- const entries = fs.readdirSync('.', { withFileTypes: true })
931
- .filter((entry) => entry.name.endsWith('.json'))
932
- .sort((left, right) => left.name.localeCompare(right.name));
933
- return entries.map((entry) => {
934
- if (!entry.isFile() || entry.isSymbolicLink()) {
935
- throw new SessionCloseJournalError(
936
- 'SESSION_CLOSE_JOURNAL_INVALID',
937
- `session close journal is not a regular file: ${entry.name}`,
938
- );
939
- }
940
- const sid = entry.name.slice(0, -'.json'.length);
941
- assertSid(sid);
942
- return readIntentFile(sid);
943
- });
944
- }
945
-
946
- function executeHelper(request) {
947
- if (!request || typeof request !== 'object' || Array.isArray(request)) {
948
- throw new SessionCloseJournalError(
949
- 'SESSION_CLOSE_JOURNAL_INVALID',
950
- 'session close journal helper request must be an object',
951
- );
952
- }
953
- enterPinnedJournalRoot(request.runtime_identity);
954
- switch (request.op) {
955
- case 'prepare':
956
- return helperPrepare(request);
957
- case 'read':
958
- return readIntentFile(assertSid(request.sid));
959
- case 'update':
960
- return helperUpdate(request);
961
- case 'discard':
962
- return helperDiscard(request);
963
- case 'list':
964
- return helperList();
965
- default:
966
- throw new SessionCloseJournalError(
967
- 'SESSION_CLOSE_JOURNAL_INVALID',
968
- `unknown session close journal operation: ${String(request.op)}`,
969
- );
970
- }
971
- }
972
-
973
- function serializeError(error) {
974
- return {
975
- code: error?.code || 'SESSION_CLOSE_JOURNAL_HELPER_FAILED',
976
- message: error?.message || String(error),
977
- details: error?.details,
978
- };
979
- }
980
-
981
- function helperMain() {
982
- try {
983
- const input = fs.readFileSync(0, 'utf8');
984
- const value = executeHelper(JSON.parse(input));
985
- process.stdout.write(JSON.stringify({ ok: true, value }));
986
- } catch (error) {
987
- process.stdout.write(JSON.stringify({ ok: false, error: serializeError(error) }));
988
- process.exitCode = 1;
989
- }
990
- }
991
-
992
- module.exports = {
993
- JOURNAL_VERSION,
994
- JOURNAL_PHASES,
995
- TERMINAL_STATUSES,
996
- SessionCloseJournalError,
997
- journalRoot,
998
- journalPath,
999
- prepare,
1000
- read,
1001
- update,
1002
- discard,
1003
- list,
1004
- findForTask,
1005
- _internal: {
1006
- validateIntent,
1007
- runHelper,
1008
- workerPath,
1009
- helperMain,
1010
- enterPinnedJournalRoot,
1011
- acquireUpdateLock,
1012
- processStartMarker,
1013
- },
1014
- };