@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
|
@@ -1,23 +1,15 @@
|
|
|
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,
|
|
8
8
|
parseFrontMatter,
|
|
9
9
|
} from "../domain/frontmatter.ts"
|
|
10
|
-
import {
|
|
11
|
-
looksLikeShellOnly,
|
|
12
|
-
parseFloatingExit,
|
|
13
|
-
shellJoin,
|
|
14
|
-
} from "../domain/herdr.ts"
|
|
10
|
+
import { looksLikeShellOnly, shellJoin } from "../domain/herdr.ts"
|
|
15
11
|
import { abs, rel } from "../domain/paths.ts"
|
|
16
|
-
import {
|
|
17
|
-
nextAfter,
|
|
18
|
-
stepAfterArtifact,
|
|
19
|
-
toolAllowed,
|
|
20
|
-
} from "../domain/state-machine.ts"
|
|
12
|
+
import { nextAfter, toolAllowed } from "../domain/state-machine.ts"
|
|
21
13
|
import {
|
|
22
14
|
ArtifactInvalid,
|
|
23
15
|
GateRefused,
|
|
@@ -28,12 +20,16 @@ import {
|
|
|
28
20
|
} from "../errors.ts"
|
|
29
21
|
import type { FrontMatter } from "../domain/types.ts"
|
|
30
22
|
import { ok, type ToolResult } from "../result.ts"
|
|
31
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
validateArtifactCompletion,
|
|
25
|
+
type AcceptedArtifactCompletion,
|
|
26
|
+
} from "../schema/frontmatter.ts"
|
|
32
27
|
import { Config } from "../services/config.ts"
|
|
33
28
|
import { FileSystem } from "../services/file-system.ts"
|
|
34
29
|
import { Herdr, paneReadRecentArgs } from "../services/herdr.ts"
|
|
35
30
|
import { RunStore } from "../services/run-store.ts"
|
|
36
31
|
import { Vcs } from "../services/vcs.ts"
|
|
32
|
+
import type { OperationHooks } from "../operation-hooks.ts"
|
|
37
33
|
|
|
38
34
|
export type WaitParams = {
|
|
39
35
|
poll_ms?: number
|
|
@@ -155,12 +151,7 @@ export const MAX_AUTO_POLL_MS =
|
|
|
155
151
|
export const fitsHostShell = (pollMs: number): boolean =>
|
|
156
152
|
defaultBudgetFor(pollMs) < HOST_SHELL_TIMEOUT_MS
|
|
157
153
|
|
|
158
|
-
export type WaitHooks =
|
|
159
|
-
signal?: AbortSignal
|
|
160
|
-
onUpdate?: (partial: {
|
|
161
|
-
content: Array<{ type: "text"; text: string }>
|
|
162
|
-
}) => void
|
|
163
|
-
}
|
|
154
|
+
export type WaitHooks = OperationHooks
|
|
164
155
|
|
|
165
156
|
/**
|
|
166
157
|
* Async wait — polls via `Clock` + `Effect.sleep` so Pi stays responsive and
|
|
@@ -206,10 +197,10 @@ export const waitWorkflow = (
|
|
|
206
197
|
const cfg = yield* config.load(root)
|
|
207
198
|
|
|
208
199
|
const poll = params.poll_ms ?? 2000
|
|
209
|
-
if (!Number.
|
|
200
|
+
if (!Number.isSafeInteger(poll) || poll < MIN_POLL_MS) {
|
|
210
201
|
return yield* new GateRefused({
|
|
211
202
|
gate: "poll_interval",
|
|
212
|
-
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.`,
|
|
213
204
|
})
|
|
214
205
|
}
|
|
215
206
|
// Refuse to PICK a budget that a default host shell would kill; still
|
|
@@ -236,10 +227,10 @@ export const waitWorkflow = (
|
|
|
236
227
|
// NaN, and the call blocks until the role's deadline instead of
|
|
237
228
|
// returning exit 3. The CLI is protected by `parseNumFlag`; a Pi tool
|
|
238
229
|
// call reaches `run` through a raw cast with no runtime coercion.
|
|
239
|
-
if (!Number.
|
|
230
|
+
if (!Number.isSafeInteger(budget) || budget <= 0) {
|
|
240
231
|
return yield* new GateRefused({
|
|
241
232
|
gate: "budget_floor",
|
|
242
|
-
message: `budget_ms must be a
|
|
233
|
+
message: `budget_ms must be a positive safe integer; got ${budget}`,
|
|
243
234
|
})
|
|
244
235
|
}
|
|
245
236
|
const floor = minBudgetFor(poll)
|
|
@@ -277,45 +268,33 @@ export const waitWorkflow = (
|
|
|
277
268
|
const dispatchedAt = state.pending_started_at ?? startedMs
|
|
278
269
|
if (state.pending_deadline_ms == null) {
|
|
279
270
|
state.pending_started_at = dispatchedAt
|
|
280
|
-
state.pending_deadline_ms = dispatchedAt
|
|
271
|
+
state.pending_deadline_ms = deadlineAfter(dispatchedAt, timeout)
|
|
281
272
|
yield* store.save(state, root)
|
|
282
273
|
}
|
|
283
274
|
const requireVerdict = kind === "plan_review" || kind === "code_review"
|
|
284
275
|
|
|
285
|
-
const readArtifact = (): Effect.Effect<FrontMatter | null> =>
|
|
276
|
+
const readArtifact = (): Effect.Effect<FrontMatter | null, AppError> =>
|
|
286
277
|
Effect.gen(function* () {
|
|
287
|
-
const present = yield* fs.
|
|
278
|
+
const present = yield* fs.projectPathExists(root, artifactAbs)
|
|
288
279
|
if (!present) return null
|
|
289
|
-
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
|
+
)
|
|
290
289
|
return parseFrontMatter(text)
|
|
291
290
|
})
|
|
292
291
|
|
|
293
|
-
const readFloatingExitCode = (
|
|
294
|
-
exitAbs: string,
|
|
295
|
-
): Effect.Effect<number | null> =>
|
|
296
|
-
Effect.gen(function* () {
|
|
297
|
-
const present = yield* fs.exists(exitAbs)
|
|
298
|
-
if (!present) return null
|
|
299
|
-
const text = yield* fs.readFile(exitAbs)
|
|
300
|
-
return parseFloatingExit(text)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
292
|
const advanceOnComplete = (
|
|
304
293
|
fm: FrontMatter,
|
|
294
|
+
completion: AcceptedArtifactCompletion,
|
|
305
295
|
msg = "artifact ready",
|
|
306
296
|
): Effect.Effect<ToolResult, AppError> =>
|
|
307
297
|
Effect.gen(function* () {
|
|
308
|
-
if (
|
|
309
|
-
fm.rework !== undefined &&
|
|
310
|
-
(kind !== "code_review" || fm.verdict !== "CHANGES_REQUIRED")
|
|
311
|
-
) {
|
|
312
|
-
return yield* new ArtifactInvalid({
|
|
313
|
-
artifact: pendingArtifact,
|
|
314
|
-
message:
|
|
315
|
-
"rework is valid only on a code_review artifact with verdict CHANGES_REQUIRED",
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
298
|
if (
|
|
320
299
|
state.pending_role === "reviewer" &&
|
|
321
300
|
state.reviewer_tree_fingerprint != null
|
|
@@ -337,54 +316,28 @@ export const waitWorkflow = (
|
|
|
337
316
|
}
|
|
338
317
|
}
|
|
339
318
|
|
|
340
|
-
|
|
341
|
-
// completeness test. A bad/absent verdict must keep waiting, not fail
|
|
342
|
-
// here (that already happened before advanceOnComplete was called).
|
|
343
|
-
//
|
|
344
|
-
// Only review kinds carry a meaningful verdict. A stray `verdict:` on
|
|
345
|
-
// a plan/code/package artifact is noise the old parser ignored;
|
|
346
|
-
// validating it here would wedge the run permanently (state is not
|
|
347
|
-
// saved on failure, so every retry fails identically).
|
|
348
|
-
const decoded = decodeFrontMatterResult(
|
|
349
|
-
{
|
|
350
|
-
status: fm.status,
|
|
351
|
-
...(requireVerdict ? { verdict: fm.verdict } : {}),
|
|
352
|
-
nits: fm.nits,
|
|
353
|
-
...(kind === "code_review" && fm.rework
|
|
354
|
-
? { rework: fm.rework }
|
|
355
|
-
: {}),
|
|
356
|
-
},
|
|
357
|
-
pendingArtifact,
|
|
358
|
-
)
|
|
359
|
-
if (Result.isFailure(decoded)) {
|
|
360
|
-
return yield* decoded.failure
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const rework =
|
|
364
|
-
kind === "code_review" ? decoded.success.rework : undefined
|
|
365
|
-
const next = stepAfterArtifact(kind, fm.verdict, rework)
|
|
366
|
-
if (typeof next === "object") {
|
|
367
|
-
return yield* new ArtifactInvalid({
|
|
368
|
-
artifact: pendingArtifact,
|
|
369
|
-
message: next.error,
|
|
370
|
-
})
|
|
371
|
-
}
|
|
319
|
+
const { next, rework } = completion
|
|
372
320
|
|
|
373
321
|
if (kind === "phase_package") {
|
|
374
322
|
state.current_phase_package = pendingArtifact
|
|
375
323
|
}
|
|
376
324
|
if (kind === "code_review") {
|
|
377
325
|
state.current_code_review = pendingArtifact
|
|
378
|
-
state.phase_package_rework =
|
|
379
|
-
fm.verdict === "CHANGES_REQUIRED" && rework === "phase_package"
|
|
380
326
|
}
|
|
381
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
|
+
}
|
|
382
335
|
state.step = next
|
|
383
336
|
state.pending_artifact = null
|
|
384
337
|
state.pending_role = null
|
|
338
|
+
state.pending_delivery = null
|
|
385
339
|
state.pending_pane_id = null
|
|
386
340
|
state.pending_pane_label = null
|
|
387
|
-
state.pending_floating_exit = null
|
|
388
341
|
state.pending_started_at = null
|
|
389
342
|
state.pending_deadline_ms = null
|
|
390
343
|
resetRecoveryLadder(state)
|
|
@@ -406,8 +359,6 @@ export const waitWorkflow = (
|
|
|
406
359
|
)
|
|
407
360
|
})
|
|
408
361
|
|
|
409
|
-
const floatingFlushMs = 2_000
|
|
410
|
-
|
|
411
362
|
let lastStatus = "waiting"
|
|
412
363
|
/**
|
|
413
364
|
* Both counters below are per-call on purpose. They measure a duration
|
|
@@ -418,7 +369,6 @@ export const waitWorkflow = (
|
|
|
418
369
|
*/
|
|
419
370
|
let shellOnlyPolls = 0
|
|
420
371
|
let idleSince: number | null = null
|
|
421
|
-
let floatingExitSeenAt: number | null = null
|
|
422
372
|
let nudged = state.pending_nudged_at != null
|
|
423
373
|
let extendedOnce = state.pending_extended
|
|
424
374
|
let finalNudgeGrace = state.pending_final_grace
|
|
@@ -440,7 +390,10 @@ export const waitWorkflow = (
|
|
|
440
390
|
* without the second chance. `pending_final_grace` already makes the
|
|
441
391
|
* deadline rung one-shot per run, so forcing it cannot spam a pane.
|
|
442
392
|
*/
|
|
443
|
-
const tryNudge = (
|
|
393
|
+
const tryNudge = (
|
|
394
|
+
why: string,
|
|
395
|
+
force = false,
|
|
396
|
+
): Effect.Effect<void, AppError> =>
|
|
444
397
|
Effect.gen(function* () {
|
|
445
398
|
if (
|
|
446
399
|
(nudged && !force) ||
|
|
@@ -487,54 +440,20 @@ export const waitWorkflow = (
|
|
|
487
440
|
const loop: Effect.Effect<ToolResult, AppError> = Effect.gen(function* () {
|
|
488
441
|
while (true) {
|
|
489
442
|
const now = yield* Clock.currentTimeMillis
|
|
490
|
-
const deadline =
|
|
443
|
+
const deadline =
|
|
444
|
+
state.pending_deadline_ms ?? deadlineAfter(startedMs, timeout)
|
|
491
445
|
|
|
492
446
|
const fm = yield* readArtifact()
|
|
493
|
-
|
|
447
|
+
const completion = validateArtifactCompletion(kind, fm, pendingArtifact)
|
|
448
|
+
if (Result.isFailure(completion)) return yield* completion.failure
|
|
449
|
+
if (completion.success !== null) {
|
|
494
450
|
return yield* advanceOnComplete(
|
|
495
451
|
fm!,
|
|
452
|
+
completion.success,
|
|
496
453
|
nudged ? "artifact ready after nudge" : "artifact ready",
|
|
497
454
|
)
|
|
498
455
|
}
|
|
499
456
|
|
|
500
|
-
// Floating oneshot: no pane id — exit file is liveness. Fail closed
|
|
501
|
-
// when the popup dies without a complete artifact instead of
|
|
502
|
-
// hanging until timeout.
|
|
503
|
-
if (state.pending_floating_exit) {
|
|
504
|
-
const exitAbs = abs(state.pending_floating_exit, root)
|
|
505
|
-
const code = yield* readFloatingExitCode(exitAbs)
|
|
506
|
-
if (code != null) {
|
|
507
|
-
lastStatus = `floating_exit_${code}`
|
|
508
|
-
floatingExitSeenAt ??= now
|
|
509
|
-
// Short flush window: oneshot may finish writing as the process exits.
|
|
510
|
-
if (now - floatingExitSeenAt >= floatingFlushMs) {
|
|
511
|
-
const again = yield* readArtifact()
|
|
512
|
-
if (isCompleteArtifact(again, { requireVerdict })) {
|
|
513
|
-
return yield* advanceOnComplete(
|
|
514
|
-
again!,
|
|
515
|
-
"artifact ready (floating oneshot exited)",
|
|
516
|
-
)
|
|
517
|
-
}
|
|
518
|
-
state.pending_floating_exit = null
|
|
519
|
-
state.last_error = `floating oneshot exited ${code} without ${pendingArtifact}`
|
|
520
|
-
yield* store.save(state, root)
|
|
521
|
-
return yield* new HerdrError({
|
|
522
|
-
message: `floating ${state.pending_role} exited (code ${code}) without writing ${pendingArtifact}`,
|
|
523
|
-
details: {
|
|
524
|
-
exit_code: code,
|
|
525
|
-
last_agent_status: lastStatus,
|
|
526
|
-
hint:
|
|
527
|
-
code === 129
|
|
528
|
-
? "popup received Hangup (dismiss/focus steal) — re-dispatch same round; keep focus on the popup"
|
|
529
|
-
: "inspect oneshot output; re-dispatch same round or set pane_style=regular",
|
|
530
|
-
},
|
|
531
|
-
})
|
|
532
|
-
}
|
|
533
|
-
} else {
|
|
534
|
-
lastStatus = "floating_running"
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
457
|
if ((yield* herdr.enabled) && state.pending_pane_id) {
|
|
539
458
|
const info = yield* herdr.paneGet(state.pending_pane_id)
|
|
540
459
|
if (!info.ok) {
|
|
@@ -546,7 +465,7 @@ export const waitWorkflow = (
|
|
|
546
465
|
message: `role pane gone and artifact incomplete: ${pendingArtifact}`,
|
|
547
466
|
details: {
|
|
548
467
|
last_agent_status: lastStatus,
|
|
549
|
-
hint: "
|
|
468
|
+
hint: "after investigate, dispatch the same kind with redeliver=true",
|
|
550
469
|
},
|
|
551
470
|
})
|
|
552
471
|
}
|
|
@@ -558,9 +477,18 @@ export const waitWorkflow = (
|
|
|
558
477
|
// The artifact may have completed between the read above and this
|
|
559
478
|
// terminal status, so fail the contract only after one fresh read.
|
|
560
479
|
const artifactAfterDone = yield* readArtifact()
|
|
561
|
-
|
|
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) {
|
|
562
489
|
return yield* advanceOnComplete(
|
|
563
490
|
artifactAfterDone!,
|
|
491
|
+
completionAfterDone.success,
|
|
564
492
|
"artifact ready as role exited",
|
|
565
493
|
)
|
|
566
494
|
}
|
|
@@ -624,7 +552,7 @@ export const waitWorkflow = (
|
|
|
624
552
|
details: {
|
|
625
553
|
last_agent_status: lastStatus,
|
|
626
554
|
foreground: names,
|
|
627
|
-
hint: "check pane transcript;
|
|
555
|
+
hint: "check pane transcript; then dispatch the same kind with redeliver=true",
|
|
628
556
|
},
|
|
629
557
|
})
|
|
630
558
|
}
|
|
@@ -661,7 +589,7 @@ export const waitWorkflow = (
|
|
|
661
589
|
extendedOnce = true
|
|
662
590
|
const extra = Math.max(Math.floor(timeout * 0.5), 120_000)
|
|
663
591
|
state.pending_extended = true
|
|
664
|
-
state.pending_deadline_ms = now
|
|
592
|
+
state.pending_deadline_ms = deadlineAfter(now, extra)
|
|
665
593
|
yield* store.save(state, root)
|
|
666
594
|
hooks.onUpdate?.({
|
|
667
595
|
content: [
|
|
@@ -695,7 +623,7 @@ export const waitWorkflow = (
|
|
|
695
623
|
// grace is not.
|
|
696
624
|
finalNudgeGrace = true
|
|
697
625
|
state.pending_final_grace = true
|
|
698
|
-
state.pending_deadline_ms = now
|
|
626
|
+
state.pending_deadline_ms = deadlineAfter(now, 180_000)
|
|
699
627
|
yield* store.save(state, root)
|
|
700
628
|
yield* tryNudge("timeout idle", true)
|
|
701
629
|
continue
|
|
@@ -737,7 +665,8 @@ export const waitWorkflow = (
|
|
|
737
665
|
// was never the problem. `timeout` still sizes the extension,
|
|
738
666
|
// because that has to come from config to stay stable across calls.
|
|
739
667
|
const grantedMs =
|
|
740
|
-
(state.pending_deadline_ms ?? dispatchedAt
|
|
668
|
+
(state.pending_deadline_ms ?? deadlineAfter(dispatchedAt, timeout)) -
|
|
669
|
+
dispatchedAt
|
|
741
670
|
return yield* new WaitTimeout({
|
|
742
671
|
artifact: pendingArtifact,
|
|
743
672
|
timeoutMs: grantedMs,
|
|
@@ -746,7 +675,7 @@ export const waitWorkflow = (
|
|
|
746
675
|
nudged,
|
|
747
676
|
extended_once: extendedOnce,
|
|
748
677
|
configured_timeout_ms: timeout,
|
|
749
|
-
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",
|
|
750
679
|
},
|
|
751
680
|
})
|
|
752
681
|
})
|
|
@@ -763,7 +692,7 @@ export const waitWorkflow = (
|
|
|
763
692
|
artifact,
|
|
764
693
|
details: {
|
|
765
694
|
last_agent_status: lastStatus,
|
|
766
|
-
hint: "
|
|
695
|
+
hint: "after investigate, use redeliver=true for the same kind or wait again",
|
|
767
696
|
},
|
|
768
697
|
}),
|
|
769
698
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naxodev/apnea",
|
|
3
|
-
"version": "0.1
|
|
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",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"docs/adr",
|
|
40
40
|
"docs/protocol",
|
|
41
41
|
"extension",
|
|
42
|
-
"herdr-plugin",
|
|
43
42
|
"schemas",
|
|
44
43
|
"!extension/**/*.test.ts",
|
|
45
44
|
"!extension/test"
|
|
@@ -63,7 +62,7 @@
|
|
|
63
62
|
"smoke:package": "bun scripts/package-smoke.ts"
|
|
64
63
|
},
|
|
65
64
|
"dependencies": {
|
|
66
|
-
"effect": "4.0.0-
|
|
65
|
+
"effect": "4.0.0-rc.111",
|
|
67
66
|
"typebox": "^1.3.6"
|
|
68
67
|
},
|
|
69
68
|
"devDependencies": {
|
|
@@ -35,16 +35,20 @@
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
"review_round_cap": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
39
|
-
"timeouts_ms": {
|
|
40
|
-
"type": "object",
|
|
41
|
-
"additionalProperties": { "type": "integer", "minimum": 1000 }
|
|
42
|
-
},
|
|
43
38
|
"pane_style": {
|
|
44
39
|
"type": "string",
|
|
45
40
|
"enum": ["regular", "floating"],
|
|
46
|
-
"
|
|
47
|
-
"description": "
|
|
41
|
+
"deprecated": true,
|
|
42
|
+
"description": "Deprecated migration-only setting. The runtime accepts and ignores it during migration."
|
|
43
|
+
},
|
|
44
|
+
"review_round_cap": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
45
|
+
"timeouts_ms": {
|
|
46
|
+
"type": "object",
|
|
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,25 +34,61 @@
|
|
|
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
|
+
},
|
|
70
|
+
"pending_floating_exit": {
|
|
71
|
+
"type": "null",
|
|
72
|
+
"deprecated": true,
|
|
73
|
+
"description": "Deprecated migration-only field. Missing or null values migrate; active non-null legacy floating runs must be abandoned."
|
|
74
|
+
},
|
|
37
75
|
"pending_pane_id": { "type": ["string", "null"] },
|
|
38
76
|
"pending_pane_label": { "type": ["string", "null"] },
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
},
|
|
43
92
|
"pending_final_grace": { "type": "boolean" },
|
|
44
93
|
"pending_extended": { "type": "boolean" },
|
|
45
94
|
"role_panes": {
|
|
@@ -56,8 +105,117 @@
|
|
|
56
105
|
},
|
|
57
106
|
"package_root": { "type": "string" },
|
|
58
107
|
"reviewer_tree_fingerprint": { "type": ["string", "null"] },
|
|
59
|
-
"current_phase_package": {
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
220
|
}
|
|
63
221
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
id = "apnea"
|
|
2
|
-
name = "Apnea"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
description = "Apnea floating-pane worker for oneshot role dispatches."
|
|
5
|
-
min_herdr_version = "0.7.4"
|
|
6
|
-
platforms = ["linux", "macos"]
|
|
7
|
-
|
|
8
|
-
[[panes]]
|
|
9
|
-
id = "worker"
|
|
10
|
-
title = "Apnea worker"
|
|
11
|
-
placement = "overlay"
|
|
12
|
-
# Absolute bash + plugin-relative script. Bare `bash` fails on stripped plugin
|
|
13
|
-
# PATH (exit 127). Do not pass a foreign --cwd to plugin pane open — relative
|
|
14
|
-
# script paths resolve against that cwd, not plugin_root.
|
|
15
|
-
command = ["/bin/bash", "scripts/run-task.sh"]
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
if [[ -z "${APNEA_TASK_SCRIPT:-}" ]]; then
|
|
4
|
-
echo "apnea: APNEA_TASK_SCRIPT not set (open this pane via apnea dispatch)" >&2
|
|
5
|
-
exit 64
|
|
6
|
-
fi
|
|
7
|
-
# Absolute bash: popup PATH may not resolve bare `bash` (exit 127).
|
|
8
|
-
exec /bin/bash "$APNEA_TASK_SCRIPT"
|