@gr8ful/spf 0.9.2 → 0.10.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/README.md +56 -0
- package/assets/defaults/spf.config.yaml +75 -0
- package/assets/skill/references/config.md +98 -4
- package/dist/chains/index.d.ts +2 -0
- package/dist/chains/index.js +4 -0
- package/dist/cli/commands/doctor.js +339 -2
- package/dist/cli/commands/fanout.d.ts +7 -14
- package/dist/cli/commands/fanout.js +45 -39
- package/dist/cli/commands/loop.d.ts +2 -0
- package/dist/cli/commands/loop.js +198 -0
- package/dist/cli/commands/run.js +14 -4
- package/dist/cli/commands/watch.d.ts +29 -1
- package/dist/cli/commands/watch.js +219 -64
- package/dist/cli/index.js +14 -0
- package/dist/core/agent_cc.d.ts +11 -0
- package/dist/core/agent_cc.js +25 -2
- package/dist/core/agent_flue.js +14 -5
- package/dist/core/agents.d.ts +61 -1
- package/dist/core/agents.js +363 -6
- package/dist/core/data_types.d.ts +316 -0
- package/dist/core/data_types.js +143 -0
- package/dist/core/loop.d.ts +230 -0
- package/dist/core/loop.js +290 -0
- package/dist/core/quality.d.ts +1 -2
- package/dist/core/sandbox.d.ts +236 -0
- package/dist/core/sandbox.js +655 -0
- package/dist/core/sandbox_cloudflare.d.ts +137 -0
- package/dist/core/sandbox_cloudflare.js +505 -0
- package/dist/core/sandbox_opensandbox.d.ts +59 -0
- package/dist/core/sandbox_opensandbox.js +484 -0
- package/dist/core/sandbox_sdk_types.d.ts +171 -0
- package/dist/core/sandbox_sdk_types.js +20 -0
- package/dist/core/watch.d.ts +56 -0
- package/dist/core/watch.js +354 -51
- package/dist/core/worktree_data.d.ts +1 -0
- package/dist/core/worktree_data.js +37 -0
- package/package.json +1 -1
|
@@ -408,6 +408,173 @@ export declare const PromptEngineeringSchema: v.ObjectSchema<{
|
|
|
408
408
|
export type PromptEngineering = v.InferOutput<typeof PromptEngineeringSchema>;
|
|
409
409
|
export declare const ThinkingLevelSchema: v.PicklistSchema<["off", "minimal", "low", "medium", "high", "xhigh", "max"], undefined>;
|
|
410
410
|
export type ThinkingLevel = v.InferOutput<typeof ThinkingLevelSchema>;
|
|
411
|
+
export declare const SandboxBackendSchema: v.PicklistSchema<["local", "opensandbox", "cloudflare"], undefined>;
|
|
412
|
+
export type SandboxBackend = v.InferOutput<typeof SandboxBackendSchema>;
|
|
413
|
+
/**
|
|
414
|
+
* "agent" (the DEFAULT) — one sandbox per (Run, agent), so a container's
|
|
415
|
+
* process env (fixed at create time) is built from exactly one agent's
|
|
416
|
+
* allowlist. "run" — one sandbox for the whole Run; admitted only when
|
|
417
|
+
* every required agent resolves to an IDENTICAL env spec, enforced by
|
|
418
|
+
* `agents.ts`'s `validateSandboxRunScope`.
|
|
419
|
+
*/
|
|
420
|
+
export declare const SandboxScopeSchema: v.PicklistSchema<["run", "agent"], undefined>;
|
|
421
|
+
export type SandboxScope = v.InferOutput<typeof SandboxScopeSchema>;
|
|
422
|
+
export declare const SandboxEgressSchema: v.ObjectSchema<{
|
|
423
|
+
readonly default: v.OptionalSchema<v.PicklistSchema<["deny", "allow"], undefined>, "deny">;
|
|
424
|
+
readonly allow: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
425
|
+
}, undefined>;
|
|
426
|
+
export type SandboxEgressConfig = v.InferOutput<typeof SandboxEgressSchema>;
|
|
427
|
+
/**
|
|
428
|
+
* `max_mirror_bytes` caps the handoff mirror (§5.3 point 2), EACH direction,
|
|
429
|
+
* per file. `max_patch_bytes`/`max_seed_bytes` cap the code-plane transport
|
|
430
|
+
* (§5.2). Exceeding any of them FAILS the phase; none of them ever truncate.
|
|
431
|
+
*/
|
|
432
|
+
export declare const SandboxTransportSchema: v.ObjectSchema<{
|
|
433
|
+
readonly max_patch_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 8388608>;
|
|
434
|
+
readonly max_seed_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 134217728>;
|
|
435
|
+
readonly max_mirror_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 4194304>;
|
|
436
|
+
readonly mirror: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
437
|
+
}, undefined>;
|
|
438
|
+
export type SandboxTransportConfig = v.InferOutput<typeof SandboxTransportSchema>;
|
|
439
|
+
/**
|
|
440
|
+
* A plain string, not a picklist — a future broker (design §6.3, e.g.
|
|
441
|
+
* `openrouter`) registers itself in code (`sandbox.ts`'s
|
|
442
|
+
* `KNOWN_CREDENTIAL_BROKER_IDS`), so accepting new ids here must not also
|
|
443
|
+
* require a schema change in lockstep. "static" is the only broker
|
|
444
|
+
* REGISTERED in this build (PR A ships none at all — see sandbox.ts); an
|
|
445
|
+
* unknown name is a `validateSandboxConfig` failure (agents.ts), never a
|
|
446
|
+
* silent fallback to "static" — see design §6.1.
|
|
447
|
+
*/
|
|
448
|
+
export declare const SandboxCredentialsSchema: v.ObjectSchema<{
|
|
449
|
+
readonly broker: v.OptionalSchema<v.StringSchema<undefined>, "static">;
|
|
450
|
+
}, undefined>;
|
|
451
|
+
export type SandboxCredentialsConfig = v.InferOutput<typeof SandboxCredentialsSchema>;
|
|
452
|
+
export declare const SandboxOpenSandboxSchema: v.ObjectSchema<{
|
|
453
|
+
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "http://127.0.0.1:8090">;
|
|
454
|
+
readonly api_key_env: v.OptionalSchema<v.StringSchema<undefined>, "OPENSANDBOX_API_KEY">;
|
|
455
|
+
readonly use_server_proxy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
456
|
+
readonly metadata_prefix: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
457
|
+
}, undefined>;
|
|
458
|
+
export type SandboxOpenSandboxConfig = v.InferOutput<typeof SandboxOpenSandboxSchema>;
|
|
459
|
+
export declare const SandboxCloudflareSchema: v.ObjectSchema<{
|
|
460
|
+
readonly bridge_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
461
|
+
readonly api_token_env: v.OptionalSchema<v.StringSchema<undefined>, "CLOUDFLARE_API_TOKEN">;
|
|
462
|
+
readonly sandbox_name: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
463
|
+
}, undefined>;
|
|
464
|
+
export type SandboxCloudflareConfig = v.InferOutput<typeof SandboxCloudflareSchema>;
|
|
465
|
+
export declare const SandboxConfigSchema: v.ObjectSchema<{
|
|
466
|
+
readonly backend: v.OptionalSchema<v.PicklistSchema<["local", "opensandbox", "cloudflare"], undefined>, "local">;
|
|
467
|
+
readonly scope: v.OptionalSchema<v.PicklistSchema<["run", "agent"], undefined>, "agent">;
|
|
468
|
+
readonly workspace_dir: v.OptionalSchema<v.StringSchema<undefined>, "/workspace">;
|
|
469
|
+
readonly handoff_dir: v.OptionalSchema<v.StringSchema<undefined>, "/spf/handoff">;
|
|
470
|
+
readonly scratch_dir: v.OptionalSchema<v.StringSchema<undefined>, "/spf/tmp">;
|
|
471
|
+
readonly image: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
472
|
+
readonly setup: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
473
|
+
readonly request_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 960>;
|
|
474
|
+
readonly lifetime_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 3600>;
|
|
475
|
+
readonly max_total_lifetime_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 21600>;
|
|
476
|
+
readonly exec_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 900>;
|
|
477
|
+
readonly env_allowlist: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
478
|
+
readonly credentials: v.OptionalSchema<v.ObjectSchema<{
|
|
479
|
+
readonly broker: v.OptionalSchema<v.StringSchema<undefined>, "static">;
|
|
480
|
+
}, undefined>, () => {
|
|
481
|
+
broker: string;
|
|
482
|
+
}>;
|
|
483
|
+
readonly egress: v.OptionalSchema<v.ObjectSchema<{
|
|
484
|
+
readonly default: v.OptionalSchema<v.PicklistSchema<["deny", "allow"], undefined>, "deny">;
|
|
485
|
+
readonly allow: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
486
|
+
}, undefined>, () => {
|
|
487
|
+
default: "allow" | "deny";
|
|
488
|
+
allow: string[];
|
|
489
|
+
}>;
|
|
490
|
+
readonly transport: v.OptionalSchema<v.ObjectSchema<{
|
|
491
|
+
readonly max_patch_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 8388608>;
|
|
492
|
+
readonly max_seed_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 134217728>;
|
|
493
|
+
readonly max_mirror_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 4194304>;
|
|
494
|
+
readonly mirror: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
495
|
+
}, undefined>, () => {
|
|
496
|
+
max_patch_bytes: number;
|
|
497
|
+
max_seed_bytes: number;
|
|
498
|
+
max_mirror_bytes: number;
|
|
499
|
+
mirror: string[];
|
|
500
|
+
}>;
|
|
501
|
+
readonly fanout: v.OptionalSchema<v.PicklistSchema<["worktree", "sandbox"], undefined>, "worktree">;
|
|
502
|
+
readonly opensandbox: v.OptionalSchema<v.ObjectSchema<{
|
|
503
|
+
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "http://127.0.0.1:8090">;
|
|
504
|
+
readonly api_key_env: v.OptionalSchema<v.StringSchema<undefined>, "OPENSANDBOX_API_KEY">;
|
|
505
|
+
readonly use_server_proxy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
506
|
+
readonly metadata_prefix: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
507
|
+
}, undefined>, () => {
|
|
508
|
+
base_url: string;
|
|
509
|
+
api_key_env: string;
|
|
510
|
+
use_server_proxy: boolean;
|
|
511
|
+
metadata_prefix: string;
|
|
512
|
+
}>;
|
|
513
|
+
readonly cloudflare: v.OptionalSchema<v.ObjectSchema<{
|
|
514
|
+
readonly bridge_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
515
|
+
readonly api_token_env: v.OptionalSchema<v.StringSchema<undefined>, "CLOUDFLARE_API_TOKEN">;
|
|
516
|
+
readonly sandbox_name: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
517
|
+
}, undefined>, () => {
|
|
518
|
+
bridge_url: string;
|
|
519
|
+
api_token_env: string;
|
|
520
|
+
sandbox_name: string;
|
|
521
|
+
}>;
|
|
522
|
+
}, undefined>;
|
|
523
|
+
export type SandboxConfig = v.InferOutput<typeof SandboxConfigSchema>;
|
|
524
|
+
/**
|
|
525
|
+
* Pure, sync, derived from config + the run — see `agents.ts`'s
|
|
526
|
+
* `sandboxSpecFor`. Carried on `AgentRequest.sandbox` below. Lives HERE
|
|
527
|
+
* (not in `core/sandbox.ts`) so this leaf module keeps importing nothing
|
|
528
|
+
* but valibot, while fourteen-plus other core modules import it — see
|
|
529
|
+
* `core/sandbox.ts`'s module comment for the full placement rationale
|
|
530
|
+
* (SPF #15 design doc §4.3's placement table).
|
|
531
|
+
*/
|
|
532
|
+
export interface SandboxSpec {
|
|
533
|
+
backend: SandboxBackend;
|
|
534
|
+
/** Identity available at spec-construction time — BEFORE agentSessionId(). */
|
|
535
|
+
adw_id: string;
|
|
536
|
+
agent: string;
|
|
537
|
+
/**
|
|
538
|
+
* Lease identity. `<adw_id>/<agent>` when scope=agent (the DEFAULT),
|
|
539
|
+
* `<adw_id>` when scope=run. INVARIANT: the key is at least as
|
|
540
|
+
* fine-grained as `env` below, because a container's env is fixed at
|
|
541
|
+
* create time and a cache hit never re-creates.
|
|
542
|
+
*/
|
|
543
|
+
lease_key: string;
|
|
544
|
+
scope: SandboxScope;
|
|
545
|
+
/** Host side: the tree that is the record of truth. run.repo_root. */
|
|
546
|
+
host_root: string;
|
|
547
|
+
/** Sandbox side: workspace_dir. Equals host_root when backend === "local" (never constructed then — see sandboxSpecFor). */
|
|
548
|
+
workspace_dir: string;
|
|
549
|
+
/**
|
|
550
|
+
* Host + sandbox paths for the handoff plane. INVARIANT (enforced by
|
|
551
|
+
* validateSandboxConfig): handoff_sandbox is NOT under workspace_dir.
|
|
552
|
+
* SHAPE INVARIANT: handoff_sandbox is
|
|
553
|
+
* `<handoff_dir>/sessions/<adw_id>/context_handoff`, mirroring
|
|
554
|
+
* handoff_host = `<data_dir>/sessions/<adw_id>/context_handoff`
|
|
555
|
+
* (`runner.ts`) — two shipped prompts parse `<adw_id>` out of this shape.
|
|
556
|
+
*/
|
|
557
|
+
handoff_host: string;
|
|
558
|
+
handoff_sandbox: string;
|
|
559
|
+
/**
|
|
560
|
+
* The TRANSPORT'S OWN scratch plane inside the sandbox: seed.tar,
|
|
561
|
+
* seed.patch[.b64], out.patch[.b64]. SAME INVARIANT, same enforcement,
|
|
562
|
+
* same reason as handoff_sandbox: NOT under workspace_dir.
|
|
563
|
+
*/
|
|
564
|
+
scratch_dir: string;
|
|
565
|
+
/** THIS AGENT's resolved keys: sandbox.env_allowlist ∩ (agent.env_allowlist ?? all). Already filtered; may be {}. */
|
|
566
|
+
env: Record<string, string>;
|
|
567
|
+
image: string;
|
|
568
|
+
setup: string[];
|
|
569
|
+
egress: SandboxEgressConfig;
|
|
570
|
+
lifetime_seconds: number;
|
|
571
|
+
max_total_lifetime_seconds: number;
|
|
572
|
+
request_timeout_seconds: number;
|
|
573
|
+
exec_timeout_seconds: number;
|
|
574
|
+
transport: SandboxTransportConfig;
|
|
575
|
+
opensandbox: SandboxOpenSandboxConfig;
|
|
576
|
+
cloudflare: SandboxCloudflareConfig;
|
|
577
|
+
}
|
|
411
578
|
export declare const AgentConfigSchema: v.ObjectSchema<{
|
|
412
579
|
readonly name: v.StringSchema<undefined>;
|
|
413
580
|
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
@@ -423,6 +590,7 @@ export declare const AgentConfigSchema: v.ObjectSchema<{
|
|
|
423
590
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
424
591
|
readonly writes: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
425
592
|
readonly env_allowlist: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
593
|
+
readonly sandbox: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<["local", "opensandbox", "cloudflare"], undefined>, undefined>, undefined>;
|
|
426
594
|
}, undefined>;
|
|
427
595
|
export type AgentConfig = v.InferOutput<typeof AgentConfigSchema>;
|
|
428
596
|
export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
@@ -612,6 +780,37 @@ export declare const WatchRefineConfigSchema: v.ObjectSchema<{
|
|
|
612
780
|
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 1>;
|
|
613
781
|
}, undefined>;
|
|
614
782
|
export type WatchRefineConfig = v.InferOutput<typeof WatchRefineConfigSchema>;
|
|
783
|
+
/**
|
|
784
|
+
* Best-of-N per claimed issue — `core/fanout.ts`'s proven machinery pointed at
|
|
785
|
+
* watch's per-issue dispatch. `n: 1` (the DEFAULT) leaves the daemon's
|
|
786
|
+
* behaviour untouched: `core/watch.ts`'s `runIssue` takes its existing
|
|
787
|
+
* single-dispatch path, same adw_id (`issue-<id>`), same one worktree, same
|
|
788
|
+
* everything. (`spf doctor` gains one informational line either way — see
|
|
789
|
+
* that command; nothing about a running daemon changes.)
|
|
790
|
+
*
|
|
791
|
+
* `concurrency` is DELIBERATELY NOT `watch.concurrency`, which counts ISSUES in
|
|
792
|
+
* flight (`core/watch.ts`'s `state.inflight`, a set of issue ids). This one
|
|
793
|
+
* counts ATTEMPTS in flight WITHIN one issue. The two multiply — see
|
|
794
|
+
* `spf doctor`'s `watch.fanout budget` line, which prints the product rather
|
|
795
|
+
* than leaving an operator to discover it on an invoice. Same "each axis of
|
|
796
|
+
* parallelism gets its own named budget" rule `refine.concurrency` already
|
|
797
|
+
* follows (see `WatchRunState.refining`'s doc comment).
|
|
798
|
+
*
|
|
799
|
+
* NOTE that `concurrency` bounds attempts IN FLIGHT, not worktrees ON DISK: a
|
|
800
|
+
* successful attempt's tree is KEPT until every sibling has settled (it is a
|
|
801
|
+
* selection candidate), so peak disk is `n` trees per issue regardless of this
|
|
802
|
+
* number. Doctor prints both.
|
|
803
|
+
*
|
|
804
|
+
* The `n` ceiling is 8, the twin of `MAX_N` in `cli/commands/fanout.ts` — kept
|
|
805
|
+
* as a literal here rather than imported because this module imports nothing
|
|
806
|
+
* but valibot, on purpose (see `SandboxSpec`'s placement note). A daemon typing
|
|
807
|
+
* a bigger number is strictly worse than an interactive command doing so.
|
|
808
|
+
*/
|
|
809
|
+
export declare const WatchFanoutConfigSchema: v.ObjectSchema<{
|
|
810
|
+
readonly n: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 8, undefined>]>, 1>;
|
|
811
|
+
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
812
|
+
}, undefined>;
|
|
813
|
+
export type WatchFanoutConfig = v.InferOutput<typeof WatchFanoutConfigSchema>;
|
|
615
814
|
export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
616
815
|
readonly issue_provider: v.OptionalSchema<v.PicklistSchema<["github", "jira"], undefined>, "github">;
|
|
617
816
|
readonly code_host: v.OptionalSchema<v.PicklistSchema<["github", "bitbucket"], undefined>, "github">;
|
|
@@ -674,6 +873,13 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
674
873
|
chain: string;
|
|
675
874
|
concurrency: number;
|
|
676
875
|
}>;
|
|
876
|
+
readonly fanout: v.OptionalSchema<v.ObjectSchema<{
|
|
877
|
+
readonly n: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 8, undefined>]>, 1>;
|
|
878
|
+
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
879
|
+
}, undefined>, () => {
|
|
880
|
+
n: number;
|
|
881
|
+
concurrency: number;
|
|
882
|
+
}>;
|
|
677
883
|
}, undefined>;
|
|
678
884
|
export type WatchConfig = v.InferOutput<typeof WatchConfigSchema>;
|
|
679
885
|
/**
|
|
@@ -904,6 +1110,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
904
1110
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
905
1111
|
readonly writes: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
906
1112
|
readonly env_allowlist: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
1113
|
+
readonly sandbox: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<["local", "opensandbox", "cloudflare"], undefined>, undefined>, undefined>;
|
|
907
1114
|
}, undefined>, undefined>, () => never[]>;
|
|
908
1115
|
readonly quality: v.OptionalSchema<v.ObjectSchema<{
|
|
909
1116
|
readonly checks: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
@@ -988,6 +1195,13 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
988
1195
|
chain: string;
|
|
989
1196
|
concurrency: number;
|
|
990
1197
|
}>;
|
|
1198
|
+
readonly fanout: v.OptionalSchema<v.ObjectSchema<{
|
|
1199
|
+
readonly n: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 8, undefined>]>, 1>;
|
|
1200
|
+
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
1201
|
+
}, undefined>, () => {
|
|
1202
|
+
n: number;
|
|
1203
|
+
concurrency: number;
|
|
1204
|
+
}>;
|
|
991
1205
|
}, undefined>, () => {
|
|
992
1206
|
issue_provider: "github" | "jira";
|
|
993
1207
|
code_host: "bitbucket" | "github";
|
|
@@ -1017,6 +1231,10 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1017
1231
|
chain: string;
|
|
1018
1232
|
concurrency: number;
|
|
1019
1233
|
};
|
|
1234
|
+
fanout: {
|
|
1235
|
+
n: number;
|
|
1236
|
+
concurrency: number;
|
|
1237
|
+
};
|
|
1020
1238
|
}>;
|
|
1021
1239
|
readonly notifications: v.OptionalSchema<v.ObjectSchema<{
|
|
1022
1240
|
readonly events: v.OptionalSchema<v.PicklistSchema<["off", "errors", "attention", "all"], undefined>, "off">;
|
|
@@ -1063,6 +1281,102 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1063
1281
|
[x: string]: string;
|
|
1064
1282
|
};
|
|
1065
1283
|
}>;
|
|
1284
|
+
readonly sandbox: v.OptionalSchema<v.ObjectSchema<{
|
|
1285
|
+
readonly backend: v.OptionalSchema<v.PicklistSchema<["local", "opensandbox", "cloudflare"], undefined>, "local">;
|
|
1286
|
+
readonly scope: v.OptionalSchema<v.PicklistSchema<["run", "agent"], undefined>, "agent">;
|
|
1287
|
+
readonly workspace_dir: v.OptionalSchema<v.StringSchema<undefined>, "/workspace">;
|
|
1288
|
+
readonly handoff_dir: v.OptionalSchema<v.StringSchema<undefined>, "/spf/handoff">;
|
|
1289
|
+
readonly scratch_dir: v.OptionalSchema<v.StringSchema<undefined>, "/spf/tmp">;
|
|
1290
|
+
readonly image: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
1291
|
+
readonly setup: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
1292
|
+
readonly request_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 960>;
|
|
1293
|
+
readonly lifetime_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 3600>;
|
|
1294
|
+
readonly max_total_lifetime_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 21600>;
|
|
1295
|
+
readonly exec_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 900>;
|
|
1296
|
+
readonly env_allowlist: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
1297
|
+
readonly credentials: v.OptionalSchema<v.ObjectSchema<{
|
|
1298
|
+
readonly broker: v.OptionalSchema<v.StringSchema<undefined>, "static">;
|
|
1299
|
+
}, undefined>, () => {
|
|
1300
|
+
broker: string;
|
|
1301
|
+
}>;
|
|
1302
|
+
readonly egress: v.OptionalSchema<v.ObjectSchema<{
|
|
1303
|
+
readonly default: v.OptionalSchema<v.PicklistSchema<["deny", "allow"], undefined>, "deny">;
|
|
1304
|
+
readonly allow: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
1305
|
+
}, undefined>, () => {
|
|
1306
|
+
default: "allow" | "deny";
|
|
1307
|
+
allow: string[];
|
|
1308
|
+
}>;
|
|
1309
|
+
readonly transport: v.OptionalSchema<v.ObjectSchema<{
|
|
1310
|
+
readonly max_patch_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 8388608>;
|
|
1311
|
+
readonly max_seed_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 134217728>;
|
|
1312
|
+
readonly max_mirror_bytes: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 4194304>;
|
|
1313
|
+
readonly mirror: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
1314
|
+
}, undefined>, () => {
|
|
1315
|
+
max_patch_bytes: number;
|
|
1316
|
+
max_seed_bytes: number;
|
|
1317
|
+
max_mirror_bytes: number;
|
|
1318
|
+
mirror: string[];
|
|
1319
|
+
}>;
|
|
1320
|
+
readonly fanout: v.OptionalSchema<v.PicklistSchema<["worktree", "sandbox"], undefined>, "worktree">;
|
|
1321
|
+
readonly opensandbox: v.OptionalSchema<v.ObjectSchema<{
|
|
1322
|
+
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "http://127.0.0.1:8090">;
|
|
1323
|
+
readonly api_key_env: v.OptionalSchema<v.StringSchema<undefined>, "OPENSANDBOX_API_KEY">;
|
|
1324
|
+
readonly use_server_proxy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
1325
|
+
readonly metadata_prefix: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
1326
|
+
}, undefined>, () => {
|
|
1327
|
+
base_url: string;
|
|
1328
|
+
api_key_env: string;
|
|
1329
|
+
use_server_proxy: boolean;
|
|
1330
|
+
metadata_prefix: string;
|
|
1331
|
+
}>;
|
|
1332
|
+
readonly cloudflare: v.OptionalSchema<v.ObjectSchema<{
|
|
1333
|
+
readonly bridge_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
1334
|
+
readonly api_token_env: v.OptionalSchema<v.StringSchema<undefined>, "CLOUDFLARE_API_TOKEN">;
|
|
1335
|
+
readonly sandbox_name: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
1336
|
+
}, undefined>, () => {
|
|
1337
|
+
bridge_url: string;
|
|
1338
|
+
api_token_env: string;
|
|
1339
|
+
sandbox_name: string;
|
|
1340
|
+
}>;
|
|
1341
|
+
}, undefined>, () => {
|
|
1342
|
+
backend: "cloudflare" | "local" | "opensandbox";
|
|
1343
|
+
scope: "agent" | "run";
|
|
1344
|
+
workspace_dir: string;
|
|
1345
|
+
handoff_dir: string;
|
|
1346
|
+
scratch_dir: string;
|
|
1347
|
+
image: string;
|
|
1348
|
+
setup: string[];
|
|
1349
|
+
request_timeout_seconds: number;
|
|
1350
|
+
lifetime_seconds: number;
|
|
1351
|
+
max_total_lifetime_seconds: number;
|
|
1352
|
+
exec_timeout_seconds: number;
|
|
1353
|
+
env_allowlist: string[];
|
|
1354
|
+
credentials: {
|
|
1355
|
+
broker: string;
|
|
1356
|
+
};
|
|
1357
|
+
egress: {
|
|
1358
|
+
default: "allow" | "deny";
|
|
1359
|
+
allow: string[];
|
|
1360
|
+
};
|
|
1361
|
+
transport: {
|
|
1362
|
+
max_patch_bytes: number;
|
|
1363
|
+
max_seed_bytes: number;
|
|
1364
|
+
max_mirror_bytes: number;
|
|
1365
|
+
mirror: string[];
|
|
1366
|
+
};
|
|
1367
|
+
fanout: "sandbox" | "worktree";
|
|
1368
|
+
opensandbox: {
|
|
1369
|
+
base_url: string;
|
|
1370
|
+
api_key_env: string;
|
|
1371
|
+
use_server_proxy: boolean;
|
|
1372
|
+
metadata_prefix: string;
|
|
1373
|
+
};
|
|
1374
|
+
cloudflare: {
|
|
1375
|
+
bridge_url: string;
|
|
1376
|
+
api_token_env: string;
|
|
1377
|
+
sandbox_name: string;
|
|
1378
|
+
};
|
|
1379
|
+
}>;
|
|
1066
1380
|
}, undefined>;
|
|
1067
1381
|
export type SFConfig = v.InferOutput<typeof SFConfigSchema>;
|
|
1068
1382
|
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|
|
@@ -1116,6 +1430,8 @@ export interface AgentRequest {
|
|
|
1116
1430
|
cwd: string;
|
|
1117
1431
|
flue_db_path: string;
|
|
1118
1432
|
env?: Record<string, string>;
|
|
1433
|
+
/** Absent (the default) => local(), byte-identical to before this field existed. See sandbox.ts. */
|
|
1434
|
+
sandbox?: SandboxSpec;
|
|
1119
1435
|
}
|
|
1120
1436
|
/**
|
|
1121
1437
|
* Tokens and the dollars they cost, per component, summed over a call.
|
package/dist/core/data_types.js
CHANGED
|
@@ -302,6 +302,110 @@ export const PromptEngineeringSchema = v.object({
|
|
|
302
302
|
user: v.string(), // path to user.md
|
|
303
303
|
});
|
|
304
304
|
export const ThinkingLevelSchema = v.picklist(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
305
|
+
// ── Sandbox (SPF #15 — remote sandbox backends) ─────────────────────────────
|
|
306
|
+
//
|
|
307
|
+
// Absent `sandbox:` block entirely => backend: "local", byte-identical to
|
|
308
|
+
// SPF before this feature existed — see agents.ts's `mergeRawConfig` (the
|
|
309
|
+
// `sandbox` line) and `SFConfigSchema` below, whose own `v.optional` default
|
|
310
|
+
// makes `cfg.sandbox` always defined even with no config file naming it. See
|
|
311
|
+
// `assets/skill/references/config.md`'s `### sandbox` section for the
|
|
312
|
+
// field-by-field rationale; this schema only encodes shape and defaults.
|
|
313
|
+
export const SandboxBackendSchema = v.picklist(["local", "opensandbox", "cloudflare"]);
|
|
314
|
+
/**
|
|
315
|
+
* "agent" (the DEFAULT) — one sandbox per (Run, agent), so a container's
|
|
316
|
+
* process env (fixed at create time) is built from exactly one agent's
|
|
317
|
+
* allowlist. "run" — one sandbox for the whole Run; admitted only when
|
|
318
|
+
* every required agent resolves to an IDENTICAL env spec, enforced by
|
|
319
|
+
* `agents.ts`'s `validateSandboxRunScope`.
|
|
320
|
+
*/
|
|
321
|
+
export const SandboxScopeSchema = v.picklist(["run", "agent"]);
|
|
322
|
+
export const SandboxEgressSchema = v.object({
|
|
323
|
+
default: v.optional(v.picklist(["deny", "allow"]), "deny"),
|
|
324
|
+
allow: v.optional(v.array(v.string()), () => []),
|
|
325
|
+
});
|
|
326
|
+
/**
|
|
327
|
+
* `max_mirror_bytes` caps the handoff mirror (§5.3 point 2), EACH direction,
|
|
328
|
+
* per file. `max_patch_bytes`/`max_seed_bytes` cap the code-plane transport
|
|
329
|
+
* (§5.2). Exceeding any of them FAILS the phase; none of them ever truncate.
|
|
330
|
+
*/
|
|
331
|
+
export const SandboxTransportSchema = v.object({
|
|
332
|
+
max_patch_bytes: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 8_388_608),
|
|
333
|
+
max_seed_bytes: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 134_217_728),
|
|
334
|
+
max_mirror_bytes: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 4_194_304),
|
|
335
|
+
// Extra repo-relative paths to sync out of the sandbox that git cannot
|
|
336
|
+
// carry (gitignored artifacts you want back).
|
|
337
|
+
mirror: v.optional(v.array(v.string()), () => []),
|
|
338
|
+
});
|
|
339
|
+
/**
|
|
340
|
+
* A plain string, not a picklist — a future broker (design §6.3, e.g.
|
|
341
|
+
* `openrouter`) registers itself in code (`sandbox.ts`'s
|
|
342
|
+
* `KNOWN_CREDENTIAL_BROKER_IDS`), so accepting new ids here must not also
|
|
343
|
+
* require a schema change in lockstep. "static" is the only broker
|
|
344
|
+
* REGISTERED in this build (PR A ships none at all — see sandbox.ts); an
|
|
345
|
+
* unknown name is a `validateSandboxConfig` failure (agents.ts), never a
|
|
346
|
+
* silent fallback to "static" — see design §6.1.
|
|
347
|
+
*/
|
|
348
|
+
export const SandboxCredentialsSchema = v.object({
|
|
349
|
+
broker: v.optional(v.string(), "static"),
|
|
350
|
+
});
|
|
351
|
+
export const SandboxOpenSandboxSchema = v.object({
|
|
352
|
+
base_url: v.optional(v.string(), "http://127.0.0.1:8090"),
|
|
353
|
+
// env KEY NAME, never the key itself — matches the GITHUB_TOKEN style.
|
|
354
|
+
api_key_env: v.optional(v.string(), "OPENSANDBOX_API_KEY"),
|
|
355
|
+
use_server_proxy: v.optional(v.boolean(), false),
|
|
356
|
+
metadata_prefix: v.optional(v.string(), "spf"),
|
|
357
|
+
});
|
|
358
|
+
export const SandboxCloudflareSchema = v.object({
|
|
359
|
+
bridge_url: v.optional(v.string(), ""), // a deployed @cloudflare/sandbox bridge
|
|
360
|
+
api_token_env: v.optional(v.string(), "CLOUDFLARE_API_TOKEN"),
|
|
361
|
+
sandbox_name: v.optional(v.string(), ""), // optional stable name; default derives from session id
|
|
362
|
+
});
|
|
363
|
+
export const SandboxConfigSchema = v.object({
|
|
364
|
+
backend: v.optional(SandboxBackendSchema, "local"),
|
|
365
|
+
scope: v.optional(SandboxScopeSchema, "agent"),
|
|
366
|
+
workspace_dir: v.optional(v.string(), "/workspace"), // absolute path INSIDE the sandbox
|
|
367
|
+
// The ROOT of the handoff plane inside the sandbox. The per-run dir the
|
|
368
|
+
// prompts actually see is DERIVED from it: `<handoff_dir>/sessions/<adw_id>/
|
|
369
|
+
// context_handoff`, mirroring `handoff_host` (`runner.ts`'s
|
|
370
|
+
// `context_handoff_dir`) — see `SandboxSpec.handoff_sandbox` below. MUST
|
|
371
|
+
// NOT be under `workspace_dir` — enforced by `validateSandboxConfig`.
|
|
372
|
+
handoff_dir: v.optional(v.string(), "/spf/handoff"),
|
|
373
|
+
// The TRANSPORT'S OWN scratch plane (seed.tar, seed.patch[.b64],
|
|
374
|
+
// out.patch[.b64]) — same "not under workspace_dir" rule, same reason,
|
|
375
|
+
// same enforcement as handoff_dir.
|
|
376
|
+
scratch_dir: v.optional(v.string(), "/spf/tmp"),
|
|
377
|
+
// opensandbox only. NO DEFAULT: `validateSandboxConfig` rejects an empty
|
|
378
|
+
// `image` when the resolved backend is "opensandbox" — it MUST contain
|
|
379
|
+
// git + tar + base64 (the transport shells out to all three).
|
|
380
|
+
image: v.optional(v.string(), ""),
|
|
381
|
+
setup: v.optional(v.array(v.string()), () => []), // shell commands run ONCE per sandbox, after seeding
|
|
382
|
+
// ConnectionConfig.requestTimeoutSeconds — the SDK's CLIENT-SIDE HTTP
|
|
383
|
+
// timeout, applied to every control-plane call. NOT the bound on a long
|
|
384
|
+
// agent command (exec is driven with streaming handlers) — see sandbox.ts.
|
|
385
|
+
request_timeout_seconds: v.optional(v.pipe(v.number(), v.minValue(1)), 960),
|
|
386
|
+
lifetime_seconds: v.optional(v.pipe(v.number(), v.minValue(1)), 3_600), // provider-side expiry, renewed while a lease is live
|
|
387
|
+
max_total_lifetime_seconds: v.optional(v.pipe(v.number(), v.minValue(1)), 21_600), // HARD ceiling from creation; renew refuses past it
|
|
388
|
+
// The DEFAULT per-command deadline, applied only when the caller supplies
|
|
389
|
+
// none. NOT a ceiling — a caller-supplied timeoutMs passes through
|
|
390
|
+
// unmodified, exactly as it does on backend: local. See sandbox.ts.
|
|
391
|
+
exec_timeout_seconds: v.optional(v.pipe(v.number(), v.minValue(1)), 900),
|
|
392
|
+
// Credentials handed to the sandbox process environment at create time.
|
|
393
|
+
// DEFAULT IS EMPTY — deliberately NOT the operator env local() gets today.
|
|
394
|
+
// The set that actually reaches a given container is the intersection of
|
|
395
|
+
// this list with THAT AGENT's own env_allowlist (unset = no further
|
|
396
|
+
// narrowing) — see agents.ts's sandboxSpecFor.
|
|
397
|
+
env_allowlist: v.optional(v.array(v.string()), () => []),
|
|
398
|
+
credentials: v.optional(SandboxCredentialsSchema, () => v.parse(SandboxCredentialsSchema, {})),
|
|
399
|
+
egress: v.optional(SandboxEgressSchema, () => v.parse(SandboxEgressSchema, {})),
|
|
400
|
+
transport: v.optional(SandboxTransportSchema, () => v.parse(SandboxTransportSchema, {})),
|
|
401
|
+
// worktree (default, honored) | sandbox (fan-out THROUGH sandboxes — PR B).
|
|
402
|
+
// Parsed and schema-valid here in PR A; agents.ts's validateSandboxConfig
|
|
403
|
+
// hard-rejects "sandbox" until PR B lands, naming the key — never a silent
|
|
404
|
+
// degrade back to worktrees.
|
|
405
|
+
fanout: v.optional(v.picklist(["worktree", "sandbox"]), "worktree"),
|
|
406
|
+
opensandbox: v.optional(SandboxOpenSandboxSchema, () => v.parse(SandboxOpenSandboxSchema, {})),
|
|
407
|
+
cloudflare: v.optional(SandboxCloudflareSchema, () => v.parse(SandboxCloudflareSchema, {})),
|
|
408
|
+
});
|
|
305
409
|
export const AgentConfigSchema = v.object({
|
|
306
410
|
name: v.string(),
|
|
307
411
|
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
@@ -330,6 +434,13 @@ export const AgentConfigSchema = v.object({
|
|
|
330
434
|
// (PATH/HOME/USER/LANG/TERM/TMPDIR) that agent_flue.ts's local() sandbox
|
|
331
435
|
// would keep anyway.
|
|
332
436
|
env_allowlist: v.optional(v.nullable(v.array(v.string()))),
|
|
437
|
+
// Per-agent sandbox backend override (SPF #15) — a bare backend name, not
|
|
438
|
+
// a nested block; everything else (image, egress, transport, credentials
|
|
439
|
+
// POLICY) stays repo-scoped on `sandbox:` above.
|
|
440
|
+
// unset -> inherit sandbox.backend
|
|
441
|
+
// "local" -> force local for this agent
|
|
442
|
+
// null -> same as unset (the writes/env_allowlist spelling)
|
|
443
|
+
sandbox: v.optional(v.nullable(SandboxBackendSchema)),
|
|
333
444
|
});
|
|
334
445
|
export const ConfigDefaultsSchema = v.object({
|
|
335
446
|
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
@@ -504,6 +615,36 @@ export const WatchRefineConfigSchema = v.object({
|
|
|
504
615
|
chain: v.optional(v.string(), "refine"),
|
|
505
616
|
concurrency: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1),
|
|
506
617
|
});
|
|
618
|
+
/**
|
|
619
|
+
* Best-of-N per claimed issue — `core/fanout.ts`'s proven machinery pointed at
|
|
620
|
+
* watch's per-issue dispatch. `n: 1` (the DEFAULT) leaves the daemon's
|
|
621
|
+
* behaviour untouched: `core/watch.ts`'s `runIssue` takes its existing
|
|
622
|
+
* single-dispatch path, same adw_id (`issue-<id>`), same one worktree, same
|
|
623
|
+
* everything. (`spf doctor` gains one informational line either way — see
|
|
624
|
+
* that command; nothing about a running daemon changes.)
|
|
625
|
+
*
|
|
626
|
+
* `concurrency` is DELIBERATELY NOT `watch.concurrency`, which counts ISSUES in
|
|
627
|
+
* flight (`core/watch.ts`'s `state.inflight`, a set of issue ids). This one
|
|
628
|
+
* counts ATTEMPTS in flight WITHIN one issue. The two multiply — see
|
|
629
|
+
* `spf doctor`'s `watch.fanout budget` line, which prints the product rather
|
|
630
|
+
* than leaving an operator to discover it on an invoice. Same "each axis of
|
|
631
|
+
* parallelism gets its own named budget" rule `refine.concurrency` already
|
|
632
|
+
* follows (see `WatchRunState.refining`'s doc comment).
|
|
633
|
+
*
|
|
634
|
+
* NOTE that `concurrency` bounds attempts IN FLIGHT, not worktrees ON DISK: a
|
|
635
|
+
* successful attempt's tree is KEPT until every sibling has settled (it is a
|
|
636
|
+
* selection candidate), so peak disk is `n` trees per issue regardless of this
|
|
637
|
+
* number. Doctor prints both.
|
|
638
|
+
*
|
|
639
|
+
* The `n` ceiling is 8, the twin of `MAX_N` in `cli/commands/fanout.ts` — kept
|
|
640
|
+
* as a literal here rather than imported because this module imports nothing
|
|
641
|
+
* but valibot, on purpose (see `SandboxSpec`'s placement note). A daemon typing
|
|
642
|
+
* a bigger number is strictly worse than an interactive command doing so.
|
|
643
|
+
*/
|
|
644
|
+
export const WatchFanoutConfigSchema = v.object({
|
|
645
|
+
n: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(8)), 1),
|
|
646
|
+
concurrency: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 2),
|
|
647
|
+
});
|
|
507
648
|
export const WatchConfigSchema = v.object({
|
|
508
649
|
issue_provider: v.optional(WatchIssueProviderSchema, "github"),
|
|
509
650
|
code_host: v.optional(WatchCodeHostSchema, "github"),
|
|
@@ -532,6 +673,7 @@ export const WatchConfigSchema = v.object({
|
|
|
532
673
|
chain_options: v.optional(v.record(v.string(), v.string()), () => ({})),
|
|
533
674
|
jira: v.optional(WatchJiraConfigSchema, () => v.parse(WatchJiraConfigSchema, {})),
|
|
534
675
|
refine: v.optional(WatchRefineConfigSchema, () => v.parse(WatchRefineConfigSchema, {})),
|
|
676
|
+
fanout: v.optional(WatchFanoutConfigSchema, () => v.parse(WatchFanoutConfigSchema, {})),
|
|
535
677
|
});
|
|
536
678
|
/**
|
|
537
679
|
* Optional outbound push for unattended work (`spf watch`, any chain run) —
|
|
@@ -666,6 +808,7 @@ export const SFConfigSchema = v.object({
|
|
|
666
808
|
notifications: v.optional(NotificationsConfigSchema, () => v.parse(NotificationsConfigSchema, {})),
|
|
667
809
|
review: v.optional(ReviewConfigSchema, () => v.parse(ReviewConfigSchema, {})),
|
|
668
810
|
tiering: v.optional(TieringConfigSchema, () => v.parse(TieringConfigSchema, {})),
|
|
811
|
+
sandbox: v.optional(SandboxConfigSchema, () => v.parse(SandboxConfigSchema, {})),
|
|
669
812
|
});
|
|
670
813
|
// ── Tracing ──────────────────────────────────────────────────────────────────
|
|
671
814
|
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|