@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
package/extension/cli/parse.ts
CHANGED
|
@@ -11,7 +11,16 @@ export function parseFlags(tokens: string[]): {
|
|
|
11
11
|
const flags = new Set<string>()
|
|
12
12
|
const values = new Map<string, string>()
|
|
13
13
|
const rest: string[] = []
|
|
14
|
+
let positionalOnly = false
|
|
14
15
|
for (const t of tokens) {
|
|
16
|
+
if (positionalOnly) {
|
|
17
|
+
rest.push(t)
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
if (t === "--") {
|
|
21
|
+
positionalOnly = true
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
15
24
|
if (!t.startsWith("--")) {
|
|
16
25
|
rest.push(t)
|
|
17
26
|
continue
|
|
@@ -24,6 +33,162 @@ export function parseFlags(tokens: string[]): {
|
|
|
24
33
|
return { flags, values, rest }
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
type OperationArgSpec = {
|
|
37
|
+
switches: readonly string[]
|
|
38
|
+
values: readonly string[]
|
|
39
|
+
minPositionals: number
|
|
40
|
+
maxPositionals: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const OPERATION_ARGS: Readonly<Record<string, OperationArgSpec>> = {
|
|
44
|
+
setup: {
|
|
45
|
+
switches: ["project", "force", "agents-md"],
|
|
46
|
+
values: [],
|
|
47
|
+
minPositionals: 0,
|
|
48
|
+
maxPositionals: 0,
|
|
49
|
+
},
|
|
50
|
+
start: {
|
|
51
|
+
switches: ["allow-dirty"],
|
|
52
|
+
values: ["slug"],
|
|
53
|
+
minPositionals: 1,
|
|
54
|
+
maxPositionals: Number.POSITIVE_INFINITY,
|
|
55
|
+
},
|
|
56
|
+
resume: { switches: [], values: [], minPositionals: 0, maxPositionals: 0 },
|
|
57
|
+
abandon: { switches: [], values: [], minPositionals: 0, maxPositionals: 0 },
|
|
58
|
+
help: { switches: [], values: [], minPositionals: 0, maxPositionals: 0 },
|
|
59
|
+
status: { switches: [], values: [], minPositionals: 0, maxPositionals: 0 },
|
|
60
|
+
wait: {
|
|
61
|
+
switches: [],
|
|
62
|
+
values: ["poll", "budget", "timeout"],
|
|
63
|
+
minPositionals: 0,
|
|
64
|
+
maxPositionals: 0,
|
|
65
|
+
},
|
|
66
|
+
dispatch: {
|
|
67
|
+
switches: ["rework", "redeliver"],
|
|
68
|
+
values: [],
|
|
69
|
+
minPositionals: 1,
|
|
70
|
+
maxPositionals: 1,
|
|
71
|
+
},
|
|
72
|
+
commit: {
|
|
73
|
+
switches: ["done"],
|
|
74
|
+
values: [],
|
|
75
|
+
minPositionals: 0,
|
|
76
|
+
maxPositionals: Number.POSITIVE_INFINITY,
|
|
77
|
+
},
|
|
78
|
+
"reset-rounds": {
|
|
79
|
+
switches: [],
|
|
80
|
+
values: [],
|
|
81
|
+
minPositionals: 1,
|
|
82
|
+
maxPositionals: 1,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ParsedOperationArgs =
|
|
87
|
+
| {
|
|
88
|
+
ok: true
|
|
89
|
+
flags: Set<string>
|
|
90
|
+
values: Map<string, string>
|
|
91
|
+
positional: string[]
|
|
92
|
+
}
|
|
93
|
+
| { ok: false; message: string }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse and validate one human-facing Apnea invocation. This is the shared
|
|
97
|
+
* strict boundary for the standalone CLI and Pi's `/apnea` command.
|
|
98
|
+
*/
|
|
99
|
+
export function parseOperationArgs(
|
|
100
|
+
verb: string,
|
|
101
|
+
tokens: string[],
|
|
102
|
+
options: { surface?: "cli" | "slash" } = {},
|
|
103
|
+
): ParsedOperationArgs {
|
|
104
|
+
const spec = OPERATION_ARGS[verb]
|
|
105
|
+
if (!spec) return { ok: false, message: `unknown command: ${verb}` }
|
|
106
|
+
|
|
107
|
+
const allowedSwitches = new Set(spec.switches)
|
|
108
|
+
if (options.surface === "cli") {
|
|
109
|
+
allowedSwitches.add("json")
|
|
110
|
+
if (verb === "reset-rounds") allowedSwitches.add("i-am-human")
|
|
111
|
+
}
|
|
112
|
+
const allowedValues = new Set(spec.values)
|
|
113
|
+
const numericValues = new Set(["poll", "budget", "timeout"])
|
|
114
|
+
const flags = new Set<string>()
|
|
115
|
+
const values = new Map<string, string>()
|
|
116
|
+
const positional: string[] = []
|
|
117
|
+
let positionalOnly = false
|
|
118
|
+
|
|
119
|
+
for (const token of tokens) {
|
|
120
|
+
if (positionalOnly) {
|
|
121
|
+
positional.push(token)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (token === "--") {
|
|
125
|
+
positionalOnly = true
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (!token.startsWith("--")) {
|
|
129
|
+
positional.push(token)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const body = token.slice(2)
|
|
134
|
+
const equals = body.indexOf("=")
|
|
135
|
+
const key = equals >= 0 ? body.slice(0, equals) : body
|
|
136
|
+
const value = equals >= 0 ? body.slice(equals + 1) : undefined
|
|
137
|
+
|
|
138
|
+
if (allowedSwitches.has(key)) {
|
|
139
|
+
if (value !== undefined)
|
|
140
|
+
return { ok: false, message: `option --${key} does not take a value` }
|
|
141
|
+
if (flags.has(key))
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: `option --${key} was provided more than once`,
|
|
145
|
+
}
|
|
146
|
+
flags.add(key)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (allowedValues.has(key)) {
|
|
150
|
+
if (value === undefined)
|
|
151
|
+
return { ok: false, message: `option --${key} requires =<value>` }
|
|
152
|
+
if (numericValues.has(key)) {
|
|
153
|
+
const numeric = Number(value)
|
|
154
|
+
if (
|
|
155
|
+
value === "" ||
|
|
156
|
+
value.trim() !== value ||
|
|
157
|
+
!Number.isSafeInteger(numeric) ||
|
|
158
|
+
numeric <= 0
|
|
159
|
+
) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
message: `invalid numeric option --${key}=${value}`,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (values.has(key))
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
message: `option --${key} was provided more than once`,
|
|
170
|
+
}
|
|
171
|
+
values.set(key, value)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
return { ok: false, message: `unknown option --${key} for ${verb}` }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
positional.length < spec.minPositionals ||
|
|
179
|
+
positional.length > spec.maxPositionals
|
|
180
|
+
) {
|
|
181
|
+
return { ok: false, message: `invalid positional arguments for ${verb}` }
|
|
182
|
+
}
|
|
183
|
+
if (values.has("budget") && values.has("timeout")) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
message: "--budget and --timeout are aliases; provide only one",
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, flags, values, positional }
|
|
190
|
+
}
|
|
191
|
+
|
|
27
192
|
/** Reading a `--key=value` numeric flag either yields the parsed number (or
|
|
28
193
|
* `undefined` when the caller didn't pass it) or the raw token that failed
|
|
29
194
|
* to parse, so the caller can name exactly what it received. */
|
|
@@ -33,16 +198,18 @@ export type NumFlag =
|
|
|
33
198
|
/**
|
|
34
199
|
* Shared by `/apnea` and the CLI so a mistyped `--budget=abc` is refused the
|
|
35
200
|
* same way on both surfaces instead of silently falling back to a default —
|
|
36
|
-
* a scripting agent needs a signal, not a quietly-wrong value.
|
|
37
|
-
*
|
|
38
|
-
* certainly forgot the value, not asked for zero.
|
|
201
|
+
* a scripting agent needs a signal, not a quietly-wrong value. This exported
|
|
202
|
+
* boundary rejects empty and padded values even when called directly.
|
|
39
203
|
*/
|
|
40
204
|
export function parseNumFlag(
|
|
41
205
|
values: Map<string, string>,
|
|
42
206
|
key: string,
|
|
43
207
|
): NumFlag {
|
|
44
208
|
const raw = values.get(key)
|
|
45
|
-
if (raw === undefined
|
|
209
|
+
if (raw === undefined) return { ok: true, value: undefined }
|
|
210
|
+
if (raw === "" || raw.trim() !== raw) return { ok: false, raw }
|
|
46
211
|
const n = Number(raw)
|
|
47
|
-
return Number.
|
|
212
|
+
return Number.isSafeInteger(n) && n > 0
|
|
213
|
+
? { ok: true, value: n }
|
|
214
|
+
: { ok: false, raw }
|
|
48
215
|
}
|
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
import type { PaneStyle, Role } from "./types.ts"
|
|
2
|
-
|
|
3
|
-
/** Parse `herdr X.Y.Z` (or noisy multi-line) into a numeric tuple. */
|
|
4
|
-
export function parseHerdrVersion(
|
|
5
|
-
raw: string,
|
|
6
|
-
): [number, number, number] | null {
|
|
7
|
-
const m = raw.match(/(\d+)\.(\d+)\.(\d+)/)
|
|
8
|
-
if (!m) return null
|
|
9
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function versionGte(
|
|
13
|
-
a: [number, number, number],
|
|
14
|
-
b: [number, number, number],
|
|
15
|
-
): boolean {
|
|
16
|
-
const [a0, a1, a2] = a
|
|
17
|
-
const [b0, b1, b2] = b
|
|
18
|
-
if (a0 !== b0) return a0 > b0
|
|
19
|
-
if (a1 !== b1) return a1 > b1
|
|
20
|
-
return a2 >= b2
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Floating popups need herdr ≥ 0.7.4. Fail closed on unparseable versions so an
|
|
25
|
-
* unattended run never hangs on a CLI that rejects `--placement popup`.
|
|
26
|
-
*/
|
|
27
|
-
export function supportsFloating(
|
|
28
|
-
version: [number, number, number] | null,
|
|
29
|
-
): boolean {
|
|
30
|
-
return version != null && versionGte(version, [0, 7, 4])
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Configured style vs effective style. Floating is only for planner/reviewer
|
|
35
|
-
* (oneshot-eligible artifact producers); interactive roles always stay regular.
|
|
36
|
-
*/
|
|
37
|
-
export function effectivePaneStyle(
|
|
38
|
-
configured: PaneStyle,
|
|
39
|
-
role: Role,
|
|
40
|
-
): { style: PaneStyle; effective: string } {
|
|
41
|
-
if (configured === "regular") {
|
|
42
|
-
return { style: "regular", effective: "regular" }
|
|
43
|
-
}
|
|
44
|
-
if (role === "planner" || role === "reviewer") {
|
|
45
|
-
return { style: "floating", effective: "floating" }
|
|
46
|
-
}
|
|
47
|
-
return { style: "regular", effective: "regular (interactive role)" }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
1
|
export function shellJoin(parts: string[]): string {
|
|
51
2
|
return parts
|
|
52
3
|
.map((p) => {
|
|
@@ -70,40 +21,3 @@ export function looksLikeShellOnly(names: string[]): boolean {
|
|
|
70
21
|
)
|
|
71
22
|
})
|
|
72
23
|
}
|
|
73
|
-
|
|
74
|
-
/** Parse a floating task's exit-file contents; null if not a finished exit code. */
|
|
75
|
-
export function parseFloatingExit(text: string): number | null {
|
|
76
|
-
const t = text.trim()
|
|
77
|
-
const n = Number.parseInt(t, 10)
|
|
78
|
-
return Number.isFinite(n) ? n : null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Self-contained bash script body that cds to root, runs cmd + prompt as a
|
|
83
|
-
* child (not exec — so EXIT trap still fires), and always records the exit
|
|
84
|
-
* code for workflow_wait. Popups have no pane id; the exit file is the
|
|
85
|
-
* liveness signal.
|
|
86
|
-
*/
|
|
87
|
-
export function floatingTaskScriptBody(opts: {
|
|
88
|
-
root: string
|
|
89
|
-
resolvedCmd: string[]
|
|
90
|
-
prompt: string
|
|
91
|
-
exitFileAbs: string
|
|
92
|
-
}): string {
|
|
93
|
-
return [
|
|
94
|
-
"#!/bin/bash",
|
|
95
|
-
"set -uo pipefail",
|
|
96
|
-
`EXIT_FILE=${shellJoin([opts.exitFileAbs])}`,
|
|
97
|
-
"write_exit() {",
|
|
98
|
-
" local st=$?",
|
|
99
|
-
` printf '%s\n' "$st" > "$EXIT_FILE" 2>/dev/null || true`,
|
|
100
|
-
"}",
|
|
101
|
-
"trap write_exit EXIT",
|
|
102
|
-
"trap 'exit 129' HUP",
|
|
103
|
-
"trap 'exit 130' INT",
|
|
104
|
-
"trap 'exit 143' TERM",
|
|
105
|
-
shellJoin(["cd", opts.root]),
|
|
106
|
-
shellJoin([...opts.resolvedCmd, "--", opts.prompt]),
|
|
107
|
-
"",
|
|
108
|
-
].join("\n")
|
|
109
|
-
}
|
|
@@ -21,17 +21,8 @@ export function projectConfigPath(root = cwd()): string {
|
|
|
21
21
|
return path.join(apneaRoot(root), "config.json")
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function homedir(): string {
|
|
25
|
-
|
|
26
|
-
// passwd entry and ignores $HOME), then the passwd entry as the floor.
|
|
27
|
-
// Never "": an empty home makes globalConfigPath() cwd-relative, i.e. the
|
|
28
|
-
// *project* repo would be read as the trusted global config — the one place
|
|
29
|
-
// profiles/cmd_interactive are honoured. node:os is a pure read here.
|
|
30
|
-
return process.env.HOME || process.env.USERPROFILE || os.homedir()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function globalConfigPath(): string {
|
|
34
|
-
return path.join(homedir(), ".config", "apnea", "config.json")
|
|
24
|
+
export function globalConfigPath(home = os.homedir()): string {
|
|
25
|
+
return path.join(home, ".config", "apnea", "config.json")
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
export function artifactsDir(root = cwd()): string {
|
|
@@ -107,8 +98,7 @@ export function findPackageRootFrom(startDir: string): string | null {
|
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
/**
|
|
110
|
-
* Package root — the directory holding `briefs
|
|
111
|
-
* own `package.json`.
|
|
101
|
+
* Package root — the directory holding `briefs/` and our own `package.json`.
|
|
112
102
|
*
|
|
113
103
|
* Found by walking up and reading each `package.json`, NOT by counting
|
|
114
104
|
* directory levels. The level count differs for every way this code runs, and
|
|
@@ -21,19 +21,6 @@ export function deepMergeProfiles(
|
|
|
21
21
|
return out
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Carry a valid user pane_style preference forward. Setup never writes the
|
|
26
|
-
* key when absent, and never invents values — only preserves exact "regular"
|
|
27
|
-
* or "floating". Invalid prev values are dropped.
|
|
28
|
-
*/
|
|
29
|
-
export function preservePaneStyle(
|
|
30
|
-
prev: Record<string, unknown>,
|
|
31
|
-
): "regular" | "floating" | undefined {
|
|
32
|
-
const v = prev.pane_style
|
|
33
|
-
if (v === "regular" || v === "floating") return v
|
|
34
|
-
return undefined
|
|
35
|
-
}
|
|
36
|
-
|
|
37
24
|
export function buildProfiles(has: Detected): Record<string, unknown> {
|
|
38
25
|
const profiles: Record<string, unknown> = {}
|
|
39
26
|
|
|
@@ -119,8 +106,6 @@ export function buildGlobalConfig(opts: {
|
|
|
119
106
|
)
|
|
120
107
|
}
|
|
121
108
|
|
|
122
|
-
const preservedPaneStyle = preservePaneStyle(prev)
|
|
123
|
-
|
|
124
109
|
const globalConfig: Record<string, unknown> = {
|
|
125
110
|
profiles: nextProfiles,
|
|
126
111
|
roles: force || !prev.roles ? roles : prev.roles,
|
|
@@ -133,11 +118,6 @@ export function buildGlobalConfig(opts: {
|
|
|
133
118
|
// values wait/commit actually use cannot drift apart.
|
|
134
119
|
{ ...DEFAULT_TIMEOUTS },
|
|
135
120
|
}
|
|
136
|
-
// Preserve user opt-in only — never introduce pane_style when absent.
|
|
137
|
-
if (preservedPaneStyle !== undefined) {
|
|
138
|
-
globalConfig.pane_style = preservedPaneStyle
|
|
139
|
-
}
|
|
140
|
-
|
|
141
121
|
return globalConfig
|
|
142
122
|
}
|
|
143
123
|
|
|
@@ -16,6 +16,10 @@ const STEP_KEY: Record<DispatchKind, string> = {
|
|
|
16
16
|
|
|
17
17
|
export const DEFAULT_TIMEOUT_MS = 900_000
|
|
18
18
|
|
|
19
|
+
export function deadlineAfter(start: number, duration: number): number {
|
|
20
|
+
return Math.min(Number.MAX_SAFE_INTEGER, start + duration)
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
export function timeoutMsForKind(
|
|
20
24
|
kind: DispatchKind,
|
|
21
25
|
timeouts: Record<string, number>,
|
|
@@ -12,12 +12,59 @@ export type Role = "orchestrator" | "planner" | "reviewer" | "coder"
|
|
|
12
12
|
|
|
13
13
|
export type RoleMode = "oneshot" | "interactive"
|
|
14
14
|
|
|
15
|
-
export type
|
|
15
|
+
export type PendingDelivery = "manual" | "interactive"
|
|
16
16
|
|
|
17
17
|
export type Verdict = "APPROVED" | "CHANGES_REQUIRED"
|
|
18
18
|
|
|
19
19
|
export type ReworkTarget = "code" | "phase_package"
|
|
20
20
|
|
|
21
|
+
export type RequiredReworkTarget = "plan" | ReworkTarget
|
|
22
|
+
|
|
23
|
+
/** Fields every pending commit carries regardless of backend. */
|
|
24
|
+
export interface PendingCommitCore {
|
|
25
|
+
/** Transaction id; appears in the commit message as `Apnea-Transaction: <id>`. */
|
|
26
|
+
id: string
|
|
27
|
+
/** Phase whose completion this transaction commits. */
|
|
28
|
+
phase_index: number
|
|
29
|
+
/** Full commit message including the `Apnea-Transaction:` trailer line. */
|
|
30
|
+
message: string
|
|
31
|
+
/** Whether completion advances to finishing instead of the next phase. */
|
|
32
|
+
no_remaining_phases: boolean
|
|
33
|
+
/** Repo-relative `.apnea/` path of the verify log for this phase. */
|
|
34
|
+
verify_log: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Git anchor captured before any ref moves: the exact commit to create. */
|
|
38
|
+
export interface GitPendingCommit extends PendingCommitCore {
|
|
39
|
+
backend: "git"
|
|
40
|
+
/** Full ref (e.g. `refs/heads/apnea/slug`) the commit must land on. */
|
|
41
|
+
branch: string
|
|
42
|
+
/** Expected HEAD when completion starts; also the created commit's parent. */
|
|
43
|
+
parent_commit: string
|
|
44
|
+
/** Prepared tree id (staged with `.apnea` excluded). */
|
|
45
|
+
tree_id: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** jj anchor captured right after describing `@`. */
|
|
49
|
+
export interface JjPendingCommit extends PendingCommitCore {
|
|
50
|
+
backend: "jj"
|
|
51
|
+
/** Change id of the described working-copy change. */
|
|
52
|
+
change_id: string
|
|
53
|
+
/** Fingerprint of the change's non-.apnea diff at preparation time. */
|
|
54
|
+
content_fingerprint: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Durable record of an in-flight commit. Written after VCS preparation and
|
|
59
|
+
* before completion, so a crash anywhere afterwards can recognize and finish
|
|
60
|
+
* the transaction exactly once.
|
|
61
|
+
*/
|
|
62
|
+
export type PendingCommit = GitPendingCommit | JjPendingCommit
|
|
63
|
+
|
|
64
|
+
/** Internal decode marker for ambiguous version-1 planning state. */
|
|
65
|
+
export const LEGACY_PLAN_REWORK = Symbol("apnea.legacy-plan-rework")
|
|
66
|
+
export const LEGACY_CODE_REWORK = Symbol("apnea.legacy-code-rework")
|
|
67
|
+
|
|
21
68
|
export type VcsBackend = "jj" | "git"
|
|
22
69
|
|
|
23
70
|
export interface Profile {
|
|
@@ -34,11 +81,10 @@ export interface ApneaConfig {
|
|
|
34
81
|
roles: Record<string, RoleBinding>
|
|
35
82
|
review_round_cap: number
|
|
36
83
|
timeouts_ms: Record<string, number>
|
|
37
|
-
pane_style: PaneStyle
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
export interface RunState {
|
|
41
|
-
version:
|
|
87
|
+
version: 2
|
|
42
88
|
slug: string
|
|
43
89
|
step: Step
|
|
44
90
|
phase_index: number
|
|
@@ -53,16 +99,12 @@ export interface RunState {
|
|
|
53
99
|
pending_artifact: string | null
|
|
54
100
|
/** Role for pending dispatch */
|
|
55
101
|
pending_role: Role | null
|
|
102
|
+
/** Delivery boundary crossed for the pending dispatch, if known. */
|
|
103
|
+
pending_delivery: PendingDelivery | null
|
|
56
104
|
/** Herdr pane id for the in-flight dispatch */
|
|
57
105
|
pending_pane_id: string | null
|
|
58
106
|
/** Label of that pane (apnea:role:unique) */
|
|
59
107
|
pending_pane_label: string | null
|
|
60
|
-
/**
|
|
61
|
-
* Relative path to the floating oneshot exit-status file (written when the
|
|
62
|
-
* popup worker process ends). Null for regular panes. Herdr popups have no
|
|
63
|
-
* pane id — this is how wait detects death without hanging until timeout.
|
|
64
|
-
*/
|
|
65
|
-
pending_floating_exit: string | null
|
|
66
108
|
/**
|
|
67
109
|
* Epoch ms when the in-flight dispatch was launched. Null when idle.
|
|
68
110
|
* Persisted so a chunked `workflow_wait` measures elapsed time from the
|
|
@@ -108,8 +150,19 @@ export interface RunState {
|
|
|
108
150
|
current_phase_package: string | null
|
|
109
151
|
/** Last code-review path for commit gate */
|
|
110
152
|
current_code_review: string | null
|
|
111
|
-
/** The
|
|
112
|
-
|
|
153
|
+
/** The exact dispatch that owns the next review round, if any. */
|
|
154
|
+
required_rework: RequiredReworkTarget | null
|
|
155
|
+
/**
|
|
156
|
+
* In-flight commit transaction, if any. Set after VCS preparation and
|
|
157
|
+
* cleared only after completion + bookmark succeed; a crash leaves it
|
|
158
|
+
* durable so the next `workflow_commit_phase` call resumes that
|
|
159
|
+
* transaction instead of re-running gates and verification.
|
|
160
|
+
*/
|
|
161
|
+
pending_commit: PendingCommit | null
|
|
162
|
+
/** Never serialized. An old planning state needs an explicit assertion. */
|
|
163
|
+
[LEGACY_PLAN_REWORK]?: true
|
|
164
|
+
/** Never serialized. Ambiguous old coding state needs an explicit assertion. */
|
|
165
|
+
[LEGACY_CODE_REWORK]?: true
|
|
113
166
|
}
|
|
114
167
|
|
|
115
168
|
export interface FrontMatter {
|