@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,11 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path"
|
|
2
|
+
import * as os from "node:os"
|
|
2
3
|
import { Effect, Result } from "effect"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
globalConfigPath,
|
|
6
|
-
packageRoot,
|
|
7
|
-
projectConfigPath,
|
|
8
|
-
} from "../domain/paths.ts"
|
|
4
|
+
import { globalConfigPath, projectConfigPath } from "../domain/paths.ts"
|
|
9
5
|
import {
|
|
10
6
|
buildGlobalConfig,
|
|
11
7
|
detectionNotes,
|
|
@@ -14,9 +10,8 @@ import {
|
|
|
14
10
|
} from "../domain/setup.ts"
|
|
15
11
|
import { ConfigError, type AppError } from "../errors.ts"
|
|
16
12
|
import { ok, type ToolResult } from "../result.ts"
|
|
17
|
-
import { decodeGlobalConfig } from "../schema/config.ts"
|
|
13
|
+
import { decodeGlobalConfig, decodeProjectConfig } from "../schema/config.ts"
|
|
18
14
|
import { FileSystem } from "../services/file-system.ts"
|
|
19
|
-
import { Herdr } from "../services/herdr.ts"
|
|
20
15
|
|
|
21
16
|
export type SetupParams = {
|
|
22
17
|
/** Write .apnea/config.json role bindings in cwd */
|
|
@@ -76,83 +71,10 @@ export type SetupDeps = {
|
|
|
76
71
|
onPath: (bin: string) => boolean
|
|
77
72
|
/** Prepare host-specific role resources, or return null when none are needed. */
|
|
78
73
|
materializeRoleAgentDir: () => string | null
|
|
79
|
-
|
|
80
|
-
|
|
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[]
|
|
74
|
+
/** Trusted account home. Production uses node:os.homedir(). */
|
|
75
|
+
trustedHome?: () => string
|
|
86
76
|
}
|
|
87
77
|
|
|
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
78
|
/**
|
|
157
79
|
* Deterministic Apnea setup: detect binaries, write global profiles
|
|
158
80
|
* (and optional project role bindings). Never writes cmd into project config.
|
|
@@ -161,28 +83,9 @@ export const setupWorkflow = (
|
|
|
161
83
|
params: SetupParams,
|
|
162
84
|
root: string,
|
|
163
85
|
deps: SetupDeps,
|
|
164
|
-
): Effect.Effect<ToolResult, AppError, FileSystem
|
|
86
|
+
): Effect.Effect<ToolResult, AppError, FileSystem> =>
|
|
165
87
|
Effect.gen(function* () {
|
|
166
88
|
const fs = yield* FileSystem
|
|
167
|
-
const herdr = yield* Herdr
|
|
168
|
-
|
|
169
|
-
const readJsonSafe = (
|
|
170
|
-
filePath: string,
|
|
171
|
-
): Effect.Effect<Record<string, unknown>> =>
|
|
172
|
-
Effect.gen(function* () {
|
|
173
|
-
const present = yield* fs.exists(filePath)
|
|
174
|
-
if (!present) return {}
|
|
175
|
-
const text = yield* fs.readFile(filePath)
|
|
176
|
-
try {
|
|
177
|
-
const v = JSON.parse(text)
|
|
178
|
-
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
179
|
-
return v as Record<string, unknown>
|
|
180
|
-
}
|
|
181
|
-
return {}
|
|
182
|
-
} catch {
|
|
183
|
-
return {}
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
89
|
|
|
187
90
|
const has: Detected = {
|
|
188
91
|
pi: deps.onPath("pi"),
|
|
@@ -200,27 +103,64 @@ export const setupWorkflow = (
|
|
|
200
103
|
})
|
|
201
104
|
}
|
|
202
105
|
|
|
203
|
-
const
|
|
204
|
-
|
|
106
|
+
const trustedHome = deps.trustedHome?.() ?? os.homedir()
|
|
107
|
+
const gPath = globalConfigPath(trustedHome)
|
|
205
108
|
|
|
206
|
-
const prev = yield* readJsonSafe(gPath)
|
|
207
109
|
const force = params.force === true
|
|
110
|
+
let replacedMalformedGlobal = false
|
|
111
|
+
let prev: Record<string, unknown> = {}
|
|
112
|
+
if (yield* fs.exists(gPath)) {
|
|
113
|
+
const text = yield* fs.readTrustedGlobalFile(trustedHome, gPath)
|
|
114
|
+
try {
|
|
115
|
+
const parsed: unknown = JSON.parse(text)
|
|
116
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
117
|
+
throw new Error("global config must be a JSON object")
|
|
118
|
+
}
|
|
119
|
+
prev = parsed as Record<string, unknown>
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (!force) {
|
|
122
|
+
return yield* new ConfigError({
|
|
123
|
+
message: `existing global config is malformed; refusing to replace it without --force: ${error instanceof Error ? error.message : String(error)}`,
|
|
124
|
+
path: gPath,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
replacedMalformedGlobal = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
208
130
|
const globalConfig = buildGlobalConfig({ has, prev, force })
|
|
209
131
|
|
|
210
132
|
const serialized = `${JSON.stringify(globalConfig, null, 2)}\n`
|
|
211
|
-
yield* fs.writeFile(gPath, serialized)
|
|
212
133
|
|
|
213
134
|
let projectPath: string | null = null
|
|
214
135
|
if (params.project) {
|
|
215
136
|
const pPath = projectConfigPath(root)
|
|
137
|
+
if (yield* fs.projectPathExists(root, pPath)) {
|
|
138
|
+
const text = yield* fs.readProjectFile(root, pPath)
|
|
139
|
+
let parsed: unknown
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(text)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return yield* new ConfigError({
|
|
144
|
+
message: `invalid JSON at ${pPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
145
|
+
path: pPath,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
const decoded = decodeProjectConfig(parsed)
|
|
149
|
+
if (Result.isFailure(decoded)) return yield* decoded.failure
|
|
150
|
+
}
|
|
151
|
+
projectPath = pPath
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
yield* fs.writeTrustedGlobalFile(trustedHome, gPath, serialized)
|
|
155
|
+
|
|
156
|
+
if (projectPath) {
|
|
216
157
|
// project: roles only — never cmd
|
|
217
158
|
const projectCfg = { roles: pickRoles(has) }
|
|
218
159
|
yield* fs.writeProjectFile(
|
|
219
160
|
root,
|
|
220
|
-
|
|
161
|
+
projectPath,
|
|
221
162
|
`${JSON.stringify(projectCfg, null, 2)}\n`,
|
|
222
163
|
)
|
|
223
|
-
projectPath = pPath
|
|
224
164
|
}
|
|
225
165
|
|
|
226
166
|
const missing = detectionNotes(has)
|
|
@@ -241,32 +181,17 @@ export const setupWorkflow = (
|
|
|
241
181
|
)
|
|
242
182
|
}
|
|
243
183
|
|
|
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
184
|
let agentsMdPath: string | null = null
|
|
259
185
|
if (params.agents_md) {
|
|
260
186
|
const target = path.join(root, "AGENTS.md")
|
|
261
|
-
const present = yield* fs.
|
|
262
|
-
const existing = present ? yield* fs.
|
|
263
|
-
yield* fs.
|
|
187
|
+
const present = yield* fs.projectPathExists(root, target)
|
|
188
|
+
const existing = present ? yield* fs.readProjectFile(root, target) : null
|
|
189
|
+
yield* fs.writeProjectFile(root, target, mergeAgentsMd(existing))
|
|
264
190
|
agentsMdPath = target
|
|
265
191
|
}
|
|
266
192
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// actionable note, never a hard refusal where today there is none.
|
|
193
|
+
// Generated defaults can still inherit malformed profile or role shapes
|
|
194
|
+
// from valid JSON. Report those separately from JSON parse refusals.
|
|
270
195
|
const decoded = decodeGlobalConfig(globalConfig)
|
|
271
196
|
if (Result.isFailure(decoded)) {
|
|
272
197
|
missing.push(
|
|
@@ -286,16 +211,11 @@ export const setupWorkflow = (
|
|
|
286
211
|
if (params.agents_md) {
|
|
287
212
|
data.agents_md = agentsMdPath
|
|
288
213
|
}
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
? {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
: null
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return ok(`wrote global config ${gPath}`, data)
|
|
214
|
+
if (replacedMalformedGlobal) data.replaced_malformed_global = true
|
|
215
|
+
return ok(
|
|
216
|
+
replacedMalformedGlobal
|
|
217
|
+
? `replaced malformed global config ${gPath}`
|
|
218
|
+
: `wrote global config ${gPath}`,
|
|
219
|
+
data,
|
|
220
|
+
)
|
|
301
221
|
})
|
|
@@ -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,9 +112,9 @@ 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
|
-
pending_floating_exit: null,
|
|
118
118
|
pending_started_at: null,
|
|
119
119
|
pending_deadline_ms: null,
|
|
120
120
|
pending_nudged_at: null,
|
|
@@ -125,7 +125,8 @@ export const startWorkflow = (
|
|
|
125
125
|
reviewer_tree_fingerprint: null,
|
|
126
126
|
current_phase_package: null,
|
|
127
127
|
current_code_review: null,
|
|
128
|
-
|
|
128
|
+
required_rework: null,
|
|
129
|
+
pending_commit: null,
|
|
129
130
|
}
|
|
130
131
|
// The literal above assigns the ladder's fields; this re-asserts them
|
|
131
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
|