@gr8ful/spf 0.15.0 → 0.17.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 +15 -5
- package/assets/skill/references/config.md +9 -5
- package/assets/skill/references/observability.md +57 -12
- package/assets/templates/ts-opencode.spf.config.yaml +54 -0
- package/dist/chains/index.js +1 -1
- package/dist/chains/simple_sdlc.d.ts +2 -2
- package/dist/chains/simple_sdlc.js +13 -13
- package/dist/chains/steps.d.ts +2 -2
- package/dist/chains/steps.js +35 -19
- package/dist/cli/commands/abort.d.ts +1 -1
- package/dist/cli/commands/abort.js +30 -3
- package/dist/cli/commands/doctor.js +109 -8
- package/dist/cli/commands/estimate.js +3 -3
- package/dist/cli/commands/events.js +4 -4
- package/dist/cli/commands/fanout.js +93 -21
- package/dist/cli/commands/loop.js +31 -32
- package/dist/cli/commands/migrate.js +8 -1
- package/dist/cli/commands/phases.js +2 -2
- package/dist/cli/commands/sessions.js +2 -2
- package/dist/cli/commands/trace.d.ts +28 -8
- package/dist/cli/commands/trace.js +28 -15
- package/dist/cli/commands/ui.js +15 -5
- package/dist/cli/commands/watch.js +27 -27
- package/dist/cli/index.js +3 -1
- package/dist/cli/interview.d.ts +1 -0
- package/dist/cli/interview.js +86 -4
- package/dist/core/agent_opencode.d.ts +247 -0
- package/dist/core/agent_opencode.js +590 -0
- package/dist/core/agents.d.ts +12 -12
- package/dist/core/agents.js +113 -46
- package/dist/core/console.d.ts +12 -12
- package/dist/core/console.js +25 -25
- package/dist/core/data_types.d.ts +126 -12
- package/dist/core/data_types.js +101 -4
- package/dist/core/fanout.d.ts +1 -1
- package/dist/core/fanout.js +1 -1
- package/dist/core/gates.js +14 -1
- package/dist/core/paths.d.ts +41 -4
- package/dist/core/paths.js +32 -3
- package/dist/core/quality.d.ts +7 -7
- package/dist/core/quality.js +16 -10
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +39 -27
- package/dist/core/session.d.ts +2 -2
- package/dist/core/session.js +39 -18
- package/dist/core/sqlite.d.ts +14 -7
- package/dist/core/sqlite.js +14 -7
- package/dist/core/trace_db.d.ts +118 -0
- package/dist/core/trace_db.js +278 -0
- package/dist/core/tracer.d.ts +64 -34
- package/dist/core/tracer.js +141 -69
- package/dist/core/watch.d.ts +4 -4
- package/dist/core/watch.js +2 -2
- package/dist/ui/server/app.js +10 -10
- package/dist/ui/server/db.d.ts +89 -21
- package/dist/ui/server/db.js +235 -99
- package/dist/ui/server/serve.d.ts +5 -1
- package/dist/ui/server/serve.js +4 -5
- package/package.json +1 -1
- package/web/assets/index-CQ3k1Y1-.css +1 -0
- package/web/assets/index-CU8tom6S.js +21 -0
- package/web/index.html +2 -2
- package/web/assets/index-CRujNW-1.js +0 -11
- package/web/assets/index-Cto6nuQL.css +0 -1
|
@@ -671,7 +671,7 @@ export interface SandboxSpec {
|
|
|
671
671
|
}
|
|
672
672
|
export declare const AgentConfigSchema: v.ObjectSchema<{
|
|
673
673
|
readonly name: v.StringSchema<undefined>;
|
|
674
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
674
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
675
675
|
readonly model: v.OptionalSchema<v.StringSchema<undefined>, "google/gemini-3.6-flash">;
|
|
676
676
|
readonly thinking: v.OptionalSchema<v.PicklistSchema<["off", "minimal", "low", "medium", "high", "xhigh", "max"], undefined>, "medium">;
|
|
677
677
|
readonly color: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -688,7 +688,7 @@ export declare const AgentConfigSchema: v.ObjectSchema<{
|
|
|
688
688
|
}, undefined>;
|
|
689
689
|
export type AgentConfig = v.InferOutput<typeof AgentConfigSchema>;
|
|
690
690
|
export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
691
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
691
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
692
692
|
readonly model: v.OptionalSchema<v.StringSchema<undefined>, "google/gemini-3.6-flash">;
|
|
693
693
|
readonly thinking: v.OptionalSchema<v.PicklistSchema<["off", "minimal", "low", "medium", "high", "xhigh", "max"], undefined>, "medium">;
|
|
694
694
|
readonly color: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -763,8 +763,106 @@ export declare const OTelConfigSchema: v.ObjectSchema<{
|
|
|
763
763
|
readonly service_name: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
764
764
|
}, undefined>;
|
|
765
765
|
export type OTelConfig = v.InferOutput<typeof OTelConfigSchema>;
|
|
766
|
+
/**
|
|
767
|
+
* MIGRATION NOTE (BT-issue #66, 3 PRs): `observability.db` used to be ONLY a
|
|
768
|
+
* bare string (a local sqlite path, defaulting to ".spf/data/spf.db"). This
|
|
769
|
+
* is now a discriminated shape that additionally accepts an explicit
|
|
770
|
+
* `{ kind: "sqlite", path? }` object (equivalent to the string form, just
|
|
771
|
+
* spelled out) and a `{ kind: "d1", database_id, ... }` object that names a
|
|
772
|
+
* remote Cloudflare D1 database instead of a local file. The bare-string
|
|
773
|
+
* form is unchanged and remains the default for every existing config —
|
|
774
|
+
* nothing in the wild needs to change. `resolveObservabilityDb` below
|
|
775
|
+
* normalizes all three accepted forms to one shape.
|
|
776
|
+
*
|
|
777
|
+
* PR 1 (this schema + normalizer + `paths.resolveDataPaths` plumbing) left
|
|
778
|
+
* `resolveDataPaths` refusing a `kind: "d1"` config outright — no adapter
|
|
779
|
+
* existed yet. PR 2 (`core/trace_db.ts`'s `TraceDb` interface, `LocalTraceDb`,
|
|
780
|
+
* `D1TraceDb`, and `createTraceDb()`, wired through `Tracer`/`SfDb`) removes
|
|
781
|
+
* that refusal: `resolveDataPaths` now returns a usable descriptor for
|
|
782
|
+
* either kind, and both backends are real. PR 3 wires `spf doctor`/`spf
|
|
783
|
+
* init`'s interview flow to the new option.
|
|
784
|
+
*/
|
|
785
|
+
export declare const SqliteDbConfigSchema: v.ObjectSchema<{
|
|
786
|
+
readonly kind: v.LiteralSchema<"sqlite", undefined>;
|
|
787
|
+
readonly path: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data/spf.db">;
|
|
788
|
+
}, undefined>;
|
|
789
|
+
export type SqliteDbConfig = v.InferOutput<typeof SqliteDbConfigSchema>;
|
|
790
|
+
/**
|
|
791
|
+
* `account_id_env`/`api_token_env` default to the SAME env var names
|
|
792
|
+
* `core/cloudflare_provider.ts` already reads for the Cloudflare Workers AI
|
|
793
|
+
* provider (`CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_API_TOKEN`, see
|
|
794
|
+
* `core/providers.ts`'s `PROVIDER_ENV_KEYS.cloudflare`) — one Cloudflare
|
|
795
|
+
* account's credentials, read from the same two env vars, cover both
|
|
796
|
+
* Workers AI and a D1-backed trace db unless a config explicitly points
|
|
797
|
+
* elsewhere (e.g. a second Cloudflare account) by naming different env vars.
|
|
798
|
+
*/
|
|
799
|
+
export declare const D1DbConfigSchema: v.ObjectSchema<{
|
|
800
|
+
readonly kind: v.LiteralSchema<"d1", undefined>;
|
|
801
|
+
readonly database_id: v.StringSchema<undefined>;
|
|
802
|
+
readonly account_id_env: v.OptionalSchema<v.StringSchema<undefined>, "CLOUDFLARE_ACCOUNT_ID">;
|
|
803
|
+
readonly api_token_env: v.OptionalSchema<v.StringSchema<undefined>, "CLOUDFLARE_API_TOKEN">;
|
|
804
|
+
}, undefined>;
|
|
805
|
+
export type D1DbConfig = v.InferOutput<typeof D1DbConfigSchema>;
|
|
806
|
+
export declare const ObservabilityDbSchema: v.SchemaWithPipe<readonly [v.UnknownSchema, v.RawTransformAction<unknown, string | {
|
|
807
|
+
kind: "sqlite";
|
|
808
|
+
path: string;
|
|
809
|
+
} | {
|
|
810
|
+
kind: "d1";
|
|
811
|
+
database_id: string;
|
|
812
|
+
account_id_env: string;
|
|
813
|
+
api_token_env: string;
|
|
814
|
+
}>]>;
|
|
815
|
+
export type ObservabilityDbConfig = v.InferOutput<typeof ObservabilityDbSchema>;
|
|
816
|
+
/**
|
|
817
|
+
* What `resolveObservabilityDb` ACCEPTS — deliberately more permissive than
|
|
818
|
+
* `ObservabilityDbConfig` (that type is the schema's OUTPUT, post-default,
|
|
819
|
+
* so every optional field reads as required). This is the pre-default INPUT
|
|
820
|
+
* shape — `path`/`account_id_env`/`api_token_env` all optional, exactly
|
|
821
|
+
* what a hand-written `{kind:"sqlite"}` or `{kind:"d1", database_id:"..."}`
|
|
822
|
+
* literal looks like before `v.parse` fills its defaults in. An already-
|
|
823
|
+
* parsed `ObservabilityDbConfig` (every optional field present) satisfies
|
|
824
|
+
* this too, so the one normalizer works on config fresh off `v.parse` AND
|
|
825
|
+
* on a literal built by hand (e.g. in a test, or by a future caller that
|
|
826
|
+
* hasn't gone through the schema).
|
|
827
|
+
*/
|
|
828
|
+
export type ObservabilityDbInput = string | {
|
|
829
|
+
kind: "sqlite";
|
|
830
|
+
path?: string;
|
|
831
|
+
} | {
|
|
832
|
+
kind: "d1";
|
|
833
|
+
database_id: string;
|
|
834
|
+
account_id_env?: string;
|
|
835
|
+
api_token_env?: string;
|
|
836
|
+
};
|
|
837
|
+
/** What `resolveObservabilityDb` normalizes any of the three accepted `db:` forms to. PR 2's D1 adapter is built against this shape. */
|
|
838
|
+
export type NormalizedObservabilityDb = {
|
|
839
|
+
kind: "sqlite";
|
|
840
|
+
path: string;
|
|
841
|
+
} | {
|
|
842
|
+
kind: "d1";
|
|
843
|
+
database_id: string;
|
|
844
|
+
account_id_env: string;
|
|
845
|
+
api_token_env: string;
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* Normalizes `observability.db` — bare string, `{kind:"sqlite",...}`, or
|
|
849
|
+
* `{kind:"d1",...}` — to one shape. Pure and side-effect-free: does NOT
|
|
850
|
+
* resolve `path` to an absolute path (that's `paths.resolveDataPaths`'s job,
|
|
851
|
+
* which needs `repo_root` to do it) and does NOT read the D1 env vars it
|
|
852
|
+
* names (that's PR 2's adapter's job, at the point it actually needs to
|
|
853
|
+
* authenticate).
|
|
854
|
+
*/
|
|
855
|
+
export declare function resolveObservabilityDb(db: ObservabilityDbInput): NormalizedObservabilityDb;
|
|
766
856
|
export declare const ObservabilityConfigSchema: v.ObjectSchema<{
|
|
767
|
-
readonly db: v.OptionalSchema<v.
|
|
857
|
+
readonly db: v.OptionalSchema<v.SchemaWithPipe<readonly [v.UnknownSchema, v.RawTransformAction<unknown, string | {
|
|
858
|
+
kind: "sqlite";
|
|
859
|
+
path: string;
|
|
860
|
+
} | {
|
|
861
|
+
kind: "d1";
|
|
862
|
+
database_id: string;
|
|
863
|
+
account_id_env: string;
|
|
864
|
+
api_token_env: string;
|
|
865
|
+
}>]>, ".spf/data/spf.db">;
|
|
768
866
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 500>;
|
|
769
867
|
readonly otel: v.OptionalSchema<v.ObjectSchema<{
|
|
770
868
|
readonly endpoint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.UrlAction<string, undefined>]>;
|
|
@@ -1304,7 +1402,7 @@ export type ReviewConfig = v.InferOutput<typeof ReviewConfigSchema>;
|
|
|
1304
1402
|
*/
|
|
1305
1403
|
export declare const TierSchema: v.ObjectSchema<{
|
|
1306
1404
|
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1307
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1405
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
1308
1406
|
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1309
1407
|
}, undefined>;
|
|
1310
1408
|
export type Tier = v.InferOutput<typeof TierSchema>;
|
|
@@ -1339,7 +1437,7 @@ export declare const TieringConfigSchema: v.ObjectSchema<{
|
|
|
1339
1437
|
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
1340
1438
|
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
1341
1439
|
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1342
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1440
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
1343
1441
|
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1344
1442
|
}, undefined>, undefined>, () => never[]>;
|
|
1345
1443
|
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
@@ -1348,7 +1446,7 @@ export type TieringConfig = v.InferOutput<typeof TieringConfigSchema>;
|
|
|
1348
1446
|
export declare const SFConfigSchema: v.ObjectSchema<{
|
|
1349
1447
|
readonly env: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
1350
1448
|
readonly defaults: v.OptionalSchema<v.ObjectSchema<{
|
|
1351
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1449
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
1352
1450
|
readonly model: v.OptionalSchema<v.StringSchema<undefined>, "google/gemini-3.6-flash">;
|
|
1353
1451
|
readonly thinking: v.OptionalSchema<v.PicklistSchema<["off", "minimal", "low", "medium", "high", "xhigh", "max"], undefined>, "medium">;
|
|
1354
1452
|
readonly color: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -1401,7 +1499,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1401
1499
|
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
1402
1500
|
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
1403
1501
|
}, undefined>, () => {
|
|
1404
|
-
coding_agent: "claude_code" | "flue";
|
|
1502
|
+
coding_agent: "claude_code" | "flue" | "opencode";
|
|
1405
1503
|
model: string;
|
|
1406
1504
|
thinking: "high" | "low" | "max" | "medium" | "minimal" | "off" | "xhigh";
|
|
1407
1505
|
color: string;
|
|
@@ -1413,7 +1511,15 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1413
1511
|
max_run_tokens?: number | undefined;
|
|
1414
1512
|
}>;
|
|
1415
1513
|
readonly observability: v.OptionalSchema<v.ObjectSchema<{
|
|
1416
|
-
readonly db: v.OptionalSchema<v.
|
|
1514
|
+
readonly db: v.OptionalSchema<v.SchemaWithPipe<readonly [v.UnknownSchema, v.RawTransformAction<unknown, string | {
|
|
1515
|
+
kind: "sqlite";
|
|
1516
|
+
path: string;
|
|
1517
|
+
} | {
|
|
1518
|
+
kind: "d1";
|
|
1519
|
+
database_id: string;
|
|
1520
|
+
account_id_env: string;
|
|
1521
|
+
api_token_env: string;
|
|
1522
|
+
}>]>, ".spf/data/spf.db">;
|
|
1417
1523
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 500>;
|
|
1418
1524
|
readonly otel: v.OptionalSchema<v.ObjectSchema<{
|
|
1419
1525
|
readonly endpoint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.UrlAction<string, undefined>]>;
|
|
@@ -1421,7 +1527,15 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1421
1527
|
readonly service_name: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
1422
1528
|
}, undefined>, undefined>;
|
|
1423
1529
|
}, undefined>, () => {
|
|
1424
|
-
db: string
|
|
1530
|
+
db: string | {
|
|
1531
|
+
kind: "sqlite";
|
|
1532
|
+
path: string;
|
|
1533
|
+
} | {
|
|
1534
|
+
kind: "d1";
|
|
1535
|
+
database_id: string;
|
|
1536
|
+
account_id_env: string;
|
|
1537
|
+
api_token_env: string;
|
|
1538
|
+
};
|
|
1425
1539
|
poll_ms: number;
|
|
1426
1540
|
otel?: {
|
|
1427
1541
|
endpoint: string;
|
|
@@ -1433,7 +1547,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1433
1547
|
}>;
|
|
1434
1548
|
readonly agents: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
1435
1549
|
readonly name: v.StringSchema<undefined>;
|
|
1436
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1550
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
1437
1551
|
readonly model: v.OptionalSchema<v.StringSchema<undefined>, "google/gemini-3.6-flash">;
|
|
1438
1552
|
readonly thinking: v.OptionalSchema<v.PicklistSchema<["off", "minimal", "low", "medium", "high", "xhigh", "max"], undefined>, "medium">;
|
|
1439
1553
|
readonly color: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -1694,7 +1808,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1694
1808
|
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
1695
1809
|
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
1696
1810
|
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1697
|
-
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1811
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code", "opencode"], undefined>, "flue">;
|
|
1698
1812
|
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1699
1813
|
}, undefined>, undefined>, () => never[]>;
|
|
1700
1814
|
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
@@ -1702,7 +1816,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1702
1816
|
enabled: boolean;
|
|
1703
1817
|
tiers: {
|
|
1704
1818
|
name: string;
|
|
1705
|
-
coding_agent: "claude_code" | "flue";
|
|
1819
|
+
coding_agent: "claude_code" | "flue" | "opencode";
|
|
1706
1820
|
model: string;
|
|
1707
1821
|
}[];
|
|
1708
1822
|
roles: {
|
package/dist/core/data_types.js
CHANGED
|
@@ -475,7 +475,7 @@ export const SandboxConfigSchema = v.object({
|
|
|
475
475
|
});
|
|
476
476
|
export const AgentConfigSchema = v.object({
|
|
477
477
|
name: v.string(),
|
|
478
|
-
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
478
|
+
coding_agent: v.optional(v.picklist(["flue", "claude_code", "opencode"]), "flue"),
|
|
479
479
|
model: v.optional(v.string(), "google/gemini-3.6-flash"),
|
|
480
480
|
thinking: v.optional(ThinkingLevelSchema, "medium"),
|
|
481
481
|
color: v.optional(v.string(), ""), // hex swatch for this agent's lane in the UI
|
|
@@ -510,7 +510,7 @@ export const AgentConfigSchema = v.object({
|
|
|
510
510
|
sandbox: v.optional(v.nullable(SandboxBackendSchema)),
|
|
511
511
|
});
|
|
512
512
|
export const ConfigDefaultsSchema = v.object({
|
|
513
|
-
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
513
|
+
coding_agent: v.optional(v.picklist(["flue", "claude_code", "opencode"]), "flue"),
|
|
514
514
|
model: v.optional(v.string(), "google/gemini-3.6-flash"),
|
|
515
515
|
thinking: v.optional(ThinkingLevelSchema, "medium"),
|
|
516
516
|
color: v.optional(v.string(), ""),
|
|
@@ -587,8 +587,97 @@ export const OTelConfigSchema = v.object({
|
|
|
587
587
|
headers: v.optional(v.record(v.string(), v.string()), undefined),
|
|
588
588
|
service_name: v.optional(v.string(), "spf"),
|
|
589
589
|
});
|
|
590
|
+
/**
|
|
591
|
+
* MIGRATION NOTE (BT-issue #66, 3 PRs): `observability.db` used to be ONLY a
|
|
592
|
+
* bare string (a local sqlite path, defaulting to ".spf/data/spf.db"). This
|
|
593
|
+
* is now a discriminated shape that additionally accepts an explicit
|
|
594
|
+
* `{ kind: "sqlite", path? }` object (equivalent to the string form, just
|
|
595
|
+
* spelled out) and a `{ kind: "d1", database_id, ... }` object that names a
|
|
596
|
+
* remote Cloudflare D1 database instead of a local file. The bare-string
|
|
597
|
+
* form is unchanged and remains the default for every existing config —
|
|
598
|
+
* nothing in the wild needs to change. `resolveObservabilityDb` below
|
|
599
|
+
* normalizes all three accepted forms to one shape.
|
|
600
|
+
*
|
|
601
|
+
* PR 1 (this schema + normalizer + `paths.resolveDataPaths` plumbing) left
|
|
602
|
+
* `resolveDataPaths` refusing a `kind: "d1"` config outright — no adapter
|
|
603
|
+
* existed yet. PR 2 (`core/trace_db.ts`'s `TraceDb` interface, `LocalTraceDb`,
|
|
604
|
+
* `D1TraceDb`, and `createTraceDb()`, wired through `Tracer`/`SfDb`) removes
|
|
605
|
+
* that refusal: `resolveDataPaths` now returns a usable descriptor for
|
|
606
|
+
* either kind, and both backends are real. PR 3 wires `spf doctor`/`spf
|
|
607
|
+
* init`'s interview flow to the new option.
|
|
608
|
+
*/
|
|
609
|
+
export const SqliteDbConfigSchema = v.object({
|
|
610
|
+
kind: v.literal("sqlite"),
|
|
611
|
+
path: v.optional(v.string(), ".spf/data/spf.db"),
|
|
612
|
+
});
|
|
613
|
+
/**
|
|
614
|
+
* `account_id_env`/`api_token_env` default to the SAME env var names
|
|
615
|
+
* `core/cloudflare_provider.ts` already reads for the Cloudflare Workers AI
|
|
616
|
+
* provider (`CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_API_TOKEN`, see
|
|
617
|
+
* `core/providers.ts`'s `PROVIDER_ENV_KEYS.cloudflare`) — one Cloudflare
|
|
618
|
+
* account's credentials, read from the same two env vars, cover both
|
|
619
|
+
* Workers AI and a D1-backed trace db unless a config explicitly points
|
|
620
|
+
* elsewhere (e.g. a second Cloudflare account) by naming different env vars.
|
|
621
|
+
*/
|
|
622
|
+
export const D1DbConfigSchema = v.object({
|
|
623
|
+
kind: v.literal("d1"),
|
|
624
|
+
database_id: v.string(),
|
|
625
|
+
account_id_env: v.optional(v.string(), "CLOUDFLARE_ACCOUNT_ID"),
|
|
626
|
+
api_token_env: v.optional(v.string(), "CLOUDFLARE_API_TOKEN"),
|
|
627
|
+
});
|
|
628
|
+
/**
|
|
629
|
+
* A plain `v.union([v.string(), v.variant("kind", [...])])` was tried first
|
|
630
|
+
* and rejected: valibot's plain union reports every failed branch with the
|
|
631
|
+
* same generic "Expected (string | Object) but received Object", swallowing
|
|
632
|
+
* `v.variant`'s own precise "kind must be sqlite or d1" / "database_id is
|
|
633
|
+
* required" messages. This `rawTransform` dispatches on `typeof value`
|
|
634
|
+
* itself — string passes through unchanged (the back-compat form), anything
|
|
635
|
+
* else is handed to `v.variant("kind", ...)` and ITS issues (which already
|
|
636
|
+
* name the offending field) are re-surfaced verbatim, prefixed with
|
|
637
|
+
* `observability.db:` so a nested failure is still easy to place in a large
|
|
638
|
+
* config file. See `src/test/data_types.test.ts` for the exact messages
|
|
639
|
+
* this produces for each rejected shape.
|
|
640
|
+
*/
|
|
641
|
+
const ObservabilityDbVariantSchema = v.variant("kind", [SqliteDbConfigSchema, D1DbConfigSchema]);
|
|
642
|
+
export const ObservabilityDbSchema = v.pipe(v.unknown(), v.rawTransform(({ dataset, addIssue, NEVER }) => {
|
|
643
|
+
const value = dataset.value;
|
|
644
|
+
if (typeof value === "string")
|
|
645
|
+
return value;
|
|
646
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
647
|
+
const result = v.safeParse(ObservabilityDbVariantSchema, value);
|
|
648
|
+
if (result.success)
|
|
649
|
+
return result.output;
|
|
650
|
+
for (const issue of result.issues)
|
|
651
|
+
addIssue({ message: `observability.db: ${issue.message}` });
|
|
652
|
+
return NEVER;
|
|
653
|
+
}
|
|
654
|
+
addIssue({
|
|
655
|
+
message: `observability.db must be a string path, or an object with kind: "sqlite" or kind: "d1" (got ${JSON.stringify(value)})`,
|
|
656
|
+
});
|
|
657
|
+
return NEVER;
|
|
658
|
+
}));
|
|
659
|
+
/**
|
|
660
|
+
* Normalizes `observability.db` — bare string, `{kind:"sqlite",...}`, or
|
|
661
|
+
* `{kind:"d1",...}` — to one shape. Pure and side-effect-free: does NOT
|
|
662
|
+
* resolve `path` to an absolute path (that's `paths.resolveDataPaths`'s job,
|
|
663
|
+
* which needs `repo_root` to do it) and does NOT read the D1 env vars it
|
|
664
|
+
* names (that's PR 2's adapter's job, at the point it actually needs to
|
|
665
|
+
* authenticate).
|
|
666
|
+
*/
|
|
667
|
+
export function resolveObservabilityDb(db) {
|
|
668
|
+
if (typeof db === "string")
|
|
669
|
+
return { kind: "sqlite", path: db };
|
|
670
|
+
if (db.kind === "sqlite")
|
|
671
|
+
return { kind: "sqlite", path: db.path ?? ".spf/data/spf.db" };
|
|
672
|
+
return {
|
|
673
|
+
kind: "d1",
|
|
674
|
+
database_id: db.database_id,
|
|
675
|
+
account_id_env: db.account_id_env ?? "CLOUDFLARE_ACCOUNT_ID",
|
|
676
|
+
api_token_env: db.api_token_env ?? "CLOUDFLARE_API_TOKEN",
|
|
677
|
+
};
|
|
678
|
+
}
|
|
590
679
|
export const ObservabilityConfigSchema = v.object({
|
|
591
|
-
db: v.optional(
|
|
680
|
+
db: v.optional(ObservabilityDbSchema, ".spf/data/spf.db"),
|
|
592
681
|
poll_ms: v.optional(v.number(), 500),
|
|
593
682
|
// Absent by default. `agents.ts`'s mergeRawConfig spreads `observability`
|
|
594
683
|
// field-by-field, so this nested object merges as a WHOLE-OBJECT replace on
|
|
@@ -596,6 +685,14 @@ export const ObservabilityConfigSchema = v.object({
|
|
|
596
685
|
// which is the semantics you want for an endpoint + its headers (a
|
|
597
686
|
// half-merged pair of the two would send tokens to the wrong collector).
|
|
598
687
|
// Pinned by a merge-survival test in src/test/data_types.test.ts.
|
|
688
|
+
//
|
|
689
|
+
// `db` merges the same whole-value way: `mergeRawConfig`'s `observability`
|
|
690
|
+
// spread is key-by-key over `observability`'s OWN keys (db/poll_ms/otel),
|
|
691
|
+
// never deeper — an override's `db:` (string or object, either kind)
|
|
692
|
+
// replaces the base's `db:` in full, so a `{kind:"d1",...}` override can
|
|
693
|
+
// never end up with a stray `path` key leaked in from a `{kind:"sqlite",
|
|
694
|
+
// path:...}` base (or vice versa). See the merge-survival test for `db` in
|
|
695
|
+
// src/test/data_types.test.ts.
|
|
599
696
|
otel: v.optional(OTelConfigSchema),
|
|
600
697
|
});
|
|
601
698
|
/**
|
|
@@ -962,7 +1059,7 @@ export const ReviewConfigSchema = v.object({
|
|
|
962
1059
|
*/
|
|
963
1060
|
export const TierSchema = v.object({
|
|
964
1061
|
name: v.pipe(v.string(), v.minLength(1)),
|
|
965
|
-
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
1062
|
+
coding_agent: v.optional(v.picklist(["flue", "claude_code", "opencode"]), "flue"),
|
|
966
1063
|
model: v.pipe(v.string(), v.minLength(1)),
|
|
967
1064
|
});
|
|
968
1065
|
/**
|
package/dist/core/fanout.d.ts
CHANGED
|
@@ -204,7 +204,7 @@ export interface FanoutDeps {
|
|
|
204
204
|
/** Run one attempt's chain in `cwd`, returning its exit code. */
|
|
205
205
|
runAttempt: (dispatch: AttemptDispatch) => Promise<number>;
|
|
206
206
|
/** Read this adw_id's gate/usage rows from the SHARED db. Must not throw — return `ZERO_METRICS` on any hiccup. */
|
|
207
|
-
readMetrics: (adwId: string) => AttemptMetrics
|
|
207
|
+
readMetrics: (adwId: string) => Promise<AttemptMetrics>;
|
|
208
208
|
log: (message: string) => void;
|
|
209
209
|
/**
|
|
210
210
|
* Opt-in first-past-the-post. Default (unset/false): every attempt that
|
package/dist/core/fanout.js
CHANGED
|
@@ -239,7 +239,7 @@ export async function runBestOf(deps) {
|
|
|
239
239
|
decided = true;
|
|
240
240
|
let metrics = ZERO_METRICS;
|
|
241
241
|
try {
|
|
242
|
-
metrics = deps.readMetrics(adwId);
|
|
242
|
+
metrics = await deps.readMetrics(adwId);
|
|
243
243
|
}
|
|
244
244
|
catch (caught) {
|
|
245
245
|
// Metrics are for RANKING, never for correctness — an attempt whose
|
package/dist/core/gates.js
CHANGED
|
@@ -96,7 +96,20 @@ export function diffMatchesClaims(envelope, run) {
|
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
const exists = existsSync(resolved);
|
|
99
|
-
|
|
99
|
+
// A bulk/mechanical change (a repo-wide formatter, a codemod) tempts an
|
|
100
|
+
// agent to compress many identical edits into one descriptive sentence
|
|
101
|
+
// ("139 files reformatted (app/, components/, ...)") instead of listing
|
|
102
|
+
// each path — that string obviously never resolves, but the plain
|
|
103
|
+
// "does not exist" message gives no hint why, so a retry repeats the
|
|
104
|
+
// same mistake. Whitespace inside an otherwise-nonexistent claim is the
|
|
105
|
+
// cheap, cheap-to-check signal: a real repo-relative path is never a
|
|
106
|
+
// prose fragment. Never suppresses the failure itself, only clarifies it.
|
|
107
|
+
const looksLikeProse = !exists && /\s/.test(f);
|
|
108
|
+
report.check(f, exists, exists
|
|
109
|
+
? `exists, ${size(resolved)}`
|
|
110
|
+
: looksLikeProse
|
|
111
|
+
? "claimed changed file does not exist — this looks like a summary sentence, not a path; list every changed file individually, even for a bulk/mechanical change"
|
|
112
|
+
: "claimed changed file does not exist");
|
|
100
113
|
}
|
|
101
114
|
return report;
|
|
102
115
|
}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* levels below the package root, so the same relative path resolves either
|
|
17
17
|
* way), with `assets/` a direct child of it.
|
|
18
18
|
*/
|
|
19
|
+
import { type NormalizedObservabilityDb, type ObservabilityDbInput } from "./data_types.ts";
|
|
19
20
|
export declare const PACKAGE_ROOT: string;
|
|
20
21
|
export declare const ASSETS_DIR: string;
|
|
21
22
|
/** The prebuilt SPA — a sibling of dist/ and assets/, built once at publish time (see app/). */
|
|
@@ -54,9 +55,22 @@ export interface DataPaths {
|
|
|
54
55
|
data_dir: string;
|
|
55
56
|
/** Repo-root-relative, forward-slash — for matching against git's own path output. */
|
|
56
57
|
data_dir_rel: string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The normalized `observability.db` descriptor — `core/trace_db.ts`'s
|
|
60
|
+
* `createTraceDb()` (called by both `Tracer` and `SfDb`) is built against
|
|
61
|
+
* exactly this shape. For `kind: "sqlite"`, `path` here is already
|
|
62
|
+
* resolved ABSOLUTE (byte-for-byte the same value as `db_path` below); for
|
|
63
|
+
* `kind: "d1"` there is no local path at all.
|
|
64
|
+
*/
|
|
65
|
+
db: NormalizedObservabilityDb;
|
|
66
|
+
/** Absolute local sqlite path, or `null` for a `kind: "d1"` config — there is no local file to open. */
|
|
67
|
+
db_path: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Absolute. Anchored directly off `data_dir` (NOT off `db_path`) — session
|
|
70
|
+
* JSONL/envelope artifact files always live on the local filesystem
|
|
71
|
+
* regardless of where the queryable trace mirror (`db` above) lives, so
|
|
72
|
+
* this must resolve even when `db_path` is `null` for a d1-backed repo.
|
|
73
|
+
*/
|
|
60
74
|
sessions_dir: string;
|
|
61
75
|
}
|
|
62
76
|
/**
|
|
@@ -70,8 +84,31 @@ export interface DataPaths {
|
|
|
70
84
|
*
|
|
71
85
|
* Not otherwise side-effect-free: see `healBrokenDataDir` above, which every
|
|
72
86
|
* caller needs run before its own first `mkdirSync` against `data_dir`.
|
|
87
|
+
*
|
|
88
|
+
* `rawDb` accepts every form `ObservabilityConfigSchema.db` accepts (bare
|
|
89
|
+
* string, `{kind:"sqlite",...}`, `{kind:"d1",...}`) — normalized here via
|
|
90
|
+
* `resolveObservabilityDb` — so every existing call site
|
|
91
|
+
* (`resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db)`)
|
|
92
|
+
* keeps compiling and behaving unchanged with no call-site edit needed.
|
|
93
|
+
*
|
|
94
|
+
* For a `"sqlite"`-kind db, `db_path`/`db.path` resolve BYTE-FOR-BYTE
|
|
95
|
+
* identically to before this PR: `path.resolve(repo_root, path)`.
|
|
96
|
+
*
|
|
97
|
+
* For a `"d1"`-kind db there is no local file at all — `db_path` is `null`,
|
|
98
|
+
* and `db` carries the resolved `{kind:"d1", database_id, account_id_env,
|
|
99
|
+
* api_token_env}` for `core/trace_db.ts`'s `createTraceDb()` to open (PR 2:
|
|
100
|
+
* SPF #66). `sessions_dir` has no `db_path` to anchor off in this case, so it
|
|
101
|
+
* falls back to `data_dir/sessions` — session JSONL/envelope artifacts still
|
|
102
|
+
* always live on disk regardless of where the queryable trace mirror lives.
|
|
103
|
+
*
|
|
104
|
+
* For a `"sqlite"`-kind db, `sessions_dir` stays what it always was — ALWAYS
|
|
105
|
+
* a sibling of `db_path` (`dirname(db_path)/sessions`), never `data_dir`
|
|
106
|
+
* directly — `migrate.ts` and `SfDb` both depend on that: a repo whose db
|
|
107
|
+
* lives outside `data_dir` (`observability.db: elsewhere/custom.db` next to
|
|
108
|
+
* `defaults.data_dir: .spf/data`) still has its sessions found. Relocating
|
|
109
|
+
* `data_dir` alone must never orphan an existing db's session artifacts.
|
|
73
110
|
*/
|
|
74
|
-
export declare function resolveDataPaths(anchor: RepoAnchor, rawDataDir: string,
|
|
111
|
+
export declare function resolveDataPaths(anchor: RepoAnchor, rawDataDir: string, rawDb: ObservabilityDbInput): DataPaths;
|
|
75
112
|
/**
|
|
76
113
|
* Resolve a prompt ref (a `prompt_engineering.system`/`.user` value from
|
|
77
114
|
* config) to an absolute path, trying every root a ref could sensibly be
|
package/dist/core/paths.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, lstatSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { findRepoRoot } from "./git_helper.js";
|
|
22
|
+
import { resolveObservabilityDb } from "./data_types.js";
|
|
22
23
|
export const PACKAGE_ROOT = path.resolve(import.meta.dirname, "..", "..");
|
|
23
24
|
export const ASSETS_DIR = path.join(PACKAGE_ROOT, "assets");
|
|
24
25
|
/** The prebuilt SPA — a sibling of dist/ and assets/, built once at publish time (see app/). */
|
|
@@ -121,14 +122,42 @@ function healBrokenDataDir(data_dir) {
|
|
|
121
122
|
*
|
|
122
123
|
* Not otherwise side-effect-free: see `healBrokenDataDir` above, which every
|
|
123
124
|
* caller needs run before its own first `mkdirSync` against `data_dir`.
|
|
125
|
+
*
|
|
126
|
+
* `rawDb` accepts every form `ObservabilityConfigSchema.db` accepts (bare
|
|
127
|
+
* string, `{kind:"sqlite",...}`, `{kind:"d1",...}`) — normalized here via
|
|
128
|
+
* `resolveObservabilityDb` — so every existing call site
|
|
129
|
+
* (`resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db)`)
|
|
130
|
+
* keeps compiling and behaving unchanged with no call-site edit needed.
|
|
131
|
+
*
|
|
132
|
+
* For a `"sqlite"`-kind db, `db_path`/`db.path` resolve BYTE-FOR-BYTE
|
|
133
|
+
* identically to before this PR: `path.resolve(repo_root, path)`.
|
|
134
|
+
*
|
|
135
|
+
* For a `"d1"`-kind db there is no local file at all — `db_path` is `null`,
|
|
136
|
+
* and `db` carries the resolved `{kind:"d1", database_id, account_id_env,
|
|
137
|
+
* api_token_env}` for `core/trace_db.ts`'s `createTraceDb()` to open (PR 2:
|
|
138
|
+
* SPF #66). `sessions_dir` has no `db_path` to anchor off in this case, so it
|
|
139
|
+
* falls back to `data_dir/sessions` — session JSONL/envelope artifacts still
|
|
140
|
+
* always live on disk regardless of where the queryable trace mirror lives.
|
|
141
|
+
*
|
|
142
|
+
* For a `"sqlite"`-kind db, `sessions_dir` stays what it always was — ALWAYS
|
|
143
|
+
* a sibling of `db_path` (`dirname(db_path)/sessions`), never `data_dir`
|
|
144
|
+
* directly — `migrate.ts` and `SfDb` both depend on that: a repo whose db
|
|
145
|
+
* lives outside `data_dir` (`observability.db: elsewhere/custom.db` next to
|
|
146
|
+
* `defaults.data_dir: .spf/data`) still has its sessions found. Relocating
|
|
147
|
+
* `data_dir` alone must never orphan an existing db's session artifacts.
|
|
124
148
|
*/
|
|
125
|
-
export function resolveDataPaths(anchor, rawDataDir,
|
|
149
|
+
export function resolveDataPaths(anchor, rawDataDir, rawDb) {
|
|
126
150
|
const data_dir = path.resolve(anchor.repo_root, rawDataDir);
|
|
127
151
|
healBrokenDataDir(data_dir);
|
|
128
152
|
const data_dir_rel = path.relative(anchor.repo_root, data_dir).split(path.sep).join("/");
|
|
129
|
-
const
|
|
153
|
+
const normalized = resolveObservabilityDb(rawDb);
|
|
154
|
+
if (normalized.kind === "d1") {
|
|
155
|
+
const sessions_dir = path.resolve(data_dir, "sessions");
|
|
156
|
+
return { data_dir, data_dir_rel, db: normalized, db_path: null, sessions_dir };
|
|
157
|
+
}
|
|
158
|
+
const db_path = path.resolve(anchor.repo_root, normalized.path);
|
|
130
159
|
const sessions_dir = path.resolve(path.dirname(db_path), "sessions");
|
|
131
|
-
return { data_dir, data_dir_rel, db_path, sessions_dir };
|
|
160
|
+
return { data_dir, data_dir_rel, db: { kind: "sqlite", path: db_path }, db_path, sessions_dir };
|
|
132
161
|
}
|
|
133
162
|
/**
|
|
134
163
|
* Resolve a prompt ref (a `prompt_engineering.system`/`.user` value from
|
package/dist/core/quality.d.ts
CHANGED
|
@@ -22,10 +22,10 @@ export interface RunLike {
|
|
|
22
22
|
context_handoff_dir: string;
|
|
23
23
|
repo_root: string;
|
|
24
24
|
console: {
|
|
25
|
-
note: (message: string) => void
|
|
25
|
+
note: (message: string) => Promise<void>;
|
|
26
26
|
};
|
|
27
27
|
tracer: {
|
|
28
|
-
event: (record: EventRecord) => string
|
|
28
|
+
event: (record: EventRecord) => Promise<string>;
|
|
29
29
|
};
|
|
30
30
|
adw_id: string;
|
|
31
31
|
}
|
|
@@ -41,7 +41,7 @@ export declare function resolveSuite(run: RunLike, suiteName: string): QualityCh
|
|
|
41
41
|
* QualityNotConfigured (via resolveSuite) if the suite or any of its checks
|
|
42
42
|
* isn't in the config — before any check runs, let alone any agent spawns.
|
|
43
43
|
*/
|
|
44
|
-
export declare function runSuite(run: RunLike, suiteName: string): QualityResult
|
|
44
|
+
export declare function runSuite(run: RunLike, suiteName: string): Promise<QualityResult>;
|
|
45
45
|
/**
|
|
46
46
|
* The `test` suite alone — the deterministic test phase most chains use.
|
|
47
47
|
*
|
|
@@ -50,9 +50,9 @@ export declare function runSuite(run: RunLike, suiteName: string): QualityResult
|
|
|
50
50
|
* subprocess already knows; the repair loop is unchanged, because a failure
|
|
51
51
|
* still reaches the builder through `asEnvelope` below.
|
|
52
52
|
*/
|
|
53
|
-
export declare function runTests(run: RunLike): QualityResult
|
|
53
|
+
export declare function runTests(run: RunLike): Promise<QualityResult>;
|
|
54
54
|
/** Every configured check, across every configured suite's union — the `all` suite. */
|
|
55
|
-
export declare function runQuality(run: RunLike): QualityResult
|
|
55
|
+
export declare function runQuality(run: RunLike): Promise<QualityResult>;
|
|
56
56
|
/**
|
|
57
57
|
* Log a deterministic block's verdict — the same shape every chain uses.
|
|
58
58
|
*
|
|
@@ -60,8 +60,8 @@ export declare function runQuality(run: RunLike): QualityResult;
|
|
|
60
60
|
* hand; one copy here instead of one per chain.
|
|
61
61
|
*/
|
|
62
62
|
export declare function record(ph: {
|
|
63
|
-
log: (payload: Record<string, unknown>) => void
|
|
64
|
-
}, result: QualityResult): void
|
|
63
|
+
log: (payload: Record<string, unknown>) => Promise<void>;
|
|
64
|
+
}, result: QualityResult): Promise<void>;
|
|
65
65
|
/**
|
|
66
66
|
* Wrap a deterministic result so an agent can be handed it directly.
|
|
67
67
|
*
|
package/dist/core/quality.js
CHANGED
|
@@ -56,13 +56,13 @@ function shellJoin(argv) {
|
|
|
56
56
|
.map((arg) => (/[\s"'$`\\!*?[\](){}<>|&;#~]/.test(arg) ? `'${arg.replace(/'/g, `'\\''`)}'` : arg))
|
|
57
57
|
.join(" ");
|
|
58
58
|
}
|
|
59
|
-
function runCheck(spec, run) {
|
|
59
|
+
async function runCheck(spec, run) {
|
|
60
60
|
const phase = run.phases[run.phases.length - 1];
|
|
61
61
|
const outputDir = checkDir(run, spec.name);
|
|
62
62
|
const outputArtifact = path.join(outputDir, "command.log");
|
|
63
63
|
const command = shellJoin(spec.argv);
|
|
64
64
|
const env = operatorEnv(); // the engineer's own shell environment
|
|
65
|
-
run.console.note(`quality ${spec.name}: ${command}`);
|
|
65
|
+
await run.console.note(`quality ${spec.name}: ${command}`);
|
|
66
66
|
const startedAt = nowIso();
|
|
67
67
|
const clock = performance.now();
|
|
68
68
|
let stdout = "";
|
|
@@ -100,7 +100,7 @@ function runCheck(spec, run) {
|
|
|
100
100
|
writeFileSync(outputArtifact, `$ ${command}\nexit: ${returncode}\nduration_seconds: ${duration.toFixed(3)}\n` +
|
|
101
101
|
`\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}\n`);
|
|
102
102
|
const passed = returncode === 0;
|
|
103
|
-
run.tracer.event(makeEventRecord({
|
|
103
|
+
await run.tracer.event(makeEventRecord({
|
|
104
104
|
adw_id: run.adw_id,
|
|
105
105
|
phase_id: phase.phase_id,
|
|
106
106
|
type: "tool_call",
|
|
@@ -116,7 +116,7 @@ function runCheck(spec, run) {
|
|
|
116
116
|
started_at: startedAt,
|
|
117
117
|
ended_at: nowIso(),
|
|
118
118
|
}));
|
|
119
|
-
run.console.note(`quality ${spec.name}: ${passed ? "passed" : "failed"} (exit ${returncode}, ${duration.toFixed(1)}s)`);
|
|
119
|
+
await run.console.note(`quality ${spec.name}: ${passed ? "passed" : "failed"} (exit ${returncode}, ${duration.toFixed(1)}s)`);
|
|
120
120
|
return {
|
|
121
121
|
name: spec.name,
|
|
122
122
|
area: spec.area,
|
|
@@ -139,9 +139,15 @@ function runCheck(spec, run) {
|
|
|
139
139
|
* QualityNotConfigured (via resolveSuite) if the suite or any of its checks
|
|
140
140
|
* isn't in the config — before any check runs, let alone any agent spawns.
|
|
141
141
|
*/
|
|
142
|
-
export function runSuite(run, suiteName) {
|
|
142
|
+
export async function runSuite(run, suiteName) {
|
|
143
143
|
const specs = resolveSuite(run, suiteName);
|
|
144
|
-
|
|
144
|
+
// Sequential, not Promise.all: check output/duration/tool_call ordering has
|
|
145
|
+
// always been one-at-a-time (spawnSync itself was always sequential) — a
|
|
146
|
+
// parallel run would interleave `run.console.note`/`run.tracer.event`
|
|
147
|
+
// calls across checks with no ordering guarantee.
|
|
148
|
+
const checks = [];
|
|
149
|
+
for (const spec of specs)
|
|
150
|
+
checks.push(await runCheck(spec, run));
|
|
145
151
|
// A failure is the command, its exit code, and what it actually printed —
|
|
146
152
|
// everything a builder needs to repair without opening a log or being told
|
|
147
153
|
// what the error "means" by a parser that guessed.
|
|
@@ -163,11 +169,11 @@ export function runSuite(run, suiteName) {
|
|
|
163
169
|
* subprocess already knows; the repair loop is unchanged, because a failure
|
|
164
170
|
* still reaches the builder through `asEnvelope` below.
|
|
165
171
|
*/
|
|
166
|
-
export function runTests(run) {
|
|
172
|
+
export async function runTests(run) {
|
|
167
173
|
return runSuite(run, "test");
|
|
168
174
|
}
|
|
169
175
|
/** Every configured check, across every configured suite's union — the `all` suite. */
|
|
170
|
-
export function runQuality(run) {
|
|
176
|
+
export async function runQuality(run) {
|
|
171
177
|
return runSuite(run, "all");
|
|
172
178
|
}
|
|
173
179
|
/**
|
|
@@ -176,9 +182,9 @@ export function runQuality(run) {
|
|
|
176
182
|
* Every quality/test phase in every chain reported this same summary by
|
|
177
183
|
* hand; one copy here instead of one per chain.
|
|
178
184
|
*/
|
|
179
|
-
export function record(ph, result) {
|
|
185
|
+
export async function record(ph, result) {
|
|
180
186
|
const passed = result.checks.filter((c) => c.passed).length;
|
|
181
|
-
ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
|
|
187
|
+
await ph.log({ passed: result.passed, checks: `${passed}/${result.checks.length}`, artifacts: result.artifacts.join(", ") });
|
|
182
188
|
}
|
|
183
189
|
/**
|
|
184
190
|
* Wrap a deterministic result so an agent can be handed it directly.
|