@naxodev/apnea 0.1.0 → 0.2.1
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 +25 -10
- package/SECURITY.md +32 -0
- package/briefs/orchestrator.md +4 -3
- package/dist/cli.js +8571 -15324
- package/docs/adr/0005-harness-profiles.md +1 -1
- package/docs/adr/0010-package-split.md +1 -1
- package/docs/protocol/artifacts.md +18 -2
- package/docs/protocol/config.md +22 -25
- package/docs/protocol/manual-gate.md +8 -8
- package/docs/protocol/overview.md +18 -5
- 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/herdr.ts +0 -86
- package/extension/domain/paths.ts +3 -13
- package/extension/domain/setup.ts +0 -20
- package/extension/domain/timeouts.ts +4 -0
- package/extension/domain/types.ts +64 -11
- package/extension/domain/verify-commands.ts +200 -108
- 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 +86 -30
- package/extension/schema/frontmatter.ts +57 -0
- package/extension/schema/state.ts +226 -16
- 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 +393 -402
- package/extension/services/operation-lock.ts +418 -0
- package/extension/services/process.ts +477 -0
- package/extension/services/run-store.ts +38 -16
- package/extension/services/vcs.ts +1388 -86
- package/extension/workflows/commit.ts +222 -18
- package/extension/workflows/dispatch.ts +320 -220
- package/extension/workflows/setup.ts +61 -141
- package/extension/workflows/start.ts +6 -5
- package/extension/workflows/status.ts +2 -2
- package/extension/workflows/wait.ts +63 -134
- package/package.json +2 -3
- package/schemas/config.schema.json +11 -7
- package/schemas/state.schema.json +170 -12
- package/herdr-plugin/herdr-plugin.toml +0 -15
- package/herdr-plugin/scripts/run-task.sh +0 -8
|
@@ -3,7 +3,10 @@ import { Effect, Result } from "effect"
|
|
|
3
3
|
import { asVerdict, parseFrontMatter } from "../domain/frontmatter.ts"
|
|
4
4
|
import { abs, phaseDir, rel } from "../domain/paths.ts"
|
|
5
5
|
import { nextAfter, toolAllowed } from "../domain/state-machine.ts"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
extractVerifyBlocks,
|
|
8
|
+
formatVerifyCommand,
|
|
9
|
+
} from "../domain/verify-commands.ts"
|
|
7
10
|
import {
|
|
8
11
|
ArtifactInvalid,
|
|
9
12
|
GateRefused,
|
|
@@ -13,8 +16,14 @@ import {
|
|
|
13
16
|
import { ok, type ToolResult } from "../result.ts"
|
|
14
17
|
import { Config } from "../services/config.ts"
|
|
15
18
|
import { FileSystem } from "../services/file-system.ts"
|
|
16
|
-
import { RunStore } from "../services/run-store.ts"
|
|
17
|
-
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"
|
|
18
27
|
|
|
19
28
|
export type CommitParams = {
|
|
20
29
|
message?: string
|
|
@@ -25,6 +34,18 @@ export type CommitParams = {
|
|
|
25
34
|
/**
|
|
26
35
|
* Require APPROVED code review, run phase package verify commands,
|
|
27
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.
|
|
28
49
|
*/
|
|
29
50
|
export const commitWorkflow = (
|
|
30
51
|
params: CommitParams,
|
|
@@ -43,6 +64,16 @@ export const commitWorkflow = (
|
|
|
43
64
|
return yield* allowed.failure
|
|
44
65
|
}
|
|
45
66
|
|
|
67
|
+
if (state.pending_commit) {
|
|
68
|
+
return yield* resumePendingCommit({
|
|
69
|
+
params,
|
|
70
|
+
root,
|
|
71
|
+
state,
|
|
72
|
+
store,
|
|
73
|
+
vcs,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
const reviewRel = state.current_code_review
|
|
47
78
|
if (!reviewRel) {
|
|
48
79
|
return yield* new GateRefused({
|
|
@@ -52,10 +83,18 @@ export const commitWorkflow = (
|
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
const reviewAbs = abs(reviewRel, root)
|
|
55
|
-
const reviewPresent = yield* fs.
|
|
86
|
+
const reviewPresent = yield* fs.projectPathExists(root, reviewAbs)
|
|
56
87
|
let fm: ReturnType<typeof parseFrontMatter> = null
|
|
57
88
|
if (reviewPresent) {
|
|
58
|
-
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
|
+
)
|
|
59
98
|
fm = parseFrontMatter(reviewText)
|
|
60
99
|
}
|
|
61
100
|
const verdict = asVerdict(fm?.verdict)
|
|
@@ -77,7 +116,7 @@ export const commitWorkflow = (
|
|
|
77
116
|
root,
|
|
78
117
|
)
|
|
79
118
|
const pkgAbs = abs(pkgRel, root)
|
|
80
|
-
const pkgPresent = yield* fs.
|
|
119
|
+
const pkgPresent = yield* fs.projectPathExists(root, pkgAbs)
|
|
81
120
|
if (!pkgPresent) {
|
|
82
121
|
return yield* new GateRefused({
|
|
83
122
|
gate: "commit",
|
|
@@ -85,9 +124,16 @@ export const commitWorkflow = (
|
|
|
85
124
|
details: { package: pkgRel },
|
|
86
125
|
})
|
|
87
126
|
}
|
|
88
|
-
const pkgText = yield* fs
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
)
|
|
135
|
+
const blocks = extractVerifyBlocks(pkgText)
|
|
136
|
+
if (!blocks.length) {
|
|
91
137
|
return yield* new ArtifactInvalid({
|
|
92
138
|
artifact: pkgRel,
|
|
93
139
|
message: "no verify commands found in phase package (need ```sh block)",
|
|
@@ -96,15 +142,14 @@ export const commitWorkflow = (
|
|
|
96
142
|
|
|
97
143
|
const cfg = yield* config.load(root)
|
|
98
144
|
const verifyTimeout = cfg.timeouts_ms.verify ?? 900_000
|
|
99
|
-
const verify = yield* vcs.runVerify(root,
|
|
145
|
+
const verify = yield* vcs.runVerify(root, blocks, verifyTimeout)
|
|
100
146
|
|
|
101
147
|
const vlog = path.join(path.dirname(reviewAbs), "verify.log")
|
|
102
|
-
yield* fs.
|
|
103
|
-
yield* fs.writeFile(vlog, `${verify.log}\n`)
|
|
148
|
+
yield* fs.writeProjectFile(root, vlog, `${verify.log}\n`)
|
|
104
149
|
|
|
105
150
|
if (!verify.ok) {
|
|
106
151
|
return yield* new VerifyFailed({
|
|
107
|
-
commands:
|
|
152
|
+
commands: blocks.map(formatVerifyCommand),
|
|
108
153
|
outputs: [verify.log.slice(-2000)],
|
|
109
154
|
// `outputs` is a tail — point the caller at the full log on disk.
|
|
110
155
|
verify_log: rel(vlog, root),
|
|
@@ -115,9 +160,162 @@ export const commitWorkflow = (
|
|
|
115
160
|
params.message?.trim() ||
|
|
116
161
|
`feat: apnea phase ${state.phase_index} (${state.slug})`
|
|
117
162
|
|
|
118
|
-
const
|
|
163
|
+
const prepared = yield* vcs.prepareCommit(root, state.vcs, message)
|
|
119
164
|
|
|
120
|
-
|
|
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
|
+
}
|
|
300
|
+
|
|
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) {
|
|
121
319
|
state.step = "finishing"
|
|
122
320
|
if (state.vcs === "jj") {
|
|
123
321
|
yield* vcs.setBookmarkAtTerminus(root, state.slug)
|
|
@@ -129,13 +327,18 @@ export const commitWorkflow = (
|
|
|
129
327
|
state.current_code_review = null
|
|
130
328
|
}
|
|
131
329
|
state.last_error = null
|
|
330
|
+
state.pending_commit = null
|
|
132
331
|
yield* store.save(state, root)
|
|
133
332
|
|
|
333
|
+
const prefix = recovered
|
|
334
|
+
? `committed phase (recovered transaction ${transactionId})`
|
|
335
|
+
: "committed phase"
|
|
134
336
|
return ok(
|
|
135
|
-
|
|
337
|
+
`${prefix}; step → ${state.step}`,
|
|
136
338
|
{
|
|
137
|
-
vcs_detail:
|
|
138
|
-
|
|
339
|
+
vcs_detail: `${state.vcs} commit ${committedId}`,
|
|
340
|
+
transaction: transactionId,
|
|
341
|
+
verify_log: verifyLog,
|
|
139
342
|
step: state.step,
|
|
140
343
|
phase_index: state.phase_index,
|
|
141
344
|
next:
|
|
@@ -146,3 +349,4 @@ export const commitWorkflow = (
|
|
|
146
349
|
nextAfter(state.step),
|
|
147
350
|
)
|
|
148
351
|
})
|
|
352
|
+
}
|