@naxodev/apnea 0.2.0 → 0.2.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.
- package/README.md +18 -1
- package/SECURITY.md +36 -0
- package/briefs/orchestrator.md +4 -3
- package/dist/cli.js +8375 -15093
- package/docs/protocol/artifacts.md +18 -2
- package/docs/protocol/config.md +15 -3
- package/docs/protocol/manual-gate.md +8 -8
- package/docs/protocol/overview.md +17 -4
- package/extension/adapters/commit.ts +5 -1
- package/extension/adapters/dispatch.ts +9 -1
- package/extension/adapters/setup.ts +15 -1
- package/extension/adapters/start.ts +5 -1
- package/extension/adapters/status.ts +17 -2
- package/extension/adapters/wait.ts +6 -1
- package/extension/api.ts +7 -1
- package/extension/cli/main.ts +67 -7
- package/extension/cli/parse.ts +172 -5
- package/extension/domain/paths.ts +2 -11
- package/extension/domain/timeouts.ts +4 -0
- package/extension/domain/types.ts +65 -3
- package/extension/errors.ts +51 -16
- package/extension/operation-hooks.ts +6 -0
- package/extension/registry.ts +29 -15
- package/extension/run-tool.ts +19 -2
- package/extension/schema/config.ts +58 -16
- package/extension/schema/frontmatter.ts +57 -0
- package/extension/schema/state.ts +210 -13
- package/extension/services/app-live.ts +2 -1
- package/extension/services/config.ts +6 -4
- package/extension/services/file-system.ts +346 -75
- package/extension/services/herdr.ts +466 -260
- package/extension/services/operation-lock.ts +452 -0
- package/extension/services/process.ts +477 -0
- package/extension/services/run-store.ts +38 -16
- package/extension/services/vcs.ts +1258 -328
- package/extension/workflows/commit.ts +214 -13
- package/extension/workflows/dispatch.ts +305 -67
- package/extension/workflows/setup.ts +59 -32
- package/extension/workflows/start.ts +6 -4
- package/extension/workflows/status.ts +2 -2
- package/extension/workflows/wait.ts +62 -77
- package/package.json +2 -2
- package/schemas/config.schema.json +5 -1
- package/schemas/state.schema.json +165 -11
|
@@ -16,8 +16,14 @@ import {
|
|
|
16
16
|
import { ok, type ToolResult } from "../result.ts"
|
|
17
17
|
import { Config } from "../services/config.ts"
|
|
18
18
|
import { FileSystem } from "../services/file-system.ts"
|
|
19
|
-
import { RunStore } from "../services/run-store.ts"
|
|
20
|
-
import {
|
|
19
|
+
import { RunStore, type RunStoreService } from "../services/run-store.ts"
|
|
20
|
+
import {
|
|
21
|
+
Vcs,
|
|
22
|
+
type VcsService,
|
|
23
|
+
withTransactionTrailer,
|
|
24
|
+
withoutTransactionTrailer,
|
|
25
|
+
} from "../services/vcs.ts"
|
|
26
|
+
import type { RunState } from "../domain/types.ts"
|
|
21
27
|
|
|
22
28
|
export type CommitParams = {
|
|
23
29
|
message?: string
|
|
@@ -28,6 +34,18 @@ export type CommitParams = {
|
|
|
28
34
|
/**
|
|
29
35
|
* Require APPROVED code review, run phase package verify commands,
|
|
30
36
|
* jj/git commit, advance phase. Refusals are tagged failures only.
|
|
37
|
+
*
|
|
38
|
+
* The commit itself is a crash-recoverable transaction:
|
|
39
|
+
*
|
|
40
|
+
* gates → verify.log → prepare → save pending_commit (durable point)
|
|
41
|
+
* → complete or recognize → bookmark (jj, final phase) → advance + clear
|
|
42
|
+
*
|
|
43
|
+
* Once `pending_commit` is saved, cancellation does NOT undo the
|
|
44
|
+
* transaction — the prepared anchor is durable and a later commit call
|
|
45
|
+
* resumes or recognizes it. A call that loads an existing `pending_commit`
|
|
46
|
+
* skips the gates and verification entirely and resumes only that
|
|
47
|
+
* transaction; retry params conflicting with the persisted message or
|
|
48
|
+
* `--done` value are refused, not silently ignored.
|
|
31
49
|
*/
|
|
32
50
|
export const commitWorkflow = (
|
|
33
51
|
params: CommitParams,
|
|
@@ -46,6 +64,16 @@ export const commitWorkflow = (
|
|
|
46
64
|
return yield* allowed.failure
|
|
47
65
|
}
|
|
48
66
|
|
|
67
|
+
if (state.pending_commit) {
|
|
68
|
+
return yield* resumePendingCommit({
|
|
69
|
+
params,
|
|
70
|
+
root,
|
|
71
|
+
state,
|
|
72
|
+
store,
|
|
73
|
+
vcs,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
const reviewRel = state.current_code_review
|
|
50
78
|
if (!reviewRel) {
|
|
51
79
|
return yield* new GateRefused({
|
|
@@ -55,10 +83,18 @@ export const commitWorkflow = (
|
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
const reviewAbs = abs(reviewRel, root)
|
|
58
|
-
const reviewPresent = yield* fs.
|
|
86
|
+
const reviewPresent = yield* fs.projectPathExists(root, reviewAbs)
|
|
59
87
|
let fm: ReturnType<typeof parseFrontMatter> = null
|
|
60
88
|
if (reviewPresent) {
|
|
61
|
-
const reviewText = yield* fs.
|
|
89
|
+
const reviewText = yield* fs.readProjectFile(root, reviewAbs).pipe(
|
|
90
|
+
Effect.mapError(
|
|
91
|
+
(error) =>
|
|
92
|
+
new ArtifactInvalid({
|
|
93
|
+
artifact: reviewRel,
|
|
94
|
+
message: error.message,
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
62
98
|
fm = parseFrontMatter(reviewText)
|
|
63
99
|
}
|
|
64
100
|
const verdict = asVerdict(fm?.verdict)
|
|
@@ -80,7 +116,7 @@ export const commitWorkflow = (
|
|
|
80
116
|
root,
|
|
81
117
|
)
|
|
82
118
|
const pkgAbs = abs(pkgRel, root)
|
|
83
|
-
const pkgPresent = yield* fs.
|
|
119
|
+
const pkgPresent = yield* fs.projectPathExists(root, pkgAbs)
|
|
84
120
|
if (!pkgPresent) {
|
|
85
121
|
return yield* new GateRefused({
|
|
86
122
|
gate: "commit",
|
|
@@ -88,7 +124,14 @@ export const commitWorkflow = (
|
|
|
88
124
|
details: { package: pkgRel },
|
|
89
125
|
})
|
|
90
126
|
}
|
|
91
|
-
const pkgText = yield* fs
|
|
127
|
+
const pkgText = yield* fs
|
|
128
|
+
.readProjectFile(root, pkgAbs)
|
|
129
|
+
.pipe(
|
|
130
|
+
Effect.mapError(
|
|
131
|
+
(error) =>
|
|
132
|
+
new ArtifactInvalid({ artifact: pkgRel, message: error.message }),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
92
135
|
const blocks = extractVerifyBlocks(pkgText)
|
|
93
136
|
if (!blocks.length) {
|
|
94
137
|
return yield* new ArtifactInvalid({
|
|
@@ -102,8 +145,7 @@ export const commitWorkflow = (
|
|
|
102
145
|
const verify = yield* vcs.runVerify(root, blocks, verifyTimeout)
|
|
103
146
|
|
|
104
147
|
const vlog = path.join(path.dirname(reviewAbs), "verify.log")
|
|
105
|
-
yield* fs.
|
|
106
|
-
yield* fs.writeFile(vlog, `${verify.log}\n`)
|
|
148
|
+
yield* fs.writeProjectFile(root, vlog, `${verify.log}\n`)
|
|
107
149
|
|
|
108
150
|
if (!verify.ok) {
|
|
109
151
|
return yield* new VerifyFailed({
|
|
@@ -118,9 +160,162 @@ export const commitWorkflow = (
|
|
|
118
160
|
params.message?.trim() ||
|
|
119
161
|
`feat: apnea phase ${state.phase_index} (${state.slug})`
|
|
120
162
|
|
|
121
|
-
const
|
|
163
|
+
const prepared = yield* vcs.prepareCommit(root, state.vcs, message)
|
|
164
|
+
|
|
165
|
+
// Durable point. From here the transaction survives crashes and
|
|
166
|
+
// cancellation: the anchor below is everything completion needs to
|
|
167
|
+
// recognize or create the commit exactly once.
|
|
168
|
+
const noRemainingPhases = params.no_remaining_phases === true
|
|
169
|
+
const verifyLogRel = rel(vlog, root)
|
|
170
|
+
state.pending_commit =
|
|
171
|
+
prepared.backend === "git"
|
|
172
|
+
? {
|
|
173
|
+
id: prepared.id,
|
|
174
|
+
backend: prepared.backend,
|
|
175
|
+
phase_index: state.phase_index,
|
|
176
|
+
message: prepared.message,
|
|
177
|
+
no_remaining_phases: noRemainingPhases,
|
|
178
|
+
verify_log: verifyLogRel,
|
|
179
|
+
branch: prepared.branch,
|
|
180
|
+
parent_commit: prepared.parent_commit,
|
|
181
|
+
tree_id: prepared.tree_id,
|
|
182
|
+
}
|
|
183
|
+
: {
|
|
184
|
+
id: prepared.id,
|
|
185
|
+
backend: prepared.backend,
|
|
186
|
+
phase_index: state.phase_index,
|
|
187
|
+
message: prepared.message,
|
|
188
|
+
no_remaining_phases: noRemainingPhases,
|
|
189
|
+
verify_log: verifyLogRel,
|
|
190
|
+
change_id: prepared.change_id,
|
|
191
|
+
content_fingerprint: prepared.content_fingerprint,
|
|
192
|
+
}
|
|
193
|
+
yield* store.save(state, root)
|
|
194
|
+
|
|
195
|
+
const committedId = yield* vcs.completeCommit(
|
|
196
|
+
root,
|
|
197
|
+
state.vcs,
|
|
198
|
+
state.pending_commit,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return yield* advanceAndSave({
|
|
202
|
+
state,
|
|
203
|
+
store,
|
|
204
|
+
vcs,
|
|
205
|
+
root,
|
|
206
|
+
noRemainingPhases,
|
|
207
|
+
committedId,
|
|
208
|
+
recovered: false,
|
|
209
|
+
transactionId: state.pending_commit.id,
|
|
210
|
+
verifyLog: verifyLogRel,
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
type ResumeArgs = {
|
|
215
|
+
params: CommitParams
|
|
216
|
+
root: string
|
|
217
|
+
state: RunState
|
|
218
|
+
store: RunStoreService
|
|
219
|
+
vcs: VcsService
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resume the durable transaction only: no gates, no verification.
|
|
224
|
+
* Explicit retry params conflicting with the persisted values are refused.
|
|
225
|
+
*/
|
|
226
|
+
function resumePendingCommit({
|
|
227
|
+
params,
|
|
228
|
+
root,
|
|
229
|
+
state,
|
|
230
|
+
store,
|
|
231
|
+
vcs,
|
|
232
|
+
}: ResumeArgs): Effect.Effect<ToolResult, AppError> {
|
|
233
|
+
return Effect.gen(function* () {
|
|
234
|
+
const pending = state.pending_commit!
|
|
235
|
+
const requestedMessage = params.message?.trim()
|
|
236
|
+
// Compare against the exact trailer-augmented form the transaction will
|
|
237
|
+
// write, not a stripped persisted message: stripping only removes the
|
|
238
|
+
// LAST trailer line, so a user message that legitimately ends with a
|
|
239
|
+
// trailer-shaped line would otherwise refuse an identical plain retry.
|
|
240
|
+
if (
|
|
241
|
+
requestedMessage !== undefined &&
|
|
242
|
+
requestedMessage !== pending.message &&
|
|
243
|
+
withTransactionTrailer(requestedMessage, pending.id) !== pending.message
|
|
244
|
+
) {
|
|
245
|
+
return yield* new GateRefused({
|
|
246
|
+
gate: "commit",
|
|
247
|
+
message:
|
|
248
|
+
"conflicting retry: this call already has a durable commit transaction with a different message",
|
|
249
|
+
details: {
|
|
250
|
+
transaction: pending.id,
|
|
251
|
+
persisted_message: withoutTransactionTrailer(pending.message),
|
|
252
|
+
requested_message: requestedMessage,
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
params.no_remaining_phases !== undefined &&
|
|
258
|
+
params.no_remaining_phases !== pending.no_remaining_phases
|
|
259
|
+
) {
|
|
260
|
+
return yield* new GateRefused({
|
|
261
|
+
gate: "commit",
|
|
262
|
+
message:
|
|
263
|
+
"conflicting retry: this call already has a durable commit transaction with a different no_remaining_phases value",
|
|
264
|
+
details: {
|
|
265
|
+
transaction: pending.id,
|
|
266
|
+
persisted_no_remaining_phases: pending.no_remaining_phases,
|
|
267
|
+
requested_no_remaining_phases: params.no_remaining_phases,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const committedId = yield* vcs.completeCommit(root, state.vcs, pending)
|
|
273
|
+
|
|
274
|
+
return yield* advanceAndSave({
|
|
275
|
+
state,
|
|
276
|
+
store,
|
|
277
|
+
vcs,
|
|
278
|
+
root,
|
|
279
|
+
noRemainingPhases: pending.no_remaining_phases,
|
|
280
|
+
committedId,
|
|
281
|
+
recovered: true,
|
|
282
|
+
transactionId: pending.id,
|
|
283
|
+
verifyLog: pending.verify_log,
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type AdvanceArgs = {
|
|
289
|
+
state: RunState
|
|
290
|
+
store: RunStoreService
|
|
291
|
+
vcs: VcsService
|
|
292
|
+
root: string
|
|
293
|
+
noRemainingPhases: boolean
|
|
294
|
+
committedId: string
|
|
295
|
+
recovered: boolean
|
|
296
|
+
transactionId: string
|
|
297
|
+
/** Repo-relative verify.log path captured before the phase advanced. */
|
|
298
|
+
verifyLog: string
|
|
299
|
+
}
|
|
122
300
|
|
|
123
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Advance the run after completion. A bookmark failure propagates BEFORE any
|
|
303
|
+
* state mutation is saved, so `pending_commit` stays durable and the
|
|
304
|
+
* transaction remains resumable on the next call.
|
|
305
|
+
*/
|
|
306
|
+
function advanceAndSave({
|
|
307
|
+
state,
|
|
308
|
+
store,
|
|
309
|
+
vcs,
|
|
310
|
+
root,
|
|
311
|
+
noRemainingPhases,
|
|
312
|
+
committedId,
|
|
313
|
+
recovered,
|
|
314
|
+
transactionId,
|
|
315
|
+
verifyLog,
|
|
316
|
+
}: AdvanceArgs): Effect.Effect<ToolResult, AppError> {
|
|
317
|
+
return Effect.gen(function* () {
|
|
318
|
+
if (noRemainingPhases) {
|
|
124
319
|
state.step = "finishing"
|
|
125
320
|
if (state.vcs === "jj") {
|
|
126
321
|
yield* vcs.setBookmarkAtTerminus(root, state.slug)
|
|
@@ -132,13 +327,18 @@ export const commitWorkflow = (
|
|
|
132
327
|
state.current_code_review = null
|
|
133
328
|
}
|
|
134
329
|
state.last_error = null
|
|
330
|
+
state.pending_commit = null
|
|
135
331
|
yield* store.save(state, root)
|
|
136
332
|
|
|
333
|
+
const prefix = recovered
|
|
334
|
+
? `committed phase (recovered transaction ${transactionId})`
|
|
335
|
+
: "committed phase"
|
|
137
336
|
return ok(
|
|
138
|
-
|
|
337
|
+
`${prefix}; step → ${state.step}`,
|
|
139
338
|
{
|
|
140
|
-
vcs_detail:
|
|
141
|
-
|
|
339
|
+
vcs_detail: `${state.vcs} commit ${committedId}`,
|
|
340
|
+
transaction: transactionId,
|
|
341
|
+
verify_log: verifyLog,
|
|
142
342
|
step: state.step,
|
|
143
343
|
phase_index: state.phase_index,
|
|
144
344
|
next:
|
|
@@ -149,3 +349,4 @@ export const commitWorkflow = (
|
|
|
149
349
|
nextAfter(state.step),
|
|
150
350
|
)
|
|
151
351
|
})
|
|
352
|
+
}
|