@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.
Files changed (74) hide show
  1. package/CONTEXT.md +61 -0
  2. package/CONTRIBUTING.md +21 -0
  3. package/LICENSE +21 -0
  4. package/README.md +163 -0
  5. package/SECURITY.md +35 -0
  6. package/briefs/coder.md +40 -0
  7. package/briefs/orchestrator.md +49 -0
  8. package/briefs/planner.md +54 -0
  9. package/briefs/reviewer.md +40 -0
  10. package/dist/cli.js +39397 -0
  11. package/docs/adr/0001-completion-signaling.md +3 -0
  12. package/docs/adr/0002-orchestrator-authority.md +3 -0
  13. package/docs/adr/0003-verify-at-gate.md +3 -0
  14. package/docs/adr/0004-artifact-layout-and-naming.md +3 -0
  15. package/docs/adr/0005-harness-profiles.md +5 -0
  16. package/docs/adr/0006-config-trust-model.md +3 -0
  17. package/docs/adr/0007-jj-first-commits.md +3 -0
  18. package/docs/adr/0008-effect-v4-internals.md +3 -0
  19. package/docs/adr/0009-cli-driver-split.md +9 -0
  20. package/docs/adr/0010-package-split.md +23 -0
  21. package/docs/protocol/artifacts.md +68 -0
  22. package/docs/protocol/config.md +186 -0
  23. package/docs/protocol/manual-gate.md +38 -0
  24. package/docs/protocol/overview.md +96 -0
  25. package/extension/adapters/commit.ts +15 -0
  26. package/extension/adapters/dispatch.ts +15 -0
  27. package/extension/adapters/setup.ts +34 -0
  28. package/extension/adapters/start.ts +16 -0
  29. package/extension/adapters/status.ts +24 -0
  30. package/extension/adapters/wait.ts +20 -0
  31. package/extension/api.ts +16 -0
  32. package/extension/cli/format.ts +44 -0
  33. package/extension/cli/human-gate.ts +44 -0
  34. package/extension/cli/main.ts +218 -0
  35. package/extension/cli/parse.ts +48 -0
  36. package/extension/domain/artifact-kind.ts +26 -0
  37. package/extension/domain/frontmatter.ts +69 -0
  38. package/extension/domain/herdr.ts +109 -0
  39. package/extension/domain/paths.ts +139 -0
  40. package/extension/domain/recovery.ts +25 -0
  41. package/extension/domain/rounds.ts +16 -0
  42. package/extension/domain/setup.ts +158 -0
  43. package/extension/domain/slug.ts +9 -0
  44. package/extension/domain/state-machine.ts +132 -0
  45. package/extension/domain/timeouts.ts +24 -0
  46. package/extension/domain/types.ts +145 -0
  47. package/extension/domain/verify-commands.ts +128 -0
  48. package/extension/errors.ts +247 -0
  49. package/extension/host-adapter.ts +8 -0
  50. package/extension/registry.ts +323 -0
  51. package/extension/result.ts +55 -0
  52. package/extension/run-tool.ts +43 -0
  53. package/extension/schema/config.ts +315 -0
  54. package/extension/schema/frontmatter.ts +34 -0
  55. package/extension/schema/state.ts +119 -0
  56. package/extension/services/app-live.ts +24 -0
  57. package/extension/services/config.ts +103 -0
  58. package/extension/services/file-system.ts +178 -0
  59. package/extension/services/herdr.ts +860 -0
  60. package/extension/services/run-store.ts +99 -0
  61. package/extension/services/vcs.ts +246 -0
  62. package/extension/workflows/commit.ts +148 -0
  63. package/extension/workflows/dispatch.ts +693 -0
  64. package/extension/workflows/reset.ts +26 -0
  65. package/extension/workflows/setup.ts +301 -0
  66. package/extension/workflows/start.ts +149 -0
  67. package/extension/workflows/status.ts +45 -0
  68. package/extension/workflows/wait.ts +793 -0
  69. package/herdr-plugin/herdr-plugin.toml +15 -0
  70. package/herdr-plugin/scripts/run-task.sh +8 -0
  71. package/package.json +75 -0
  72. package/schemas/artifact-frontmatter.md +38 -0
  73. package/schemas/config.schema.json +50 -0
  74. 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
+ )