@llm4ts/runner 2.44.1 → 2.46.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/dist/Connectors.d.ts +1 -1
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +7 -2
- package/dist/Connectors.js.map +1 -1
- package/dist/Costs.d.ts +8 -4
- package/dist/Costs.d.ts.map +1 -1
- package/dist/Costs.js +32 -13
- package/dist/Costs.js.map +1 -1
- package/dist/Doctor.d.ts +24 -2
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +32 -12
- package/dist/Doctor.js.map +1 -1
- package/dist/FlowRunner.d.ts +38 -2
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +128 -13
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +3 -3
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/RunReaders.d.ts +69 -0
- package/dist/RunReaders.d.ts.map +1 -0
- package/dist/RunReaders.js +277 -0
- package/dist/RunReaders.js.map +1 -0
- package/dist/RunSummary.d.ts +3 -0
- package/dist/RunSummary.d.ts.map +1 -0
- package/dist/RunSummary.js +49 -0
- package/dist/RunSummary.js.map +1 -0
- package/dist/StoryReaders.d.ts +16 -0
- package/dist/StoryReaders.d.ts.map +1 -0
- package/dist/StoryReaders.js +107 -0
- package/dist/StoryReaders.js.map +1 -0
- package/dist/Terminal.d.ts +2 -0
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +4 -1
- package/dist/Terminal.js.map +1 -1
- package/dist/Watch.d.ts +2 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +2 -1
- package/dist/Watch.js.map +1 -1
- package/package.json +6 -3
- package/src/Connectors.ts +8 -2
- package/src/Costs.ts +44 -12
- package/src/Doctor.ts +62 -14
- package/src/FlowRunner.ts +177 -16
- package/src/NodeGeminiCliExecutor.ts +3 -3
- package/src/RunReaders.ts +413 -0
- package/src/RunSummary.ts +62 -0
- package/src/StoryReaders.ts +157 -0
- package/src/Terminal.ts +4 -1
- package/src/Watch.ts +2 -1
package/src/FlowRunner.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as Cause from "effect/Cause"
|
|
|
5
5
|
import * as Clock from "effect/Clock"
|
|
6
6
|
import * as Effect from "effect/Effect"
|
|
7
7
|
import * as Exit from "effect/Exit"
|
|
8
|
+
import * as Fiber from "effect/Fiber"
|
|
8
9
|
import * as Ref from "effect/Ref"
|
|
9
10
|
import type * as Scope from "effect/Scope"
|
|
10
11
|
import type { ConnectorConfig } from "@llm4ts/core/ConnectorConfig"
|
|
@@ -82,6 +83,7 @@ import { makeJudgmentLog, type JudgmentLogShape } from "@llm4ts/flow/JudgmentLog
|
|
|
82
83
|
import { makeGitHubTool } from "@llm4ts/flow/GitHubTool"
|
|
83
84
|
import { makeGitTool } from "@llm4ts/flow/GitTool"
|
|
84
85
|
import type { PlainFileStoreShape } from "@llm4ts/flow/Persistence"
|
|
86
|
+
import { ignoringRunState } from "@llm4ts/flow/RunState"
|
|
85
87
|
import { makeTransientRetry } from "@llm4ts/flow/TransientRetry"
|
|
86
88
|
import {
|
|
87
89
|
Exclusion,
|
|
@@ -147,6 +149,16 @@ export interface NodeConnectorDependencies {
|
|
|
147
149
|
readonly geminiCli: GeminiCliExecutorShape
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The dependencies with the run-state rule on their file store: the first
|
|
154
|
+
* write under any `.llm4ts/` leaves a `.gitignore` there, so a trace, a
|
|
155
|
+
* plan or a transcript is never staged by a flow commit (`RunState`).
|
|
156
|
+
*/
|
|
157
|
+
const withRunStateIgnore = (dependencies: FlowRunnerDependencies): FlowRunnerDependencies => ({
|
|
158
|
+
...dependencies,
|
|
159
|
+
files: ignoringRunState(dependencies.files)
|
|
160
|
+
})
|
|
161
|
+
|
|
150
162
|
export const nodeFlowRunnerDependencies = (): FlowRunnerDependencies => ({
|
|
151
163
|
process: nodeProcessExecutor,
|
|
152
164
|
files: nodePlainFileStore,
|
|
@@ -223,6 +235,14 @@ export interface FlowRunnerOptions {
|
|
|
223
235
|
* roster files); `"none"` runs with one executor per seat.
|
|
224
236
|
*/
|
|
225
237
|
readonly roster?: RosterDocument | "none"
|
|
238
|
+
/**
|
|
239
|
+
* Globs, relative to each seat's working directory, every CLI seat of
|
|
240
|
+
* the run is denied (`CliConnectorConfig.deniedPaths`). Default: the
|
|
241
|
+
* run's own state, `.llm4ts/**`. A flow whose coder writes there on
|
|
242
|
+
* purpose (port-guide's trial ports) passes `[]`; a seat that names its
|
|
243
|
+
* own list keeps it.
|
|
244
|
+
*/
|
|
245
|
+
readonly deniedPaths?: ReadonlyArray<string>
|
|
226
246
|
}
|
|
227
247
|
|
|
228
248
|
export interface FlowRunnerBundle {
|
|
@@ -329,12 +349,34 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
|
|
|
329
349
|
: config
|
|
330
350
|
})
|
|
331
351
|
|
|
352
|
+
const nonBlank = (value: string | undefined): string | undefined =>
|
|
353
|
+
value === undefined || value.trim() === "" ? undefined : value
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The run id and trace path a parent process (the MCP operator server) hands
|
|
357
|
+
* to a child run. Explicit options win; blank variables count as unset.
|
|
358
|
+
*/
|
|
359
|
+
export const runIdentityFromEnvironment = (
|
|
360
|
+
options: Pick<FlowRunnerOptions, "runId" | "tracePath">,
|
|
361
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
362
|
+
): { readonly runId?: string; readonly tracePath?: string } => {
|
|
363
|
+
const runId = nonBlank(options.runId) ?? nonBlank(environment["LLM4TS_RUN_ID"])
|
|
364
|
+
const tracePath = nonBlank(options.tracePath) ?? nonBlank(environment["LLM4TS_TRACE_PATH"])
|
|
365
|
+
return {
|
|
366
|
+
...(runId === undefined ? {} : { runId }),
|
|
367
|
+
...(tracePath === undefined ? {} : { tracePath })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
332
371
|
export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeContext")(function* (
|
|
333
372
|
options: FlowRunnerOptions,
|
|
334
|
-
|
|
373
|
+
given: FlowRunnerDependencies
|
|
335
374
|
): Effect.fn.Return<FlowRunnerBundle, FlowError, Scope.Scope> {
|
|
375
|
+
const dependencies = withRunStateIgnore(given)
|
|
336
376
|
const events = yield* makeFlowEventHub()
|
|
337
|
-
const runId =
|
|
377
|
+
const runId =
|
|
378
|
+
runIdentityFromEnvironment(options, options.environment ?? process.env).runId ??
|
|
379
|
+
`run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
338
380
|
// `llm4ts run --transcript`: what each seat is told and answers, per story,
|
|
339
381
|
// in its own files — never in the trace.
|
|
340
382
|
const transcriptOn =
|
|
@@ -391,6 +433,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
391
433
|
readonly readOnlyEnforcement: string
|
|
392
434
|
readonly effort: string
|
|
393
435
|
readonly isolatedHeadless: string
|
|
436
|
+
readonly pathDeny: string
|
|
394
437
|
}
|
|
395
438
|
}
|
|
396
439
|
): Effect.Effect<void> => {
|
|
@@ -428,6 +471,24 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
428
471
|
: "nothing reaches the harness")
|
|
429
472
|
)
|
|
430
473
|
}
|
|
474
|
+
// The run state (ADR 0027 amendment): a harness with no per-path deny
|
|
475
|
+
// rule keeps the model out of `.llm4ts/` by the contract text alone.
|
|
476
|
+
if (
|
|
477
|
+
configuration instanceof CliConnectorConfig &&
|
|
478
|
+
configuration.deniedPaths !== undefined &&
|
|
479
|
+
configuration.deniedPaths.length > 0 &&
|
|
480
|
+
connector.capabilities.pathDeny !== "enforced"
|
|
481
|
+
) {
|
|
482
|
+
const paths = configuration.deniedPaths.join(", ")
|
|
483
|
+
notes.push(
|
|
484
|
+
`'${name}' is asked to keep out of ${paths} (the run's own state: traces, plans, ` +
|
|
485
|
+
"transcripts), but its harness " +
|
|
486
|
+
(connector.capabilities.pathDeny === "partial"
|
|
487
|
+
? "denies it by rule, not at the filesystem — a shell command that spells the path " +
|
|
488
|
+
"another way can still reach it"
|
|
489
|
+
: "has no per-path deny rule — only the contract text in its prompt keeps it out")
|
|
490
|
+
)
|
|
491
|
+
}
|
|
431
492
|
return Effect.forEach(
|
|
432
493
|
notes,
|
|
433
494
|
(detail) => events.publish(FlowEventsValues.CapabilityUnenforceable(detail)),
|
|
@@ -545,12 +606,16 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
545
606
|
Scope.Scope
|
|
546
607
|
> {
|
|
547
608
|
const environment = options.environment ?? process.env
|
|
548
|
-
const coder = yield* resolveSeat(
|
|
609
|
+
const coder = yield* resolveSeat(
|
|
610
|
+
prepareConnector(bridged(options.coder), workDir, environment, options.deniedPaths)
|
|
611
|
+
)
|
|
549
612
|
const reasoningService = yield* resolveSeat(
|
|
550
|
-
prepareConnector(bridged(reasoning), workDir, environment)
|
|
613
|
+
prepareConnector(bridged(reasoning), workDir, environment, options.deniedPaths)
|
|
551
614
|
)
|
|
552
615
|
const reviewers = yield* Effect.forEach(options.reviewers ?? [], (configuration) =>
|
|
553
|
-
resolveSeat(
|
|
616
|
+
resolveSeat(
|
|
617
|
+
prepareConnector(bridged(configuration), workDir, environment, options.deniedPaths)
|
|
618
|
+
)
|
|
554
619
|
)
|
|
555
620
|
// The judgment seat is the reasoner with its tools taken away: a judge
|
|
556
621
|
// reads, it does not act. Over an API reasoner (no tools to take) it is
|
|
@@ -561,7 +626,9 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
561
626
|
const judgmentSeat =
|
|
562
627
|
judgmentConfig === undefined
|
|
563
628
|
? reasoningService
|
|
564
|
-
: yield* resolveSeat(
|
|
629
|
+
: yield* resolveSeat(
|
|
630
|
+
prepareConnector(bridged(judgmentConfig), workDir, environment, options.deniedPaths)
|
|
631
|
+
)
|
|
565
632
|
const judgment = yield* judgmentFor(judgmentSeat, judgmentConfig ?? reasoning, environment)
|
|
566
633
|
return { coder, reasoning: reasoningService, reviewers, judge: judgmentSeat, judgment }
|
|
567
634
|
})
|
|
@@ -720,7 +787,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
720
787
|
})
|
|
721
788
|
}
|
|
722
789
|
const seat: RosterSeat = yield* resolveSeat(
|
|
723
|
-
prepareConnector(bridged(configuration), workDir, runEnvironment)
|
|
790
|
+
prepareConnector(bridged(configuration), workDir, runEnvironment, options.deniedPaths)
|
|
724
791
|
).pipe(ScopeModule.provide(runScope))
|
|
725
792
|
seatCache.set(key, seat)
|
|
726
793
|
return seat
|
|
@@ -918,8 +985,9 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
918
985
|
bundle: FlowRunnerBundle,
|
|
919
986
|
options: FlowRunnerOptions,
|
|
920
987
|
body: (context: FlowContextShape) => Effect.Effect<A, E, R>,
|
|
921
|
-
|
|
988
|
+
given: FlowRunnerDependencies = nodeFlowRunnerDependencies()
|
|
922
989
|
): Effect.fn.Return<A, E | FlowError, R | Scope.Scope> {
|
|
990
|
+
const dependencies = withRunStateIgnore(given)
|
|
923
991
|
const startedAt = yield* Clock.currentTimeMillis
|
|
924
992
|
const environment = options.environment ?? process.env
|
|
925
993
|
const verbosity = options.verbosity ?? "Normal"
|
|
@@ -928,7 +996,7 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
928
996
|
// per run, flushed when the run's scope closes.
|
|
929
997
|
const otel = otelConfig(environment)
|
|
930
998
|
const tracePath =
|
|
931
|
-
options.tracePath ??
|
|
999
|
+
runIdentityFromEnvironment(options, environment).tracePath ??
|
|
932
1000
|
(persistRun ? join(options.workDir, ".llm4ts", `trace-${startedAt}.jsonl`) : undefined)
|
|
933
1001
|
const costLedgerPath =
|
|
934
1002
|
options.costLedgerPath ??
|
|
@@ -977,11 +1045,7 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
977
1045
|
: `executors ${bundle.roster.executors.map(describeExecutor).join(", ")} (roster in force: the flow's own seat choices are not used)`
|
|
978
1046
|
yield* surface.log(palette.info(`${seats} · ${options.workDir}`))
|
|
979
1047
|
if (tracePath !== undefined) {
|
|
980
|
-
yield* surface.log(
|
|
981
|
-
palette.info(
|
|
982
|
-
`trace ${tracePath}${options.runId === undefined ? "" : ` · run ${options.runId}`}`
|
|
983
|
-
)
|
|
984
|
-
)
|
|
1048
|
+
yield* surface.log(palette.info(`trace ${tracePath} · run ${bundle.runId}`))
|
|
985
1049
|
}
|
|
986
1050
|
const where = otelSummary(otel)
|
|
987
1051
|
if (where !== undefined) {
|
|
@@ -1012,7 +1076,7 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1012
1076
|
const at = new Date(yield* Clock.currentTimeMillis).toISOString()
|
|
1013
1077
|
yield* checkCostBudget(
|
|
1014
1078
|
makeCostRecord({
|
|
1015
|
-
runId:
|
|
1079
|
+
runId: bundle.runId,
|
|
1016
1080
|
at,
|
|
1017
1081
|
repo: options.workDir,
|
|
1018
1082
|
prompt: options.userPrompt,
|
|
@@ -1110,6 +1174,22 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1110
1174
|
),
|
|
1111
1175
|
Effect.andThen(tree === undefined ? Effect.void : tree.close),
|
|
1112
1176
|
Effect.andThen(appendLedger),
|
|
1177
|
+
// A trace that lost lines says so here, once; the ledger line above
|
|
1178
|
+
// is complete, and `llm4ts costs` reads it for this run's remainder.
|
|
1179
|
+
Effect.andThen(
|
|
1180
|
+
recorder === undefined
|
|
1181
|
+
? Effect.void
|
|
1182
|
+
: Effect.flatMap(recorder.shortfall, (shortfall) =>
|
|
1183
|
+
shortfall === undefined
|
|
1184
|
+
? Effect.void
|
|
1185
|
+
: surface.log(
|
|
1186
|
+
palette.fail(
|
|
1187
|
+
`trace short: ${shortfall.lines} line${shortfall.lines === 1 ? "" : "s"} not recorded after "${shortfall.reason}" — ` +
|
|
1188
|
+
"the cost ledger line is complete, and llm4ts costs reads it for this run"
|
|
1189
|
+
)
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
),
|
|
1113
1193
|
Effect.andThen(surface.setStatus(undefined)),
|
|
1114
1194
|
Effect.andThen(tracker.summary),
|
|
1115
1195
|
Effect.flatMap((summary) => surface.log(`\n${summary}`))
|
|
@@ -1189,8 +1269,83 @@ export const runNode = <A, E, R>(
|
|
|
1189
1269
|
const mainErrorMessage = (error: unknown): string =>
|
|
1190
1270
|
error instanceof Error && error.message.length > 0 ? error.message : String(error)
|
|
1191
1271
|
|
|
1272
|
+
export type InterruptSignal = "SIGINT" | "SIGTERM"
|
|
1273
|
+
|
|
1274
|
+
export interface SignalSource {
|
|
1275
|
+
readonly once: (signal: InterruptSignal, handler: () => void) => void
|
|
1276
|
+
readonly off: (signal: InterruptSignal, handler: () => void) => void
|
|
1277
|
+
readonly exit: (code: number) => void
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const signalExitCode: Record<InterruptSignal, number> = { SIGINT: 130, SIGTERM: 143 }
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* The first of the given signals (SIGINT by default) interrupts the fiber
|
|
1284
|
+
* (finalizers run, the trace ends `interrupted`); any later one exits at
|
|
1285
|
+
* once, 130 for SIGINT and 143 for SIGTERM. The handlers re-arm themselves
|
|
1286
|
+
* after the first signal. Returns a disposer that removes whichever handlers
|
|
1287
|
+
* are armed.
|
|
1288
|
+
*/
|
|
1289
|
+
export const interruptOnSignal = <A, E>(
|
|
1290
|
+
fiber: Fiber.Fiber<A, E>,
|
|
1291
|
+
signals: SignalSource,
|
|
1292
|
+
signal: InterruptSignal | ReadonlyArray<InterruptSignal> = "SIGINT"
|
|
1293
|
+
): (() => void) => {
|
|
1294
|
+
const watched: ReadonlyArray<InterruptSignal> = typeof signal === "string" ? [signal] : signal
|
|
1295
|
+
const seconds = new Map<InterruptSignal, () => void>(
|
|
1296
|
+
watched.map((name) => [name, () => signals.exit(signalExitCode[name])])
|
|
1297
|
+
)
|
|
1298
|
+
const firsts = new Map<InterruptSignal, () => void>(
|
|
1299
|
+
watched.map((name) => [
|
|
1300
|
+
name,
|
|
1301
|
+
() => {
|
|
1302
|
+
for (const other of watched) {
|
|
1303
|
+
const otherFirst = firsts.get(other)
|
|
1304
|
+
if (otherFirst !== undefined) signals.off(other, otherFirst)
|
|
1305
|
+
const second = seconds.get(other)
|
|
1306
|
+
if (second !== undefined) signals.once(other, second)
|
|
1307
|
+
}
|
|
1308
|
+
Effect.runFork(Fiber.interrupt(fiber))
|
|
1309
|
+
}
|
|
1310
|
+
])
|
|
1311
|
+
)
|
|
1312
|
+
const dispose = (): void => {
|
|
1313
|
+
for (const name of watched) {
|
|
1314
|
+
const first = firsts.get(name)
|
|
1315
|
+
if (first !== undefined) signals.off(name, first)
|
|
1316
|
+
const second = seconds.get(name)
|
|
1317
|
+
if (second !== undefined) signals.off(name, second)
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
for (const [name, first] of firsts) signals.once(name, first)
|
|
1321
|
+
// After an interrupt the handlers stay: a second signal must still kill a
|
|
1322
|
+
// run whose finalizers hang. Otherwise they are removed when the fiber ends.
|
|
1323
|
+
fiber.addObserver((exit) => {
|
|
1324
|
+
if (!(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))) {
|
|
1325
|
+
dispose()
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1328
|
+
return dispose
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* The signals `runFlowMain` stops on. SIGTERM counts like SIGINT, so a
|
|
1333
|
+
* supervisor's or init system's stop ends the trace `interrupted` too.
|
|
1334
|
+
*/
|
|
1335
|
+
export const flowMainSignals: ReadonlyArray<InterruptSignal> = ["SIGINT", "SIGTERM"]
|
|
1336
|
+
|
|
1337
|
+
const processSignals: SignalSource = {
|
|
1338
|
+
once: (signal, handler) => {
|
|
1339
|
+
process.once(signal, handler)
|
|
1340
|
+
},
|
|
1341
|
+
off: (signal, handler) => {
|
|
1342
|
+
process.off(signal, handler)
|
|
1343
|
+
},
|
|
1344
|
+
exit: (code) => process.exit(code)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1192
1347
|
export const runFlowMain = <E>(program: Effect.Effect<void, E>): void => {
|
|
1193
|
-
Effect.runFork(
|
|
1348
|
+
const fiber = Effect.runFork(
|
|
1194
1349
|
program.pipe(
|
|
1195
1350
|
Effect.match({
|
|
1196
1351
|
onFailure: (error) =>
|
|
@@ -1203,4 +1358,10 @@ export const runFlowMain = <E>(program: Effect.Effect<void, E>): void => {
|
|
|
1203
1358
|
Effect.flatten
|
|
1204
1359
|
)
|
|
1205
1360
|
)
|
|
1361
|
+
fiber.addObserver((exit) => {
|
|
1362
|
+
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
|
|
1363
|
+
process.exitCode = 130
|
|
1364
|
+
}
|
|
1365
|
+
})
|
|
1366
|
+
interruptOnSignal(fiber, processSignals, flowMainSignals)
|
|
1206
1367
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
GeminiCliExecutor,
|
|
9
9
|
buildGeminiArgs,
|
|
10
10
|
extractGeminiCliResponse,
|
|
11
|
-
|
|
11
|
+
geminiHarnessPolicyFor,
|
|
12
12
|
geminiProcessEnv,
|
|
13
13
|
geminiSupportsAdminPolicy,
|
|
14
14
|
geminiTurnLimitSettingsJson,
|
|
@@ -39,7 +39,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
39
39
|
),
|
|
40
40
|
(path) => geminiProcessEnv(context, path)
|
|
41
41
|
)
|
|
42
|
-
// The harness policy (`
|
|
42
|
+
// The harness policy (`geminiHarnessPolicyFor`) goes in with
|
|
43
43
|
// `--admin-policy` on a CLI that lists the flag: `gemini --help` is asked
|
|
44
44
|
// once, and an older CLI simply runs without it — an unknown flag would
|
|
45
45
|
// fail every turn.
|
|
@@ -68,7 +68,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
68
68
|
Effect.flatMap(supportsAdminPolicy, (supported) =>
|
|
69
69
|
supported
|
|
70
70
|
? Effect.map(
|
|
71
|
-
temporaryFiles.write("gemini-policy-", ".toml",
|
|
71
|
+
temporaryFiles.write("gemini-policy-", ".toml", geminiHarnessPolicyFor(context)),
|
|
72
72
|
(path) => buildGeminiArgs(config, context, outputFormat, path)
|
|
73
73
|
)
|
|
74
74
|
: Effect.succeed(buildGeminiArgs(config, context, outputFormat))
|