@naxodev/apnea 0.1.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/CONTEXT.md +61 -0
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/SECURITY.md +35 -0
- package/briefs/coder.md +40 -0
- package/briefs/orchestrator.md +49 -0
- package/briefs/planner.md +54 -0
- package/briefs/reviewer.md +40 -0
- package/dist/cli.js +39397 -0
- package/docs/adr/0001-completion-signaling.md +3 -0
- package/docs/adr/0002-orchestrator-authority.md +3 -0
- package/docs/adr/0003-verify-at-gate.md +3 -0
- package/docs/adr/0004-artifact-layout-and-naming.md +3 -0
- package/docs/adr/0005-harness-profiles.md +5 -0
- package/docs/adr/0006-config-trust-model.md +3 -0
- package/docs/adr/0007-jj-first-commits.md +3 -0
- package/docs/adr/0008-effect-v4-internals.md +3 -0
- package/docs/adr/0009-cli-driver-split.md +9 -0
- package/docs/adr/0010-package-split.md +23 -0
- package/docs/protocol/artifacts.md +68 -0
- package/docs/protocol/config.md +186 -0
- package/docs/protocol/manual-gate.md +38 -0
- package/docs/protocol/overview.md +96 -0
- package/extension/adapters/commit.ts +15 -0
- package/extension/adapters/dispatch.ts +15 -0
- package/extension/adapters/setup.ts +34 -0
- package/extension/adapters/start.ts +16 -0
- package/extension/adapters/status.ts +24 -0
- package/extension/adapters/wait.ts +20 -0
- package/extension/api.ts +16 -0
- package/extension/cli/format.ts +44 -0
- package/extension/cli/human-gate.ts +44 -0
- package/extension/cli/main.ts +218 -0
- package/extension/cli/parse.ts +48 -0
- package/extension/domain/artifact-kind.ts +26 -0
- package/extension/domain/frontmatter.ts +69 -0
- package/extension/domain/herdr.ts +109 -0
- package/extension/domain/paths.ts +139 -0
- package/extension/domain/recovery.ts +25 -0
- package/extension/domain/rounds.ts +16 -0
- package/extension/domain/setup.ts +158 -0
- package/extension/domain/slug.ts +9 -0
- package/extension/domain/state-machine.ts +132 -0
- package/extension/domain/timeouts.ts +24 -0
- package/extension/domain/types.ts +145 -0
- package/extension/domain/verify-commands.ts +128 -0
- package/extension/errors.ts +247 -0
- package/extension/host-adapter.ts +8 -0
- package/extension/registry.ts +323 -0
- package/extension/result.ts +55 -0
- package/extension/run-tool.ts +43 -0
- package/extension/schema/config.ts +315 -0
- package/extension/schema/frontmatter.ts +34 -0
- package/extension/schema/state.ts +119 -0
- package/extension/services/app-live.ts +24 -0
- package/extension/services/config.ts +103 -0
- package/extension/services/file-system.ts +178 -0
- package/extension/services/herdr.ts +860 -0
- package/extension/services/run-store.ts +99 -0
- package/extension/services/vcs.ts +246 -0
- package/extension/workflows/commit.ts +148 -0
- package/extension/workflows/dispatch.ts +693 -0
- package/extension/workflows/reset.ts +26 -0
- package/extension/workflows/setup.ts +301 -0
- package/extension/workflows/start.ts +149 -0
- package/extension/workflows/status.ts +45 -0
- package/extension/workflows/wait.ts +793 -0
- package/herdr-plugin/herdr-plugin.toml +15 -0
- package/herdr-plugin/scripts/run-task.sh +8 -0
- package/package.json +75 -0
- package/schemas/artifact-frontmatter.md +38 -0
- package/schemas/config.schema.json +50 -0
- package/schemas/state.schema.json +63 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as fs from "node:fs"
|
|
2
|
+
import * as path from "node:path"
|
|
3
|
+
import { randomUUID } from "node:crypto"
|
|
4
|
+
import { Context, Effect, Layer } from "effect"
|
|
5
|
+
import { ConfigError } from "../errors.ts"
|
|
6
|
+
|
|
7
|
+
export interface FileSystemService {
|
|
8
|
+
readonly readFile: (path: string) => Effect.Effect<string>
|
|
9
|
+
readonly writeFile: (path: string, content: string) => Effect.Effect<void>
|
|
10
|
+
readonly writeProjectFile: (
|
|
11
|
+
root: string,
|
|
12
|
+
destination: string,
|
|
13
|
+
content: string,
|
|
14
|
+
) => Effect.Effect<void, ConfigError>
|
|
15
|
+
readonly rename: (from: string, to: string) => Effect.Effect<void>
|
|
16
|
+
readonly exists: (path: string) => Effect.Effect<boolean>
|
|
17
|
+
readonly mkdir: (
|
|
18
|
+
path: string,
|
|
19
|
+
opts?: { recursive?: boolean },
|
|
20
|
+
) => Effect.Effect<void>
|
|
21
|
+
readonly remove: (path: string) => Effect.Effect<void>
|
|
22
|
+
readonly copyDir: (from: string, to: string) => Effect.Effect<void>
|
|
23
|
+
readonly chmod: (path: string, mode: number) => Effect.Effect<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Thin FS service. Unexpected IO failures are defects (orDie) — no FsError tag.
|
|
28
|
+
*/
|
|
29
|
+
export class FileSystem extends Context.Service<
|
|
30
|
+
FileSystem,
|
|
31
|
+
FileSystemService
|
|
32
|
+
>()("apnea/FileSystem") {}
|
|
33
|
+
|
|
34
|
+
export const FileSystemLive = Layer.succeed(
|
|
35
|
+
FileSystem,
|
|
36
|
+
FileSystem.of({
|
|
37
|
+
readFile: (path) =>
|
|
38
|
+
Effect.try({
|
|
39
|
+
try: () => fs.readFileSync(path, "utf8"),
|
|
40
|
+
catch: (e) => e,
|
|
41
|
+
}).pipe(Effect.orDie),
|
|
42
|
+
|
|
43
|
+
writeFile: (path, content) =>
|
|
44
|
+
Effect.try({
|
|
45
|
+
try: () => {
|
|
46
|
+
fs.writeFileSync(path, content, "utf8")
|
|
47
|
+
},
|
|
48
|
+
catch: (e) => e,
|
|
49
|
+
}).pipe(Effect.orDie),
|
|
50
|
+
|
|
51
|
+
writeProjectFile: (root, destination, content) =>
|
|
52
|
+
Effect.try({
|
|
53
|
+
try: () => {
|
|
54
|
+
const projectRoot = path.resolve(root)
|
|
55
|
+
const target = path.resolve(destination)
|
|
56
|
+
const relative = path.relative(projectRoot, target)
|
|
57
|
+
if (
|
|
58
|
+
relative === "" ||
|
|
59
|
+
relative === ".." ||
|
|
60
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
61
|
+
path.isAbsolute(relative)
|
|
62
|
+
) {
|
|
63
|
+
throw new ConfigError({
|
|
64
|
+
message: "project file destination must be below project root",
|
|
65
|
+
path: target,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const components = relative.split(path.sep)
|
|
70
|
+
let current = projectRoot
|
|
71
|
+
for (const component of components.slice(0, -1)) {
|
|
72
|
+
current = path.join(current, component)
|
|
73
|
+
try {
|
|
74
|
+
const stat = fs.lstatSync(current)
|
|
75
|
+
if (stat.isSymbolicLink()) {
|
|
76
|
+
throw new ConfigError({
|
|
77
|
+
message: `refusing symlink below project root: ${current}`,
|
|
78
|
+
path: current,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
if (!stat.isDirectory()) {
|
|
82
|
+
throw new ConfigError({
|
|
83
|
+
message: `project path component is not a directory: ${current}`,
|
|
84
|
+
path: current,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
89
|
+
throw error
|
|
90
|
+
fs.mkdirSync(current)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
if (fs.lstatSync(target).isSymbolicLink()) {
|
|
96
|
+
throw new ConfigError({
|
|
97
|
+
message: `refusing symlink destination: ${target}`,
|
|
98
|
+
path: target,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const temporary = path.join(
|
|
106
|
+
path.dirname(target),
|
|
107
|
+
`.${path.basename(target)}.${randomUUID()}.tmp`,
|
|
108
|
+
)
|
|
109
|
+
let descriptor: number | undefined
|
|
110
|
+
try {
|
|
111
|
+
descriptor = fs.openSync(
|
|
112
|
+
temporary,
|
|
113
|
+
fs.constants.O_WRONLY |
|
|
114
|
+
fs.constants.O_CREAT |
|
|
115
|
+
fs.constants.O_EXCL |
|
|
116
|
+
fs.constants.O_NOFOLLOW,
|
|
117
|
+
0o600,
|
|
118
|
+
)
|
|
119
|
+
fs.writeFileSync(descriptor, content, "utf8")
|
|
120
|
+
fs.fsyncSync(descriptor)
|
|
121
|
+
fs.closeSync(descriptor)
|
|
122
|
+
descriptor = undefined
|
|
123
|
+
fs.renameSync(temporary, target)
|
|
124
|
+
} finally {
|
|
125
|
+
if (descriptor !== undefined) fs.closeSync(descriptor)
|
|
126
|
+
fs.rmSync(temporary, { force: true })
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
catch: (e) => e,
|
|
130
|
+
}).pipe(
|
|
131
|
+
Effect.catch((error) =>
|
|
132
|
+
error instanceof ConfigError ? Effect.fail(error) : Effect.die(error),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
|
|
136
|
+
rename: (from, to) =>
|
|
137
|
+
Effect.try({
|
|
138
|
+
try: () => {
|
|
139
|
+
fs.renameSync(from, to)
|
|
140
|
+
},
|
|
141
|
+
catch: (e) => e,
|
|
142
|
+
}).pipe(Effect.orDie),
|
|
143
|
+
|
|
144
|
+
exists: (path) => Effect.sync(() => fs.existsSync(path)),
|
|
145
|
+
|
|
146
|
+
mkdir: (path, opts) =>
|
|
147
|
+
Effect.try({
|
|
148
|
+
try: () => {
|
|
149
|
+
fs.mkdirSync(path, { recursive: opts?.recursive ?? true })
|
|
150
|
+
},
|
|
151
|
+
catch: (e) => e,
|
|
152
|
+
}).pipe(Effect.orDie),
|
|
153
|
+
|
|
154
|
+
remove: (path) =>
|
|
155
|
+
Effect.try({
|
|
156
|
+
try: () => {
|
|
157
|
+
fs.rmSync(path, { force: true })
|
|
158
|
+
},
|
|
159
|
+
catch: (e) => e,
|
|
160
|
+
}).pipe(Effect.orDie),
|
|
161
|
+
|
|
162
|
+
copyDir: (from, to) =>
|
|
163
|
+
Effect.try({
|
|
164
|
+
try: () => {
|
|
165
|
+
fs.cpSync(from, to, { recursive: true, force: true })
|
|
166
|
+
},
|
|
167
|
+
catch: (e) => e,
|
|
168
|
+
}).pipe(Effect.orDie),
|
|
169
|
+
|
|
170
|
+
chmod: (path, mode) =>
|
|
171
|
+
Effect.try({
|
|
172
|
+
try: () => {
|
|
173
|
+
fs.chmodSync(path, mode)
|
|
174
|
+
},
|
|
175
|
+
catch: (e) => e,
|
|
176
|
+
}).pipe(Effect.orDie),
|
|
177
|
+
}),
|
|
178
|
+
)
|