@naxodev/apnea 0.2.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 +18 -1
- package/SECURITY.md +32 -0
- package/briefs/orchestrator.md +4 -3
- package/dist/cli.js +8283 -15058
- 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 +389 -242
- 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 +1258 -328
- package/extension/workflows/commit.ts +214 -13
- package/extension/workflows/dispatch.ts +274 -57
- 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
|
@@ -47,7 +47,7 @@ export const startWorkflow = (
|
|
|
47
47
|
let pendingStatus: string = "none"
|
|
48
48
|
if (pending) {
|
|
49
49
|
const absPath = pending.startsWith("/") ? pending : `${root}/${pending}`
|
|
50
|
-
const present = yield* fs.
|
|
50
|
+
const present = yield* fs.projectPathExists(root, absPath)
|
|
51
51
|
pendingStatus = present ? "artifact_exists" : "artifact_missing"
|
|
52
52
|
}
|
|
53
53
|
return ok(
|
|
@@ -59,7 +59,7 @@ export const startWorkflow = (
|
|
|
59
59
|
pendingStatus === "artifact_exists"
|
|
60
60
|
? "call workflow_wait to ingest pending artifact"
|
|
61
61
|
: pendingStatus === "artifact_missing"
|
|
62
|
-
? "offer
|
|
62
|
+
? "offer same-round dispatch_role redeliver=true after proving the prior delivery is dead"
|
|
63
63
|
: "inspect workflow_status and continue legal next step",
|
|
64
64
|
},
|
|
65
65
|
nextAfter(existing.step),
|
|
@@ -100,7 +100,7 @@ export const startWorkflow = (
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const state: RunState = {
|
|
103
|
-
version:
|
|
103
|
+
version: 2,
|
|
104
104
|
slug,
|
|
105
105
|
step: "planning",
|
|
106
106
|
phase_index: 1,
|
|
@@ -112,6 +112,7 @@ export const startWorkflow = (
|
|
|
112
112
|
last_error: null,
|
|
113
113
|
pending_artifact: null,
|
|
114
114
|
pending_role: null,
|
|
115
|
+
pending_delivery: null,
|
|
115
116
|
pending_pane_id: null,
|
|
116
117
|
pending_pane_label: null,
|
|
117
118
|
pending_started_at: null,
|
|
@@ -124,7 +125,8 @@ export const startWorkflow = (
|
|
|
124
125
|
reviewer_tree_fingerprint: null,
|
|
125
126
|
current_phase_package: null,
|
|
126
127
|
current_code_review: null,
|
|
127
|
-
|
|
128
|
+
required_rework: null,
|
|
129
|
+
pending_commit: null,
|
|
128
130
|
}
|
|
129
131
|
// The literal above assigns the ladder's fields; this re-asserts them
|
|
130
132
|
// through the shared helper so a rung added there cannot be missed here.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Result } from "effect"
|
|
2
2
|
import { LEGAL_TOOLS, nextAfter } from "../domain/state-machine.ts"
|
|
3
|
-
import type {
|
|
3
|
+
import type { AppError } from "../errors.ts"
|
|
4
4
|
import { ok, type ToolResult } from "../result.ts"
|
|
5
5
|
import { Config } from "../services/config.ts"
|
|
6
6
|
import { RunStore } from "../services/run-store.ts"
|
|
@@ -12,7 +12,7 @@ import { Vcs } from "../services/vcs.ts"
|
|
|
12
12
|
*/
|
|
13
13
|
export const statusWorkflow = (
|
|
14
14
|
root: string,
|
|
15
|
-
): Effect.Effect<ToolResult,
|
|
15
|
+
): Effect.Effect<ToolResult, AppError, RunStore | Config | Vcs> =>
|
|
16
16
|
Effect.gen(function* () {
|
|
17
17
|
const store = yield* RunStore
|
|
18
18
|
const config = yield* Config
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clock, Effect, Exit, Option, Result } from "effect"
|
|
2
2
|
import { resetRecoveryLadder } from "../domain/recovery.ts"
|
|
3
3
|
import { inferKind } from "../domain/artifact-kind.ts"
|
|
4
|
-
import { timeoutMsForKind } from "../domain/timeouts.ts"
|
|
4
|
+
import { deadlineAfter, timeoutMsForKind } from "../domain/timeouts.ts"
|
|
5
5
|
import {
|
|
6
6
|
asVerdict,
|
|
7
7
|
isCompleteArtifact,
|
|
@@ -9,11 +9,7 @@ import {
|
|
|
9
9
|
} from "../domain/frontmatter.ts"
|
|
10
10
|
import { looksLikeShellOnly, shellJoin } from "../domain/herdr.ts"
|
|
11
11
|
import { abs, rel } from "../domain/paths.ts"
|
|
12
|
-
import {
|
|
13
|
-
nextAfter,
|
|
14
|
-
stepAfterArtifact,
|
|
15
|
-
toolAllowed,
|
|
16
|
-
} from "../domain/state-machine.ts"
|
|
12
|
+
import { nextAfter, toolAllowed } from "../domain/state-machine.ts"
|
|
17
13
|
import {
|
|
18
14
|
ArtifactInvalid,
|
|
19
15
|
GateRefused,
|
|
@@ -24,12 +20,16 @@ import {
|
|
|
24
20
|
} from "../errors.ts"
|
|
25
21
|
import type { FrontMatter } from "../domain/types.ts"
|
|
26
22
|
import { ok, type ToolResult } from "../result.ts"
|
|
27
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
validateArtifactCompletion,
|
|
25
|
+
type AcceptedArtifactCompletion,
|
|
26
|
+
} from "../schema/frontmatter.ts"
|
|
28
27
|
import { Config } from "../services/config.ts"
|
|
29
28
|
import { FileSystem } from "../services/file-system.ts"
|
|
30
29
|
import { Herdr, paneReadRecentArgs } from "../services/herdr.ts"
|
|
31
30
|
import { RunStore } from "../services/run-store.ts"
|
|
32
31
|
import { Vcs } from "../services/vcs.ts"
|
|
32
|
+
import type { OperationHooks } from "../operation-hooks.ts"
|
|
33
33
|
|
|
34
34
|
export type WaitParams = {
|
|
35
35
|
poll_ms?: number
|
|
@@ -151,12 +151,7 @@ export const MAX_AUTO_POLL_MS =
|
|
|
151
151
|
export const fitsHostShell = (pollMs: number): boolean =>
|
|
152
152
|
defaultBudgetFor(pollMs) < HOST_SHELL_TIMEOUT_MS
|
|
153
153
|
|
|
154
|
-
export type WaitHooks =
|
|
155
|
-
signal?: AbortSignal
|
|
156
|
-
onUpdate?: (partial: {
|
|
157
|
-
content: Array<{ type: "text"; text: string }>
|
|
158
|
-
}) => void
|
|
159
|
-
}
|
|
154
|
+
export type WaitHooks = OperationHooks
|
|
160
155
|
|
|
161
156
|
/**
|
|
162
157
|
* Async wait — polls via `Clock` + `Effect.sleep` so Pi stays responsive and
|
|
@@ -202,10 +197,10 @@ export const waitWorkflow = (
|
|
|
202
197
|
const cfg = yield* config.load(root)
|
|
203
198
|
|
|
204
199
|
const poll = params.poll_ms ?? 2000
|
|
205
|
-
if (!Number.
|
|
200
|
+
if (!Number.isSafeInteger(poll) || poll < MIN_POLL_MS) {
|
|
206
201
|
return yield* new GateRefused({
|
|
207
202
|
gate: "poll_interval",
|
|
208
|
-
message: `poll_ms must be a
|
|
203
|
+
message: `poll_ms must be a safe integer >= ${MIN_POLL_MS}; got ${poll}. Every poll spawns two herdr subprocesses, so a tiny interval is a busy-spin, not a faster wait.`,
|
|
209
204
|
})
|
|
210
205
|
}
|
|
211
206
|
// Refuse to PICK a budget that a default host shell would kill; still
|
|
@@ -232,10 +227,10 @@ export const waitWorkflow = (
|
|
|
232
227
|
// NaN, and the call blocks until the role's deadline instead of
|
|
233
228
|
// returning exit 3. The CLI is protected by `parseNumFlag`; a Pi tool
|
|
234
229
|
// call reaches `run` through a raw cast with no runtime coercion.
|
|
235
|
-
if (!Number.
|
|
230
|
+
if (!Number.isSafeInteger(budget) || budget <= 0) {
|
|
236
231
|
return yield* new GateRefused({
|
|
237
232
|
gate: "budget_floor",
|
|
238
|
-
message: `budget_ms must be a
|
|
233
|
+
message: `budget_ms must be a positive safe integer; got ${budget}`,
|
|
239
234
|
})
|
|
240
235
|
}
|
|
241
236
|
const floor = minBudgetFor(poll)
|
|
@@ -273,35 +268,33 @@ export const waitWorkflow = (
|
|
|
273
268
|
const dispatchedAt = state.pending_started_at ?? startedMs
|
|
274
269
|
if (state.pending_deadline_ms == null) {
|
|
275
270
|
state.pending_started_at = dispatchedAt
|
|
276
|
-
state.pending_deadline_ms = dispatchedAt
|
|
271
|
+
state.pending_deadline_ms = deadlineAfter(dispatchedAt, timeout)
|
|
277
272
|
yield* store.save(state, root)
|
|
278
273
|
}
|
|
279
274
|
const requireVerdict = kind === "plan_review" || kind === "code_review"
|
|
280
275
|
|
|
281
|
-
const readArtifact = (): Effect.Effect<FrontMatter | null> =>
|
|
276
|
+
const readArtifact = (): Effect.Effect<FrontMatter | null, AppError> =>
|
|
282
277
|
Effect.gen(function* () {
|
|
283
|
-
const present = yield* fs.
|
|
278
|
+
const present = yield* fs.projectPathExists(root, artifactAbs)
|
|
284
279
|
if (!present) return null
|
|
285
|
-
const text = yield* fs.
|
|
280
|
+
const text = yield* fs.readProjectFile(root, artifactAbs).pipe(
|
|
281
|
+
Effect.mapError(
|
|
282
|
+
(error) =>
|
|
283
|
+
new ArtifactInvalid({
|
|
284
|
+
artifact: pendingArtifact,
|
|
285
|
+
message: error.message,
|
|
286
|
+
}),
|
|
287
|
+
),
|
|
288
|
+
)
|
|
286
289
|
return parseFrontMatter(text)
|
|
287
290
|
})
|
|
288
291
|
|
|
289
292
|
const advanceOnComplete = (
|
|
290
293
|
fm: FrontMatter,
|
|
294
|
+
completion: AcceptedArtifactCompletion,
|
|
291
295
|
msg = "artifact ready",
|
|
292
296
|
): Effect.Effect<ToolResult, AppError> =>
|
|
293
297
|
Effect.gen(function* () {
|
|
294
|
-
if (
|
|
295
|
-
fm.rework !== undefined &&
|
|
296
|
-
(kind !== "code_review" || fm.verdict !== "CHANGES_REQUIRED")
|
|
297
|
-
) {
|
|
298
|
-
return yield* new ArtifactInvalid({
|
|
299
|
-
artifact: pendingArtifact,
|
|
300
|
-
message:
|
|
301
|
-
"rework is valid only on a code_review artifact with verdict CHANGES_REQUIRED",
|
|
302
|
-
})
|
|
303
|
-
}
|
|
304
|
-
|
|
305
298
|
if (
|
|
306
299
|
state.pending_role === "reviewer" &&
|
|
307
300
|
state.reviewer_tree_fingerprint != null
|
|
@@ -323,51 +316,26 @@ export const waitWorkflow = (
|
|
|
323
316
|
}
|
|
324
317
|
}
|
|
325
318
|
|
|
326
|
-
|
|
327
|
-
// completeness test. A bad/absent verdict must keep waiting, not fail
|
|
328
|
-
// here (that already happened before advanceOnComplete was called).
|
|
329
|
-
//
|
|
330
|
-
// Only review kinds carry a meaningful verdict. A stray `verdict:` on
|
|
331
|
-
// a plan/code/package artifact is noise the old parser ignored;
|
|
332
|
-
// validating it here would wedge the run permanently (state is not
|
|
333
|
-
// saved on failure, so every retry fails identically).
|
|
334
|
-
const decoded = decodeFrontMatterResult(
|
|
335
|
-
{
|
|
336
|
-
status: fm.status,
|
|
337
|
-
...(requireVerdict ? { verdict: fm.verdict } : {}),
|
|
338
|
-
nits: fm.nits,
|
|
339
|
-
...(kind === "code_review" && fm.rework
|
|
340
|
-
? { rework: fm.rework }
|
|
341
|
-
: {}),
|
|
342
|
-
},
|
|
343
|
-
pendingArtifact,
|
|
344
|
-
)
|
|
345
|
-
if (Result.isFailure(decoded)) {
|
|
346
|
-
return yield* decoded.failure
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const rework =
|
|
350
|
-
kind === "code_review" ? decoded.success.rework : undefined
|
|
351
|
-
const next = stepAfterArtifact(kind, fm.verdict, rework)
|
|
352
|
-
if (typeof next === "object") {
|
|
353
|
-
return yield* new ArtifactInvalid({
|
|
354
|
-
artifact: pendingArtifact,
|
|
355
|
-
message: next.error,
|
|
356
|
-
})
|
|
357
|
-
}
|
|
319
|
+
const { next, rework } = completion
|
|
358
320
|
|
|
359
321
|
if (kind === "phase_package") {
|
|
360
322
|
state.current_phase_package = pendingArtifact
|
|
361
323
|
}
|
|
362
324
|
if (kind === "code_review") {
|
|
363
325
|
state.current_code_review = pendingArtifact
|
|
364
|
-
state.phase_package_rework =
|
|
365
|
-
fm.verdict === "CHANGES_REQUIRED" && rework === "phase_package"
|
|
366
326
|
}
|
|
367
327
|
const verdict = asVerdict(fm.verdict)
|
|
328
|
+
if (kind === "plan_review") {
|
|
329
|
+
state.required_rework = verdict === "CHANGES_REQUIRED" ? "plan" : null
|
|
330
|
+
}
|
|
331
|
+
if (kind === "code_review") {
|
|
332
|
+
state.required_rework =
|
|
333
|
+
verdict === "CHANGES_REQUIRED" ? (rework ?? "code") : null
|
|
334
|
+
}
|
|
368
335
|
state.step = next
|
|
369
336
|
state.pending_artifact = null
|
|
370
337
|
state.pending_role = null
|
|
338
|
+
state.pending_delivery = null
|
|
371
339
|
state.pending_pane_id = null
|
|
372
340
|
state.pending_pane_label = null
|
|
373
341
|
state.pending_started_at = null
|
|
@@ -422,7 +390,10 @@ export const waitWorkflow = (
|
|
|
422
390
|
* without the second chance. `pending_final_grace` already makes the
|
|
423
391
|
* deadline rung one-shot per run, so forcing it cannot spam a pane.
|
|
424
392
|
*/
|
|
425
|
-
const tryNudge = (
|
|
393
|
+
const tryNudge = (
|
|
394
|
+
why: string,
|
|
395
|
+
force = false,
|
|
396
|
+
): Effect.Effect<void, AppError> =>
|
|
426
397
|
Effect.gen(function* () {
|
|
427
398
|
if (
|
|
428
399
|
(nudged && !force) ||
|
|
@@ -469,12 +440,16 @@ export const waitWorkflow = (
|
|
|
469
440
|
const loop: Effect.Effect<ToolResult, AppError> = Effect.gen(function* () {
|
|
470
441
|
while (true) {
|
|
471
442
|
const now = yield* Clock.currentTimeMillis
|
|
472
|
-
const deadline =
|
|
443
|
+
const deadline =
|
|
444
|
+
state.pending_deadline_ms ?? deadlineAfter(startedMs, timeout)
|
|
473
445
|
|
|
474
446
|
const fm = yield* readArtifact()
|
|
475
|
-
|
|
447
|
+
const completion = validateArtifactCompletion(kind, fm, pendingArtifact)
|
|
448
|
+
if (Result.isFailure(completion)) return yield* completion.failure
|
|
449
|
+
if (completion.success !== null) {
|
|
476
450
|
return yield* advanceOnComplete(
|
|
477
451
|
fm!,
|
|
452
|
+
completion.success,
|
|
478
453
|
nudged ? "artifact ready after nudge" : "artifact ready",
|
|
479
454
|
)
|
|
480
455
|
}
|
|
@@ -490,7 +465,7 @@ export const waitWorkflow = (
|
|
|
490
465
|
message: `role pane gone and artifact incomplete: ${pendingArtifact}`,
|
|
491
466
|
details: {
|
|
492
467
|
last_agent_status: lastStatus,
|
|
493
|
-
hint: "
|
|
468
|
+
hint: "after investigate, dispatch the same kind with redeliver=true",
|
|
494
469
|
},
|
|
495
470
|
})
|
|
496
471
|
}
|
|
@@ -502,9 +477,18 @@ export const waitWorkflow = (
|
|
|
502
477
|
// The artifact may have completed between the read above and this
|
|
503
478
|
// terminal status, so fail the contract only after one fresh read.
|
|
504
479
|
const artifactAfterDone = yield* readArtifact()
|
|
505
|
-
|
|
480
|
+
const completionAfterDone = validateArtifactCompletion(
|
|
481
|
+
kind,
|
|
482
|
+
artifactAfterDone,
|
|
483
|
+
pendingArtifact,
|
|
484
|
+
)
|
|
485
|
+
if (Result.isFailure(completionAfterDone)) {
|
|
486
|
+
return yield* completionAfterDone.failure
|
|
487
|
+
}
|
|
488
|
+
if (completionAfterDone.success !== null) {
|
|
506
489
|
return yield* advanceOnComplete(
|
|
507
490
|
artifactAfterDone!,
|
|
491
|
+
completionAfterDone.success,
|
|
508
492
|
"artifact ready as role exited",
|
|
509
493
|
)
|
|
510
494
|
}
|
|
@@ -568,7 +552,7 @@ export const waitWorkflow = (
|
|
|
568
552
|
details: {
|
|
569
553
|
last_agent_status: lastStatus,
|
|
570
554
|
foreground: names,
|
|
571
|
-
hint: "check pane transcript;
|
|
555
|
+
hint: "check pane transcript; then dispatch the same kind with redeliver=true",
|
|
572
556
|
},
|
|
573
557
|
})
|
|
574
558
|
}
|
|
@@ -605,7 +589,7 @@ export const waitWorkflow = (
|
|
|
605
589
|
extendedOnce = true
|
|
606
590
|
const extra = Math.max(Math.floor(timeout * 0.5), 120_000)
|
|
607
591
|
state.pending_extended = true
|
|
608
|
-
state.pending_deadline_ms = now
|
|
592
|
+
state.pending_deadline_ms = deadlineAfter(now, extra)
|
|
609
593
|
yield* store.save(state, root)
|
|
610
594
|
hooks.onUpdate?.({
|
|
611
595
|
content: [
|
|
@@ -639,7 +623,7 @@ export const waitWorkflow = (
|
|
|
639
623
|
// grace is not.
|
|
640
624
|
finalNudgeGrace = true
|
|
641
625
|
state.pending_final_grace = true
|
|
642
|
-
state.pending_deadline_ms = now
|
|
626
|
+
state.pending_deadline_ms = deadlineAfter(now, 180_000)
|
|
643
627
|
yield* store.save(state, root)
|
|
644
628
|
yield* tryNudge("timeout idle", true)
|
|
645
629
|
continue
|
|
@@ -681,7 +665,8 @@ export const waitWorkflow = (
|
|
|
681
665
|
// was never the problem. `timeout` still sizes the extension,
|
|
682
666
|
// because that has to come from config to stay stable across calls.
|
|
683
667
|
const grantedMs =
|
|
684
|
-
(state.pending_deadline_ms ?? dispatchedAt
|
|
668
|
+
(state.pending_deadline_ms ?? deadlineAfter(dispatchedAt, timeout)) -
|
|
669
|
+
dispatchedAt
|
|
685
670
|
return yield* new WaitTimeout({
|
|
686
671
|
artifact: pendingArtifact,
|
|
687
672
|
timeoutMs: grantedMs,
|
|
@@ -690,7 +675,7 @@ export const waitWorkflow = (
|
|
|
690
675
|
nudged,
|
|
691
676
|
extended_once: extendedOnce,
|
|
692
677
|
configured_timeout_ms: timeout,
|
|
693
|
-
hint: "inspect pane transcript;
|
|
678
|
+
hint: "inspect pane transcript; use redeliver=true for the same kind only after proving delivery is dead, or nudge via herdr pane run",
|
|
694
679
|
},
|
|
695
680
|
})
|
|
696
681
|
})
|
|
@@ -707,7 +692,7 @@ export const waitWorkflow = (
|
|
|
707
692
|
artifact,
|
|
708
693
|
details: {
|
|
709
694
|
last_agent_status: lastStatus,
|
|
710
|
-
hint: "
|
|
695
|
+
hint: "after investigate, use redeliver=true for the same kind or wait again",
|
|
711
696
|
},
|
|
712
697
|
}),
|
|
713
698
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naxodev/apnea",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Host-neutral multi-role workflow engine and standalone Bun CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nacho Vazquez",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"smoke:package": "bun scripts/package-smoke.ts"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"effect": "4.0.0-
|
|
65
|
+
"effect": "4.0.0-rc.111",
|
|
66
66
|
"typebox": "^1.3.6"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
@@ -44,7 +44,11 @@
|
|
|
44
44
|
"review_round_cap": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
45
45
|
"timeouts_ms": {
|
|
46
46
|
"type": "object",
|
|
47
|
-
"additionalProperties": {
|
|
47
|
+
"additionalProperties": {
|
|
48
|
+
"type": "integer",
|
|
49
|
+
"minimum": 1000,
|
|
50
|
+
"maximum": 9007199254740991
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -5,8 +5,21 @@
|
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"required": ["version", "slug", "step", "phase_index", "rounds", "vcs"],
|
|
8
|
+
"allOf": [
|
|
9
|
+
{
|
|
10
|
+
"if": {
|
|
11
|
+
"properties": { "version": { "const": 2 } },
|
|
12
|
+
"required": ["version"]
|
|
13
|
+
},
|
|
14
|
+
"then": { "required": ["pending_commit"] },
|
|
15
|
+
"description": "Runtime enforcement is authoritative: decodeRunState applies the same rule plus consistency checks the JSON Schema cannot express."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
8
18
|
"properties": {
|
|
9
|
-
"version": {
|
|
19
|
+
"version": {
|
|
20
|
+
"enum": [1, 2],
|
|
21
|
+
"description": "Version 1 is accepted INPUT for migration (decodes with pending_commit=null); version 2 is the OUTPUT every save writes and must carry pending_commit explicitly."
|
|
22
|
+
},
|
|
10
23
|
"slug": { "type": "string", "minLength": 1 },
|
|
11
24
|
"step": {
|
|
12
25
|
"type": "string",
|
|
@@ -21,19 +34,39 @@
|
|
|
21
34
|
"done"
|
|
22
35
|
]
|
|
23
36
|
},
|
|
24
|
-
"phase_index": {
|
|
25
|
-
|
|
37
|
+
"phase_index": {
|
|
38
|
+
"type": "integer",
|
|
39
|
+
"minimum": 1,
|
|
40
|
+
"maximum": 9007199254740991
|
|
41
|
+
},
|
|
42
|
+
"phase_count_hint": {
|
|
43
|
+
"type": ["integer", "null"],
|
|
44
|
+
"minimum": 0,
|
|
45
|
+
"maximum": 9007199254740991
|
|
46
|
+
},
|
|
26
47
|
"rounds": {
|
|
27
48
|
"type": "object",
|
|
28
49
|
"description": "Keys like phase-01/plan_review or phase-01/code_review",
|
|
29
|
-
"additionalProperties": {
|
|
50
|
+
"additionalProperties": {
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"minimum": 1,
|
|
53
|
+
"maximum": 9007199254740991
|
|
54
|
+
}
|
|
30
55
|
},
|
|
31
56
|
"vcs": { "type": "string", "enum": ["jj", "git"] },
|
|
32
57
|
"allow_dirty": { "type": "boolean" },
|
|
33
58
|
"goal": { "type": "string" },
|
|
34
59
|
"last_error": { "type": ["string", "null"] },
|
|
35
|
-
"pending_artifact": {
|
|
60
|
+
"pending_artifact": {
|
|
61
|
+
"type": ["string", "null"],
|
|
62
|
+
"pattern": "^\\.apnea\\/(?!.*\\u0000)(?!\\.(?:/|$)|\\.\\.(?:/|$))(?!.*\\/\\.(?:\\/|$))(?!.*\\/\\.\\.(?:\\/|$))(?!.*\\\\)[^/]+(?:/[^/]+)*$"
|
|
63
|
+
},
|
|
36
64
|
"pending_role": { "type": ["string", "null"] },
|
|
65
|
+
"pending_delivery": {
|
|
66
|
+
"type": ["string", "null"],
|
|
67
|
+
"enum": ["manual", "interactive", null],
|
|
68
|
+
"description": "Known delivery boundary for pending ownership; null is idle or legacy-ambiguous"
|
|
69
|
+
},
|
|
37
70
|
"pending_floating_exit": {
|
|
38
71
|
"type": "null",
|
|
39
72
|
"deprecated": true,
|
|
@@ -41,9 +74,21 @@
|
|
|
41
74
|
},
|
|
42
75
|
"pending_pane_id": { "type": ["string", "null"] },
|
|
43
76
|
"pending_pane_label": { "type": ["string", "null"] },
|
|
44
|
-
"pending_started_at": {
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
"pending_started_at": {
|
|
78
|
+
"type": ["integer", "null"],
|
|
79
|
+
"minimum": 0,
|
|
80
|
+
"maximum": 9007199254740991
|
|
81
|
+
},
|
|
82
|
+
"pending_deadline_ms": {
|
|
83
|
+
"type": ["integer", "null"],
|
|
84
|
+
"minimum": 0,
|
|
85
|
+
"maximum": 9007199254740991
|
|
86
|
+
},
|
|
87
|
+
"pending_nudged_at": {
|
|
88
|
+
"type": ["integer", "null"],
|
|
89
|
+
"minimum": 0,
|
|
90
|
+
"maximum": 9007199254740991
|
|
91
|
+
},
|
|
47
92
|
"pending_final_grace": { "type": "boolean" },
|
|
48
93
|
"pending_extended": { "type": "boolean" },
|
|
49
94
|
"role_panes": {
|
|
@@ -60,8 +105,117 @@
|
|
|
60
105
|
},
|
|
61
106
|
"package_root": { "type": "string" },
|
|
62
107
|
"reviewer_tree_fingerprint": { "type": ["string", "null"] },
|
|
63
|
-
"current_phase_package": {
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
"current_phase_package": {
|
|
109
|
+
"type": ["string", "null"],
|
|
110
|
+
"pattern": "^\\.apnea\\/(?!.*\\u0000)(?!\\.(?:/|$)|\\.\\.(?:/|$))(?!.*\\/\\.(?:\\/|$))(?!.*\\/\\.\\.(?:\\/|$))(?!.*\\\\)[^/]+(?:/[^/]+)*$"
|
|
111
|
+
},
|
|
112
|
+
"current_code_review": {
|
|
113
|
+
"type": ["string", "null"],
|
|
114
|
+
"pattern": "^\\.apnea\\/(?!.*\\u0000)(?!\\.(?:/|$)|\\.\\.(?:/|$))(?!.*\\/\\.(?:\\/|$))(?!.*\\/\\.\\.(?:\\/|$))(?!.*\\\\)[^/]+(?:/[^/]+)*$"
|
|
115
|
+
},
|
|
116
|
+
"required_rework": {
|
|
117
|
+
"type": ["string", "null"],
|
|
118
|
+
"enum": ["plan", "code", "phase_package", null],
|
|
119
|
+
"description": "Exact dispatch target that owns the next review round"
|
|
120
|
+
},
|
|
121
|
+
"pending_commit": {
|
|
122
|
+
"description": "Durable record of an in-flight commit transaction (crash-recoverable commit completion). Requires step=committing and phase_index equal to the run's phase_index; non-committing states must carry null. Version-1 files must not contain this key.",
|
|
123
|
+
"oneOf": [
|
|
124
|
+
{
|
|
125
|
+
"type": "null"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"type": "object",
|
|
129
|
+
"additionalProperties": false,
|
|
130
|
+
"required": [
|
|
131
|
+
"backend",
|
|
132
|
+
"id",
|
|
133
|
+
"phase_index",
|
|
134
|
+
"message",
|
|
135
|
+
"no_remaining_phases",
|
|
136
|
+
"verify_log",
|
|
137
|
+
"branch",
|
|
138
|
+
"parent_commit",
|
|
139
|
+
"tree_id"
|
|
140
|
+
],
|
|
141
|
+
"properties": {
|
|
142
|
+
"backend": { "const": "git" },
|
|
143
|
+
"id": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"minLength": 1,
|
|
146
|
+
"description": "Transaction uuid; appears in the commit message as Apnea-Transaction: <id>"
|
|
147
|
+
},
|
|
148
|
+
"phase_index": { "type": "integer", "minimum": 1 },
|
|
149
|
+
"message": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"minLength": 1,
|
|
152
|
+
"description": "Full commit message including the Apnea-Transaction trailer line"
|
|
153
|
+
},
|
|
154
|
+
"no_remaining_phases": { "type": "boolean" },
|
|
155
|
+
"verify_log": { "type": "string", "minLength": 1 },
|
|
156
|
+
"branch": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"minLength": 1,
|
|
159
|
+
"description": "Full ref the commit must land on, e.g. refs/heads/apnea/<slug>"
|
|
160
|
+
},
|
|
161
|
+
"parent_commit": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"minLength": 1,
|
|
164
|
+
"description": "Expected HEAD at completion; also the created commit's parent"
|
|
165
|
+
},
|
|
166
|
+
"tree_id": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"minLength": 1,
|
|
169
|
+
"description": "Prepared tree id staged with .apnea excluded"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"type": "object",
|
|
175
|
+
"additionalProperties": false,
|
|
176
|
+
"required": [
|
|
177
|
+
"backend",
|
|
178
|
+
"id",
|
|
179
|
+
"phase_index",
|
|
180
|
+
"message",
|
|
181
|
+
"no_remaining_phases",
|
|
182
|
+
"verify_log",
|
|
183
|
+
"change_id",
|
|
184
|
+
"content_fingerprint"
|
|
185
|
+
],
|
|
186
|
+
"properties": {
|
|
187
|
+
"backend": { "const": "jj" },
|
|
188
|
+
"id": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"minLength": 1,
|
|
191
|
+
"description": "Transaction uuid; appears in the change description as Apnea-Transaction: <id>"
|
|
192
|
+
},
|
|
193
|
+
"phase_index": { "type": "integer", "minimum": 1 },
|
|
194
|
+
"message": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"minLength": 1,
|
|
197
|
+
"description": "Full description including the Apnea-Transaction trailer line"
|
|
198
|
+
},
|
|
199
|
+
"no_remaining_phases": { "type": "boolean" },
|
|
200
|
+
"verify_log": { "type": "string", "minLength": 1 },
|
|
201
|
+
"change_id": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"minLength": 1,
|
|
204
|
+
"description": "Change id of the described working-copy change"
|
|
205
|
+
},
|
|
206
|
+
"content_fingerprint": {
|
|
207
|
+
"type": "string",
|
|
208
|
+
"minLength": 1,
|
|
209
|
+
"description": "Fingerprint of the change's non-.apnea diff at preparation time"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
"phase_package_rework": {
|
|
216
|
+
"type": "boolean",
|
|
217
|
+
"deprecated": true,
|
|
218
|
+
"description": "Deprecated migration-only field. True migrates to required_rework=phase_package."
|
|
219
|
+
}
|
|
66
220
|
}
|
|
67
221
|
}
|