@nifrajs/coding-agent 4.0.2 → 5.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/approvals.d.ts +30 -2
  4. package/dist/approvals.d.ts.map +1 -1
  5. package/dist/approvals.js +43 -12
  6. package/dist/approvals.js.map +1 -1
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +120 -100
  10. package/dist/cli.js.map +1 -1
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +9 -21
  13. package/dist/context.js.map +1 -1
  14. package/dist/diff.d.ts.map +1 -1
  15. package/dist/diff.js +8 -21
  16. package/dist/diff.js.map +1 -1
  17. package/dist/extensions.js +4 -2
  18. package/dist/extensions.js.map +1 -1
  19. package/dist/handoffs.d.ts +5 -0
  20. package/dist/handoffs.d.ts.map +1 -1
  21. package/dist/handoffs.js +27 -0
  22. package/dist/handoffs.js.map +1 -1
  23. package/dist/host.d.ts +15 -1
  24. package/dist/host.d.ts.map +1 -1
  25. package/dist/host.js +152 -37
  26. package/dist/host.js.map +1 -1
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/isolated.js +3 -2
  32. package/dist/isolated.js.map +1 -1
  33. package/dist/native.d.ts +29 -2
  34. package/dist/native.d.ts.map +1 -1
  35. package/dist/native.js +86 -26
  36. package/dist/native.js.map +1 -1
  37. package/dist/orchestration/dispatch.d.ts +5 -1
  38. package/dist/orchestration/dispatch.d.ts.map +1 -1
  39. package/dist/orchestration/dispatch.js +1 -1
  40. package/dist/orchestration/dispatch.js.map +1 -1
  41. package/dist/orchestration/evidence-store.d.ts.map +1 -1
  42. package/dist/orchestration/evidence-store.js +5 -6
  43. package/dist/orchestration/evidence-store.js.map +1 -1
  44. package/dist/orchestration/host.d.ts +14 -0
  45. package/dist/orchestration/host.d.ts.map +1 -1
  46. package/dist/orchestration/host.js +26 -0
  47. package/dist/orchestration/host.js.map +1 -1
  48. package/dist/orchestration/index.d.ts +1 -1
  49. package/dist/orchestration/index.d.ts.map +1 -1
  50. package/dist/orchestration/index.js.map +1 -1
  51. package/dist/orchestration/recovery.d.ts +5 -1
  52. package/dist/orchestration/recovery.d.ts.map +1 -1
  53. package/dist/orchestration/recovery.js +5 -3
  54. package/dist/orchestration/recovery.js.map +1 -1
  55. package/dist/process.d.ts +22 -1
  56. package/dist/process.d.ts.map +1 -1
  57. package/dist/process.js +75 -1
  58. package/dist/process.js.map +1 -1
  59. package/dist/rpc.d.ts +29 -0
  60. package/dist/rpc.d.ts.map +1 -1
  61. package/dist/rpc.js +205 -46
  62. package/dist/rpc.js.map +1 -1
  63. package/dist/session-migration.d.ts.map +1 -1
  64. package/dist/session-migration.js +30 -6
  65. package/dist/session-migration.js.map +1 -1
  66. package/dist/sessions.d.ts +13 -2
  67. package/dist/sessions.d.ts.map +1 -1
  68. package/dist/sessions.js +104 -24
  69. package/dist/sessions.js.map +1 -1
  70. package/dist/subagents.d.ts.map +1 -1
  71. package/dist/subagents.js +13 -2
  72. package/dist/subagents.js.map +1 -1
  73. package/dist/turn-replay.d.ts +40 -0
  74. package/dist/turn-replay.d.ts.map +1 -0
  75. package/dist/turn-replay.js +136 -0
  76. package/dist/turn-replay.js.map +1 -0
  77. package/dist/ui.js +84 -30
  78. package/dist/ui.js.map +1 -1
  79. package/dist/verification.d.ts +7 -0
  80. package/dist/verification.d.ts.map +1 -1
  81. package/dist/verification.js +66 -63
  82. package/dist/verification.js.map +1 -1
  83. package/dist/workflows.d.ts +5 -0
  84. package/dist/workflows.d.ts.map +1 -1
  85. package/dist/workflows.js +2 -1
  86. package/dist/workflows.js.map +1 -1
  87. package/package.json +6 -6
  88. package/src/approvals.ts +70 -16
  89. package/src/cli.ts +125 -93
  90. package/src/context.ts +12 -20
  91. package/src/diff.ts +11 -23
  92. package/src/extensions.ts +6 -2
  93. package/src/handoffs.ts +27 -0
  94. package/src/host.ts +165 -34
  95. package/src/index.ts +3 -0
  96. package/src/isolated.ts +3 -2
  97. package/src/native.ts +137 -34
  98. package/src/orchestration/dispatch.ts +6 -2
  99. package/src/orchestration/evidence-store.ts +5 -6
  100. package/src/orchestration/host.ts +44 -0
  101. package/src/orchestration/index.ts +1 -0
  102. package/src/orchestration/recovery.ts +14 -8
  103. package/src/process.ts +89 -0
  104. package/src/rpc.ts +268 -49
  105. package/src/session-migration.ts +29 -5
  106. package/src/sessions.ts +115 -29
  107. package/src/subagents.ts +11 -2
  108. package/src/turn-replay.ts +151 -0
  109. package/src/ui.ts +90 -32
  110. package/src/verification.ts +74 -60
  111. package/src/workflows.ts +7 -1
