@naxodev/apnea 0.1.0 → 0.2.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.
- package/README.md +8 -10
- package/dist/cli.js +552 -530
- package/docs/adr/0005-harness-profiles.md +1 -1
- package/docs/adr/0010-package-split.md +1 -1
- package/docs/protocol/config.md +7 -22
- package/docs/protocol/manual-gate.md +1 -1
- package/docs/protocol/overview.md +1 -1
- package/extension/domain/herdr.ts +0 -86
- package/extension/domain/paths.ts +1 -2
- package/extension/domain/setup.ts +0 -20
- package/extension/domain/types.ts +0 -9
- package/extension/domain/verify-commands.ts +200 -108
- package/extension/errors.ts +1 -1
- package/extension/schema/config.ts +31 -17
- package/extension/schema/state.ts +16 -3
- package/extension/services/herdr.ts +5 -161
- package/extension/services/vcs.ts +393 -21
- package/extension/workflows/commit.ts +8 -5
- package/extension/workflows/dispatch.ts +60 -177
- package/extension/workflows/setup.ts +2 -109
- package/extension/workflows/start.ts +0 -1
- package/extension/workflows/wait.ts +1 -57
- package/package.json +1 -2
- package/schemas/config.schema.json +6 -6
- package/schemas/state.schema.json +5 -1
- package/herdr-plugin/herdr-plugin.toml +0 -15
- package/herdr-plugin/scripts/run-task.sh +0 -8
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path"
|
|
2
2
|
import { Cause, Clock, Effect, Exit, Result } from "effect"
|
|
3
|
-
import { effectivePaneStyle, supportsFloating } from "../domain/herdr.ts"
|
|
4
3
|
import {
|
|
5
|
-
abs,
|
|
6
4
|
packageRoot,
|
|
7
5
|
phaseDir,
|
|
8
6
|
planPath,
|
|
@@ -110,8 +108,8 @@ function herdrAfterRollback(
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
/**
|
|
113
|
-
* Write task file, open interactive harness TUI in a Herdr pane
|
|
114
|
-
*
|
|
111
|
+
* Write task file, open an interactive harness TUI in a reusable Herdr pane,
|
|
112
|
+
* wait until idle, then submit a short pointer prompt.
|
|
115
113
|
* Refusals are tagged failures only — never ok:false ToolResults.
|
|
116
114
|
*/
|
|
117
115
|
export const dispatchWorkflow = (
|
|
@@ -345,57 +343,13 @@ export const dispatchWorkflow = (
|
|
|
345
343
|
})
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (paneStyle.style === "floating") {
|
|
356
|
-
const version = yield* herdr.version
|
|
357
|
-
if (!supportsFloating(version)) {
|
|
358
|
-
return yield* new HerdrError({
|
|
359
|
-
message:
|
|
360
|
-
"floating panes need herdr >= 0.7.4 — run `herdr update`, or set pane_style=regular",
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
if (!(yield* herdr.hasApneaPlugin)) {
|
|
364
|
-
return yield* new HerdrError({
|
|
365
|
-
message: `apnea herdr plugin not linked. Run /apnea setup, or: herdr plugin link ${livePackageRoot}/herdr-plugin`,
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
if (state.pending_floating_exit) {
|
|
369
|
-
const prevExitAbs = abs(state.pending_floating_exit, root)
|
|
370
|
-
if (!(yield* fs.exists(prevExitAbs))) {
|
|
371
|
-
return yield* new GateRefused({
|
|
372
|
-
gate: "floating_in_flight",
|
|
373
|
-
message:
|
|
374
|
-
"floating oneshot already in flight (popup still open). Call workflow_wait, or dismiss the popup and re-dispatch after it exits",
|
|
375
|
-
details: {
|
|
376
|
-
pending_artifact: state.pending_artifact,
|
|
377
|
-
pending_floating_exit: state.pending_floating_exit,
|
|
378
|
-
},
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const cmdResult = yield* Effect.result(
|
|
383
|
-
config.resolveRoleCmd(cfg, role, "oneshot"),
|
|
384
|
-
)
|
|
385
|
-
if (Result.isFailure(cmdResult)) {
|
|
386
|
-
return yield* new HerdrError({
|
|
387
|
-
message: `floating dispatch requires cmd_oneshot on the role profile: ${cmdResult.failure.message}`,
|
|
388
|
-
})
|
|
389
|
-
}
|
|
390
|
-
roleCmd = cmdResult.success
|
|
391
|
-
} else {
|
|
392
|
-
roleCmd = yield* config.resolveRoleCmd(cfg, role, "interactive")
|
|
393
|
-
profileFingerprint = JSON.stringify([
|
|
394
|
-
cfg.roles[role]?.profile ?? null,
|
|
395
|
-
roleCmd,
|
|
396
|
-
])
|
|
397
|
-
}
|
|
398
|
-
}
|
|
346
|
+
// Resolve the interactive command before mutating artifacts. Apnea never
|
|
347
|
+
// falls back to a profile's oneshot command, even outside Herdr.
|
|
348
|
+
const roleCmd = yield* config.resolveRoleCmd(cfg, role, "interactive")
|
|
349
|
+
const profileFingerprint = JSON.stringify([
|
|
350
|
+
cfg.roles[role]?.profile ?? null,
|
|
351
|
+
roleCmd,
|
|
352
|
+
])
|
|
399
353
|
|
|
400
354
|
if (role === "reviewer") {
|
|
401
355
|
state.reviewer_tree_fingerprint = yield* vcsSvc.treeFingerprint(
|
|
@@ -451,8 +405,6 @@ export const dispatchWorkflow = (
|
|
|
451
405
|
|
|
452
406
|
let launch: Record<string, unknown> = {
|
|
453
407
|
mode: ROLE_MODE[role],
|
|
454
|
-
pane_style: cfg.pane_style,
|
|
455
|
-
pane_style_effective: paneStyle.effective,
|
|
456
408
|
}
|
|
457
409
|
|
|
458
410
|
const rollbackLaunch = (restoreState = true) =>
|
|
@@ -466,14 +418,6 @@ export const dispatchWorkflow = (
|
|
|
466
418
|
}
|
|
467
419
|
})
|
|
468
420
|
yield* attempt("remove task", fs.remove(taskFile))
|
|
469
|
-
yield* attempt(
|
|
470
|
-
"remove floating script",
|
|
471
|
-
fs.remove(taskFile.replace(/\.md$/, ".sh")),
|
|
472
|
-
)
|
|
473
|
-
yield* attempt(
|
|
474
|
-
"remove floating exit marker",
|
|
475
|
-
fs.remove(taskFile.replace(/\.md$/, ".exit")),
|
|
476
|
-
)
|
|
477
421
|
yield* attempt("remove replacement artifact", fs.remove(artifactAbs))
|
|
478
422
|
if (backupAbs != null) {
|
|
479
423
|
yield* attempt(
|
|
@@ -505,7 +449,6 @@ export const dispatchWorkflow = (
|
|
|
505
449
|
markPending(preparedAt)
|
|
506
450
|
state.pending_pane_id = null
|
|
507
451
|
state.pending_pane_label = null
|
|
508
|
-
state.pending_floating_exit = null
|
|
509
452
|
const preparedState = yield* Effect.exit(store.save(state, root))
|
|
510
453
|
if (Exit.isFailure(preparedState)) {
|
|
511
454
|
const persistenceError = new HerdrError({
|
|
@@ -544,127 +487,67 @@ export const dispatchWorkflow = (
|
|
|
544
487
|
)
|
|
545
488
|
}
|
|
546
489
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Popups have no pane id — liveness is the exit file; leave role_panes alone.
|
|
568
|
-
state.pending_pane_id = null
|
|
569
|
-
state.pending_pane_label = null
|
|
570
|
-
state.pending_floating_exit = rel(exitAbs, root)
|
|
571
|
-
yield* store.save(state, root)
|
|
572
|
-
const opened = yield* Effect.result(
|
|
573
|
-
herdr.openFloatingPane(scriptAbs, root),
|
|
574
|
-
)
|
|
575
|
-
if (Result.isFailure(opened)) {
|
|
490
|
+
// Interactive TUI: open harness, wait idle, submit pointer via pane run.
|
|
491
|
+
const cmd = roleCmd
|
|
492
|
+
const remembered = state.role_panes[role] ?? null
|
|
493
|
+
const prefer =
|
|
494
|
+
remembered?.profile_fingerprint === profileFingerprint ? remembered : null
|
|
495
|
+
const launched = yield* Effect.result(
|
|
496
|
+
herdr.runInteractivePrompt(role, cmd, prompt, prefer),
|
|
497
|
+
)
|
|
498
|
+
if (Result.isFailure(launched)) {
|
|
499
|
+
if (launched.failure.details?.delivery === "unknown") {
|
|
500
|
+
const paneId = String(launched.failure.details.pane_id)
|
|
501
|
+
const paneLabel = String(launched.failure.details.pane_label)
|
|
502
|
+
state.pending_pane_id = paneId
|
|
503
|
+
state.pending_pane_label = paneLabel
|
|
504
|
+
state.role_panes[role] = {
|
|
505
|
+
pane_id: paneId,
|
|
506
|
+
label: paneLabel,
|
|
507
|
+
profile_fingerprint: profileFingerprint,
|
|
508
|
+
}
|
|
509
|
+
yield* store.save(state, root)
|
|
576
510
|
return yield* new HerdrError({
|
|
577
|
-
message:
|
|
578
|
-
...(
|
|
579
|
-
? { command:
|
|
511
|
+
message: launched.failure.message,
|
|
512
|
+
...(launched.failure.command !== undefined
|
|
513
|
+
? { command: launched.failure.command }
|
|
580
514
|
: {}),
|
|
581
515
|
details: {
|
|
582
|
-
...(
|
|
583
|
-
delivery: "unknown",
|
|
516
|
+
...(launched.failure.details ?? {}),
|
|
584
517
|
task_attempted: taskRef.task,
|
|
585
518
|
artifact: artifactRel,
|
|
586
519
|
pending_preserved: true,
|
|
587
520
|
},
|
|
588
521
|
})
|
|
589
522
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
} else {
|
|
600
|
-
// Interactive TUI: open harness, wait idle, submit pointer via pane run.
|
|
601
|
-
const cmd = roleCmd!
|
|
602
|
-
const remembered = state.role_panes[role] ?? null
|
|
603
|
-
const prefer =
|
|
604
|
-
remembered?.profile_fingerprint === profileFingerprint
|
|
605
|
-
? remembered
|
|
606
|
-
: null
|
|
607
|
-
const launched = yield* Effect.result(
|
|
608
|
-
herdr.runInteractivePrompt(role, cmd, prompt, prefer),
|
|
523
|
+
const rollbackErrors = yield* rollbackLaunch()
|
|
524
|
+
return yield* herdrAfterRollback(
|
|
525
|
+
launched.failure,
|
|
526
|
+
{
|
|
527
|
+
task_attempted: taskRef.task,
|
|
528
|
+
artifact: artifactRel,
|
|
529
|
+
},
|
|
530
|
+
rollbackErrors,
|
|
609
531
|
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
...(launched.failure.details ?? {}),
|
|
630
|
-
task_attempted: taskRef.task,
|
|
631
|
-
artifact: artifactRel,
|
|
632
|
-
pending_preserved: true,
|
|
633
|
-
},
|
|
634
|
-
})
|
|
635
|
-
}
|
|
636
|
-
const rollbackErrors = yield* rollbackLaunch()
|
|
637
|
-
return yield* herdrAfterRollback(
|
|
638
|
-
launched.failure,
|
|
639
|
-
{
|
|
640
|
-
task_attempted: taskRef.task,
|
|
641
|
-
artifact: artifactRel,
|
|
642
|
-
},
|
|
643
|
-
rollbackErrors,
|
|
644
|
-
)
|
|
645
|
-
}
|
|
646
|
-
const r = launched.success
|
|
647
|
-
launch = {
|
|
648
|
-
mode: "interactive",
|
|
649
|
-
pane_id: r.pane_id,
|
|
650
|
-
label: r.label,
|
|
651
|
-
reused: r.reused,
|
|
652
|
-
cmd,
|
|
653
|
-
prompt,
|
|
654
|
-
pane_style: cfg.pane_style,
|
|
655
|
-
pane_style_effective: paneStyle.effective,
|
|
656
|
-
prompt_accepted: r.prompt_accepted,
|
|
657
|
-
prompt_attempts: r.prompt_attempts,
|
|
658
|
-
last_status: r.last_status ?? null,
|
|
659
|
-
}
|
|
660
|
-
state.pending_pane_id = r.pane_id
|
|
661
|
-
state.pending_pane_label = r.label
|
|
662
|
-
state.pending_floating_exit = null
|
|
663
|
-
state.role_panes[role] = {
|
|
664
|
-
pane_id: r.pane_id,
|
|
665
|
-
label: r.label,
|
|
666
|
-
profile_fingerprint: profileFingerprint,
|
|
667
|
-
}
|
|
532
|
+
}
|
|
533
|
+
const r = launched.success
|
|
534
|
+
launch = {
|
|
535
|
+
mode: "interactive",
|
|
536
|
+
pane_id: r.pane_id,
|
|
537
|
+
label: r.label,
|
|
538
|
+
reused: r.reused,
|
|
539
|
+
cmd,
|
|
540
|
+
prompt,
|
|
541
|
+
prompt_accepted: r.prompt_accepted,
|
|
542
|
+
prompt_attempts: r.prompt_attempts,
|
|
543
|
+
last_status: r.last_status ?? null,
|
|
544
|
+
}
|
|
545
|
+
state.pending_pane_id = r.pane_id
|
|
546
|
+
state.pending_pane_label = r.label
|
|
547
|
+
state.role_panes[role] = {
|
|
548
|
+
pane_id: r.pane_id,
|
|
549
|
+
label: r.label,
|
|
550
|
+
profile_fingerprint: profileFingerprint,
|
|
668
551
|
}
|
|
669
552
|
|
|
670
553
|
// After the launch, not before it: `runInteractivePrompt` blocks in
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path"
|
|
2
2
|
import { Effect, Result } from "effect"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
globalConfigPath,
|
|
6
|
-
packageRoot,
|
|
7
|
-
projectConfigPath,
|
|
8
|
-
} from "../domain/paths.ts"
|
|
3
|
+
import { globalConfigPath, projectConfigPath } from "../domain/paths.ts"
|
|
9
4
|
import {
|
|
10
5
|
buildGlobalConfig,
|
|
11
6
|
detectionNotes,
|
|
@@ -16,7 +11,6 @@ import { ConfigError, type AppError } from "../errors.ts"
|
|
|
16
11
|
import { ok, type ToolResult } from "../result.ts"
|
|
17
12
|
import { decodeGlobalConfig } from "../schema/config.ts"
|
|
18
13
|
import { FileSystem } from "../services/file-system.ts"
|
|
19
|
-
import { Herdr } from "../services/herdr.ts"
|
|
20
14
|
|
|
21
15
|
export type SetupParams = {
|
|
22
16
|
/** Write .apnea/config.json role bindings in cwd */
|
|
@@ -78,81 +72,6 @@ export type SetupDeps = {
|
|
|
78
72
|
materializeRoleAgentDir: () => string | null
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
export type ProvisionResult = {
|
|
82
|
-
copied: string | null // dest dir, or null when skipped
|
|
83
|
-
linked: boolean // true only when we ran `herdr plugin link` OK
|
|
84
|
-
already_linked: boolean
|
|
85
|
-
notes: string[]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Copy the package's herdr-plugin into a stable config-local path and, when
|
|
90
|
-
* herdr is new enough and the plugin isn't already linked, run
|
|
91
|
-
* `herdr plugin link`. Never fails — every branch is a note.
|
|
92
|
-
*/
|
|
93
|
-
export const provisionHerdrPlugin = (opts: {
|
|
94
|
-
srcDir: string // packageRoot()/herdr-plugin
|
|
95
|
-
destDir: string // dirname(globalConfigPath())/herdr-plugin
|
|
96
|
-
version: [number, number, number] | null // herdr.version
|
|
97
|
-
}): Effect.Effect<ProvisionResult, never, FileSystem | Herdr> =>
|
|
98
|
-
Effect.gen(function* () {
|
|
99
|
-
const fs = yield* FileSystem
|
|
100
|
-
const herdr = yield* Herdr
|
|
101
|
-
const notes: string[] = []
|
|
102
|
-
|
|
103
|
-
const srcExists = yield* fs.exists(opts.srcDir)
|
|
104
|
-
if (!srcExists) {
|
|
105
|
-
notes.push("herdr-plugin missing from package — reinstall @naxodev/apnea")
|
|
106
|
-
return { copied: null, linked: false, already_linked: false, notes }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
yield* fs.copyDir(opts.srcDir, opts.destDir)
|
|
110
|
-
const runTask = path.join(opts.destDir, "scripts", "run-task.sh")
|
|
111
|
-
if (yield* fs.exists(runTask)) {
|
|
112
|
-
yield* fs.chmod(runTask, 0o755)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!supportsFloating(opts.version)) {
|
|
116
|
-
const ver =
|
|
117
|
-
opts.version == null
|
|
118
|
-
? "unknown"
|
|
119
|
-
: `${opts.version[0]}.${opts.version[1]}.${opts.version[2]}`
|
|
120
|
-
notes.push(
|
|
121
|
-
`herdr ${ver} < 0.7.4 — floating panes unavailable; run \`herdr update\`, then re-run /apnea setup`,
|
|
122
|
-
)
|
|
123
|
-
return {
|
|
124
|
-
copied: opts.destDir,
|
|
125
|
-
linked: false,
|
|
126
|
-
already_linked: false,
|
|
127
|
-
notes,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (yield* herdr.hasApneaPlugin) {
|
|
132
|
-
return {
|
|
133
|
-
copied: opts.destDir,
|
|
134
|
-
linked: false,
|
|
135
|
-
already_linked: true,
|
|
136
|
-
notes,
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const linkResult = yield* herdr.linkPlugin(opts.destDir)
|
|
141
|
-
if (!linkResult.ok) {
|
|
142
|
-
notes.push(
|
|
143
|
-
`herdr plugin link failed: ${linkResult.raw.trim() || "(no output)"}`,
|
|
144
|
-
)
|
|
145
|
-
return {
|
|
146
|
-
copied: opts.destDir,
|
|
147
|
-
linked: false,
|
|
148
|
-
already_linked: false,
|
|
149
|
-
notes,
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { copied: opts.destDir, linked: true, already_linked: false, notes }
|
|
154
|
-
})
|
|
155
|
-
|
|
156
75
|
/**
|
|
157
76
|
* Deterministic Apnea setup: detect binaries, write global profiles
|
|
158
77
|
* (and optional project role bindings). Never writes cmd into project config.
|
|
@@ -161,10 +80,9 @@ export const setupWorkflow = (
|
|
|
161
80
|
params: SetupParams,
|
|
162
81
|
root: string,
|
|
163
82
|
deps: SetupDeps,
|
|
164
|
-
): Effect.Effect<ToolResult, AppError, FileSystem
|
|
83
|
+
): Effect.Effect<ToolResult, AppError, FileSystem> =>
|
|
165
84
|
Effect.gen(function* () {
|
|
166
85
|
const fs = yield* FileSystem
|
|
167
|
-
const herdr = yield* Herdr
|
|
168
86
|
|
|
169
87
|
const readJsonSafe = (
|
|
170
88
|
filePath: string,
|
|
@@ -241,20 +159,6 @@ export const setupWorkflow = (
|
|
|
241
159
|
)
|
|
242
160
|
}
|
|
243
161
|
|
|
244
|
-
let herdrPlugin: ProvisionResult | null = null
|
|
245
|
-
let herdrVer: string | null = null
|
|
246
|
-
if (has.herdr) {
|
|
247
|
-
const version = yield* herdr.version
|
|
248
|
-
herdrVer =
|
|
249
|
-
version == null ? null : `${version[0]}.${version[1]}.${version[2]}`
|
|
250
|
-
herdrPlugin = yield* provisionHerdrPlugin({
|
|
251
|
-
srcDir: path.join(packageRoot(), "herdr-plugin"),
|
|
252
|
-
destDir: path.join(path.dirname(globalConfigPath()), "herdr-plugin"),
|
|
253
|
-
version,
|
|
254
|
-
})
|
|
255
|
-
missing.push(...herdrPlugin.notes)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
162
|
let agentsMdPath: string | null = null
|
|
259
163
|
if (params.agents_md) {
|
|
260
164
|
const target = path.join(root, "AGENTS.md")
|
|
@@ -286,16 +190,5 @@ export const setupWorkflow = (
|
|
|
286
190
|
if (params.agents_md) {
|
|
287
191
|
data.agents_md = agentsMdPath
|
|
288
192
|
}
|
|
289
|
-
if (has.herdr) {
|
|
290
|
-
data.herdr_version = herdrVer
|
|
291
|
-
data.herdr_plugin = herdrPlugin
|
|
292
|
-
? {
|
|
293
|
-
copied: herdrPlugin.copied,
|
|
294
|
-
linked: herdrPlugin.linked,
|
|
295
|
-
already_linked: herdrPlugin.already_linked,
|
|
296
|
-
}
|
|
297
|
-
: null
|
|
298
|
-
}
|
|
299
|
-
|
|
300
193
|
return ok(`wrote global config ${gPath}`, data)
|
|
301
194
|
})
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
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
12
|
import {
|
|
17
13
|
nextAfter,
|
|
@@ -290,16 +286,6 @@ export const waitWorkflow = (
|
|
|
290
286
|
return parseFrontMatter(text)
|
|
291
287
|
})
|
|
292
288
|
|
|
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
289
|
const advanceOnComplete = (
|
|
304
290
|
fm: FrontMatter,
|
|
305
291
|
msg = "artifact ready",
|
|
@@ -384,7 +370,6 @@ export const waitWorkflow = (
|
|
|
384
370
|
state.pending_role = null
|
|
385
371
|
state.pending_pane_id = null
|
|
386
372
|
state.pending_pane_label = null
|
|
387
|
-
state.pending_floating_exit = null
|
|
388
373
|
state.pending_started_at = null
|
|
389
374
|
state.pending_deadline_ms = null
|
|
390
375
|
resetRecoveryLadder(state)
|
|
@@ -406,8 +391,6 @@ export const waitWorkflow = (
|
|
|
406
391
|
)
|
|
407
392
|
})
|
|
408
393
|
|
|
409
|
-
const floatingFlushMs = 2_000
|
|
410
|
-
|
|
411
394
|
let lastStatus = "waiting"
|
|
412
395
|
/**
|
|
413
396
|
* Both counters below are per-call on purpose. They measure a duration
|
|
@@ -418,7 +401,6 @@ export const waitWorkflow = (
|
|
|
418
401
|
*/
|
|
419
402
|
let shellOnlyPolls = 0
|
|
420
403
|
let idleSince: number | null = null
|
|
421
|
-
let floatingExitSeenAt: number | null = null
|
|
422
404
|
let nudged = state.pending_nudged_at != null
|
|
423
405
|
let extendedOnce = state.pending_extended
|
|
424
406
|
let finalNudgeGrace = state.pending_final_grace
|
|
@@ -497,44 +479,6 @@ export const waitWorkflow = (
|
|
|
497
479
|
)
|
|
498
480
|
}
|
|
499
481
|
|
|
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
482
|
if ((yield* herdr.enabled) && state.pending_pane_id) {
|
|
539
483
|
const info = yield* herdr.paneGet(state.pending_pane_id)
|
|
540
484
|
if (!info.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naxodev/apnea",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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"
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
+
"pane_style": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["regular", "floating"],
|
|
41
|
+
"deprecated": true,
|
|
42
|
+
"description": "Deprecated migration-only setting. The runtime accepts and ignores it during migration."
|
|
43
|
+
},
|
|
38
44
|
"review_round_cap": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
39
45
|
"timeouts_ms": {
|
|
40
46
|
"type": "object",
|
|
41
47
|
"additionalProperties": { "type": "integer", "minimum": 1000 }
|
|
42
|
-
},
|
|
43
|
-
"pane_style": {
|
|
44
|
-
"type": "string",
|
|
45
|
-
"enum": ["regular", "floating"],
|
|
46
|
-
"default": "regular",
|
|
47
|
-
"description": "Herdr pane style for role dispatches. \"floating\" applies to oneshot roles only and requires herdr >= 0.7.4; interactive roles always use regular panes. Default: regular."
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -34,9 +34,13 @@
|
|
|
34
34
|
"last_error": { "type": ["string", "null"] },
|
|
35
35
|
"pending_artifact": { "type": ["string", "null"] },
|
|
36
36
|
"pending_role": { "type": ["string", "null"] },
|
|
37
|
+
"pending_floating_exit": {
|
|
38
|
+
"type": "null",
|
|
39
|
+
"deprecated": true,
|
|
40
|
+
"description": "Deprecated migration-only field. Missing or null values migrate; active non-null legacy floating runs must be abandoned."
|
|
41
|
+
},
|
|
37
42
|
"pending_pane_id": { "type": ["string", "null"] },
|
|
38
43
|
"pending_pane_label": { "type": ["string", "null"] },
|
|
39
|
-
"pending_floating_exit": { "type": ["string", "null"] },
|
|
40
44
|
"pending_started_at": { "type": ["integer", "null"] },
|
|
41
45
|
"pending_deadline_ms": { "type": ["integer", "null"] },
|
|
42
46
|
"pending_nudged_at": { "type": ["integer", "null"] },
|
|
@@ -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"
|