@@ -1,5 +1,7 @@
1
+ import { accessSync, constants, statSync } from "node:fs"
2
+ import { isAbsolute, resolve } from "node:path"
1
3
  import { publicErrorDetails } from "./errors.ts"
2
- import { readBoundedText } from "./process.ts"
4
+ import { type BoundedProcessResult, runBoundedProcess } from "./process.ts"
3
5
 
4
6
  export interface VerificationResult {
5
7
  readonly name: "check" | "assure" | "test"
@@ -9,6 +11,8 @@ export interface VerificationResult {
9
11
  readonly output?: string
10
12
  readonly error?: string
11
13
  readonly stack?: string
14
+ /** The gate's command was not found or cannot run, so nothing was checked; never a repair task. */
15
+ readonly unavailable?: true
12
16
  }
13
17
 
14
18
  export interface VerificationRepairTask {
@@ -27,7 +31,7 @@ export function createVerificationRepairTask(
27
31
  result: VerificationResult,
28
32
  cwd: string,
29
33
  ): VerificationRepairTask | undefined {
30
- if (result.ok) return undefined
34
+ if (result.ok || result.unavailable === true) return undefined
31
35
  return Object.freeze({
32
36
  id: `verification-${result.name}-${Date.now().toString(36)}`,
33
37
  verification: result.name,
@@ -108,56 +112,35 @@ export async function runNifraReview(options: ReviewOptions): Promise<ReviewExec
108
112
  const args = ["review", "--json"]
109
113
  if (options.strict === true) args.push("--strict")
110
114
  if (options.diff !== undefined) args.push("--diff", options.diff)
111
- let proc: ReturnType<typeof Bun.spawn>
115
+ const maxOutputBytes = options.maxOutputBytes ?? 1_048_576
116
+ let run: BoundedProcessResult
112
117
  try {
113
- proc = Bun.spawn([command, ...commandArgs, ...args], {
118
+ run = await runBoundedProcess([command, ...commandArgs, ...args], {
114
119
  cwd: options.cwd,
115
- stdin: "ignore",
116
- stdout: "pipe",
117
- stderr: "pipe",
118
120
  env: safeEnv(options.env),
121
+ timeoutMs: options.timeoutMs ?? 120_000,
122
+ maxStdoutBytes: maxOutputBytes,
123
+ maxStderrBytes: maxOutputBytes,
119
124
  })
120
125
  } catch {
121
126
  return { name: "review", ok: false, status: null, errorCode: "spawn-failed" }
122
127
  }
123
- const timeoutMs = options.timeoutMs ?? 120_000
124
- let timedOut = false
125
- const timer = setTimeout(() => {
126
- timedOut = true
127
- proc.kill()
128
- }, timeoutMs)
128
+ const { stdout, stderr, status } = run
129
+ if (run.timedOut) return { name: "review", ok: false, status, errorCode: "timeout" }
130
+ if (stdout.truncated || stderr.truncated)
131
+ return { name: "review", ok: false, status, errorCode: "output-truncated" }
129
132
  try {
130
- const [stdout, stderr, status] = await Promise.all([
131
- readBoundedText(proc.stdout, options.maxOutputBytes ?? 1_048_576),
132
- readBoundedText(proc.stderr, options.maxOutputBytes ?? 1_048_576),
133
- proc.exited,
134
- ])
135
- void stderr
136
- if (timedOut) return { name: "review", ok: false, status, errorCode: "timeout" }
137
- if (stdout.truncated || stderr.truncated)
138
- return { name: "review", ok: false, status, errorCode: "output-truncated" }
139
- try {
140
- const raw = JSON.parse(stdout.text) as unknown
141
- const { parseReviewReport } = await import("@nifrajs/agent-review")
142
- const report = await parseReviewReport(raw)
143
- return {
144
- name: "review",
145
- ok: status === 0 && report.ok,
146
- status,
147
- report,
148
- }
149
- } catch {
150
- return { name: "review", ok: false, status, errorCode: "invalid-report" }
151
- }
152
- } catch {
133
+ const raw = JSON.parse(stdout.text) as unknown
134
+ const { parseReviewReport } = await import("@nifrajs/agent-review")
135
+ const report = await parseReviewReport(raw)
153
136
  return {
154
137
  name: "review",
155
- ok: false,
156
- status: null,
157
- errorCode: timedOut ? "timeout" : "spawn-failed",
138
+ ok: status === 0 && report.ok,
139
+ status,
140
+ report,
158
141
  }
159
- } finally {
160
- clearTimeout(timer)
142
+ } catch {
143
+ return { name: "review", ok: false, status, errorCode: "invalid-report" }
161
144
  }
162
145
  }
163
146
 
@@ -173,25 +156,19 @@ export async function runNifraVerification(
173
156
  const command = options.command ?? (name === "test" ? process.execPath : "nifra")
174
157
  const commandArgs = options.commandArgs ?? []
175
158
  const args = name === "test" ? ["test"] : [name, "--json"]
176
- const proc = Bun.spawn([command, ...commandArgs, ...args], {
177
- cwd: options.cwd,
178
- stdin: "ignore",
179
- stdout: "pipe",
180
- stderr: "pipe",
181
- env: safeEnv(options.env),
182
- })
183
159
  const timeoutMs = options.timeoutMs ?? 120_000
184
- let timedOut = false
185
- const timer = setTimeout(() => {
186
- timedOut = true
187
- proc.kill()
188
- }, timeoutMs)
189
160
  try {
190
- const [stdout, stderr, status] = await Promise.all([
191
- readBoundedText(proc.stdout, options.maxOutputBytes ?? 1_048_576),
192
- readBoundedText(proc.stderr, options.maxOutputBytes ?? 1_048_576),
193
- proc.exited,
194
- ])
161
+ const maxOutputBytes = options.maxOutputBytes ?? 1_048_576
162
+ const { stdout, stderr, status, timedOut } = await runBoundedProcess(
163
+ [command, ...commandArgs, ...args],
164
+ {
165
+ cwd: options.cwd,
166
+ env: safeEnv(options.env),
167
+ timeoutMs,
168
+ maxStdoutBytes: maxOutputBytes,
169
+ maxStderrBytes: maxOutputBytes,
170
+ },
171
+ )
195
172
  const output = stdout.text || stderr.text
196
173
  let report: unknown
197
174
  try {
@@ -208,6 +185,14 @@ export async function runNifraVerification(
208
185
  ...(timedOut ? { error: `nifra ${name} timed out after ${timeoutMs}ms` } : {}),
209
186
  }
210
187
  } catch (error) {
188
+ if (commandMissing(error, options.cwd))
189
+ return {
190
+ name,
191
+ ok: false,
192
+ status: null,
193
+ unavailable: true,
194
+ error: `${command} is not installed or cannot run, so ${name} did not run`,
195
+ }
211
196
  const details = publicErrorDetails(
212
197
  error,
213
198
  "verification failed",
@@ -220,8 +205,37 @@ export async function runNifraVerification(
220
205
  error: details.message,
221
206
  ...(details.stack === undefined ? {} : { stack: details.stack }),
222
207
  }
223
- } finally {
224
- clearTimeout(timer)
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Whether a gate's command can be found and run from `cwd`, without running the gate. `test` runs
213
+ * on the Bun this agent runs on, so it is always available unless `command` replaces it.
214
+ */
215
+ export function isVerificationAvailable(
216
+ name: "check" | "assure" | "test",
217
+ options: Pick<VerificationOptions, "cwd" | "command" | "env">,
218
+ ): boolean {
219
+ const command = options.command ?? (name === "test" ? process.execPath : "nifra")
220
+ if (!command.includes("/") && !(process.platform === "win32" && command.includes("\\")))
221
+ return Bun.which(command, { PATH: safeEnv(options.env).PATH ?? "" }) !== null
222
+ try {
223
+ accessSync(isAbsolute(command) ? command : resolve(options.cwd, command), constants.X_OK)
224
+ return true
225
+ } catch {
226
+ return false
227
+ }
228
+ }
229
+
230
+ // Bun reports a missing executable and a missing cwd alike (ENOENT), so the cwd decides which.
231
+ function commandMissing(error: unknown, cwd: string): boolean {
232
+ const code =
233
+ typeof error === "object" && error !== null && "code" in error ? error.code : undefined
234
+ if (code !== "ENOENT" && code !== "EACCES") return false
235
+ try {
236
+ return statSync(cwd).isDirectory()
237
+ } catch {
238
+ return false
225
239
  }
226
240
  }
227
241
 
package/src/workflows.ts CHANGED
@@ -58,6 +58,11 @@ export type WorkflowStep =
58
58
 
59
59
  export interface WorkflowRunnerOptions {
60
60
  readonly signal?: AbortSignal
61
+ /**
62
+ * The map steps read and write through `values` and `set`. Pass one map to several runners to
63
+ * carry values from one run into the next. Default: a fresh map per runner.
64
+ */
65
+ readonly values?: Map<string, unknown>
61
66
  readonly maxSteps?: number
62
67
  readonly maxDepth?: number
63
68
  readonly onEvent?: (event: WorkflowEvent) => void | PromiseLike<void>
@@ -89,7 +94,7 @@ export class WorkflowRunner {
89
94
  private readonly options: Required<Pick<WorkflowRunnerOptions, "maxSteps" | "maxDepth">> &
90
95
  WorkflowRunnerOptions
91
96
  private readonly signal: AbortSignal
92
- private readonly values = new Map<string, unknown>()
97
+ private readonly values: Map<string, unknown>
93
98
  private readonly completed: string[] = []
94
99
  private steps = 0
95
100
 
@@ -100,6 +105,7 @@ export class WorkflowRunner {
100
105
  maxDepth: options.maxDepth ?? 8,
101
106
  }
102
107
  this.signal = options.signal ?? new AbortController().signal
108
+ this.values = options.values ?? new Map()
103
109
  if (!Number.isSafeInteger(this.options.maxSteps) || this.options.maxSteps < 1)
104
110
  throw new RangeError("workflow: maxSteps must be positive")
105
111
  if (!Number.isSafeInteger(this.options.maxDepth) || this.options.maxDepth < 1)