@opengsd/gsd-pi 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +10 -9
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
  4. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  5. package/dist/resources/extensions/gsd/auto-verification.js +25 -12
  6. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
  7. package/dist/resources/extensions/gsd/db/engine.js +8 -2
  8. package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
  9. package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
  12. package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
  13. package/dist/resources/extensions/gsd/model-router.js +10 -5
  14. package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
  15. package/dist/resources/extensions/gsd/preferences-models.js +50 -10
  16. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  17. package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
  18. package/dist/resources/extensions/gsd/preferences.js +7 -0
  19. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  20. package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
  21. package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
  22. package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
  23. package/dist/resources/extensions/subagent/index.js +1 -1
  24. package/dist/startup-model-validation.d.ts +1 -1
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.html +1 -1
  46. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  53. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  58. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  59. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  60. package/integrations/hermes/pyproject.toml +1 -1
  61. package/package.json +7 -6
  62. package/packages/cloud-mcp-gateway/package.json +2 -2
  63. package/packages/contracts/dist/rpc.d.ts +1 -1
  64. package/packages/contracts/dist/rpc.d.ts.map +1 -1
  65. package/packages/contracts/dist/rpc.js +1 -1
  66. package/packages/contracts/dist/rpc.js.map +1 -1
  67. package/packages/contracts/dist/rpc.test.js +1 -1
  68. package/packages/contracts/dist/rpc.test.js.map +1 -1
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/daemon/package.json +4 -4
  71. package/packages/gsd-agent-core/package.json +5 -5
  72. package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
  73. package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  81. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  83. package/packages/gsd-agent-modes/package.json +7 -7
  84. package/packages/gsd-cloud/README.md +12 -2
  85. package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
  86. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
  87. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cli.js +36 -12
  89. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  90. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
  91. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  92. package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
  93. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  94. package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
  95. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
  96. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
  97. package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
  98. package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
  99. package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
  100. package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
  101. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
  102. package/packages/gsd-cloud/dist/runtime-process.js +429 -61
  103. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
  104. package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
  105. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
  106. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
  107. package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
  108. package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
  109. package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
  110. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
  111. package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
  112. package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
  113. package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
  114. package/packages/gsd-cloud/package.json +2 -2
  115. package/packages/mcp-server/package.json +4 -4
  116. package/packages/native/package.json +1 -1
  117. package/packages/pi-agent-core/dist/types.d.ts +2 -2
  118. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  119. package/packages/pi-agent-core/dist/types.js.map +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/models.generated.d.ts +113 -2
  123. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  124. package/packages/pi-ai/dist/models.generated.js +84 -9
  125. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  126. package/packages/pi-ai/dist/models.js +18 -6
  127. package/packages/pi-ai/dist/models.js.map +1 -1
  128. package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
  129. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  130. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  131. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  134. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  135. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  136. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  138. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  140. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  141. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  142. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  143. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  145. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +1 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/types.d.ts +9 -1
  151. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  152. package/packages/pi-ai/dist/types.js.map +1 -1
  153. package/packages/pi-ai/package.json +1 -1
  154. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
  156. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  158. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
  160. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
  163. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  164. package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
  165. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  166. package/packages/pi-coding-agent/package.json +7 -7
  167. package/packages/pi-tui/package.json +2 -2
  168. package/packages/rpc-client/package.json +2 -2
  169. package/pkg/dist/theme/theme.d.ts +1 -1
  170. package/pkg/dist/theme/theme.d.ts.map +1 -1
  171. package/pkg/dist/theme/theme.js +1 -0
  172. package/pkg/dist/theme/theme.js.map +1 -1
  173. package/pkg/package.json +1 -1
  174. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
  175. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  176. package/src/resources/extensions/gsd/auto-verification.ts +46 -12
  177. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
  178. package/src/resources/extensions/gsd/db/engine.ts +9 -1
  179. package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
  180. package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
  181. package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
  182. package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
  183. package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
  184. package/src/resources/extensions/gsd/model-router.ts +10 -5
  185. package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
  186. package/src/resources/extensions/gsd/preferences-models.ts +54 -10
  187. package/src/resources/extensions/gsd/preferences-types.ts +10 -2
  188. package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
  189. package/src/resources/extensions/gsd/preferences.ts +13 -0
  190. package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
  191. package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
  192. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
  193. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
  194. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
  195. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  197. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
  198. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
  201. package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
  202. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
  204. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
  205. package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
  206. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
  207. package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
  208. package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
  209. package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
  210. package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
  211. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
  212. package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
  213. package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
  214. package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
  215. package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
  216. package/src/resources/extensions/subagent/index.ts +1 -1
  217. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
  218. /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
@@ -0,0 +1,459 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Executable contract for the additive v31 canonical database foundation.
3
+
4
+ import assert from "node:assert/strict";
5
+ import { spawnSync } from "node:child_process";
6
+ import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import { afterEach, test } from "node:test";
11
+ import { pathToFileURL } from "node:url";
12
+
13
+ import {
14
+ SCHEMA_VERSION,
15
+ _getAdapter,
16
+ _setMigrationFaultForTest,
17
+ closeDatabase,
18
+ openDatabase,
19
+ } from "../gsd-db.ts";
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const tempDirs = new Set<string>();
23
+
24
+ interface RawDb {
25
+ exec(sql: string): void;
26
+ prepare(sql: string): {
27
+ get(...args: unknown[]): Record<string, unknown> | undefined;
28
+ all(...args: unknown[]): Array<Record<string, unknown>>;
29
+ };
30
+ close(): void;
31
+ }
32
+
33
+ function openRawDatabase(path: string): RawDb {
34
+ const sqlite = require("node:sqlite") as { DatabaseSync: new (path: string) => RawDb };
35
+ return new sqlite.DatabaseSync(path);
36
+ }
37
+
38
+ function createDatabasePath(): string {
39
+ const dir = mkdtempSync(join(tmpdir(), "gsd-canonical-foundation-"));
40
+ tempDirs.add(dir);
41
+ return join(dir, "gsd.db");
42
+ }
43
+
44
+ function tableExists(db: RawDb, table: string): boolean {
45
+ return Boolean(
46
+ db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?").get(table),
47
+ );
48
+ }
49
+
50
+ function maxSchemaVersion(db: RawDb): number {
51
+ return Number(db.prepare("SELECT MAX(version) AS version FROM schema_version").get()?.version);
52
+ }
53
+
54
+ function rewindToV30(dbPath: string): void {
55
+ assert.equal(openDatabase(dbPath), true);
56
+ closeDatabase();
57
+
58
+ const raw = openRawDatabase(dbPath);
59
+ try {
60
+ raw.exec(`
61
+ DROP TABLE IF EXISTS workflow_outbox;
62
+ DROP TABLE IF EXISTS workflow_domain_events;
63
+ DROP TABLE IF EXISTS workflow_operations;
64
+ DROP TABLE IF EXISTS project_authority;
65
+ DELETE FROM schema_version;
66
+ INSERT INTO schema_version (version, applied_at) VALUES (30, '2026-07-12T00:00:00.000Z');
67
+ INSERT OR IGNORE INTO milestones (id, title, status, created_at)
68
+ VALUES ('M-LEGACY', 'Preserved legacy milestone', 'active', '2026-07-12T00:00:00.000Z');
69
+ INSERT OR IGNORE INTO audit_events
70
+ (event_id, trace_id, category, type, ts, payload_json)
71
+ VALUES
72
+ ('audit-legacy', 'trace-legacy', 'workflow', 'legacy', '2026-07-12T00:00:00.000Z', '{"kept":true}');
73
+ `);
74
+ } finally {
75
+ raw.close();
76
+ }
77
+ }
78
+
79
+ function inspectFromFreshProcess(dbPath: string): Record<string, unknown> {
80
+ const moduleHref = pathToFileURL(
81
+ join(process.cwd(), "src/resources/extensions/gsd/gsd-db.ts"),
82
+ ).href;
83
+ const script = [
84
+ `import { openDatabase, closeDatabase, _getAdapter } from ${JSON.stringify(moduleHref)};`,
85
+ "const path = process.argv[1];",
86
+ "if (!openDatabase(path)) throw new Error('database open failed');",
87
+ "const db = _getAdapter();",
88
+ "const version = db.prepare('SELECT MAX(version) AS value FROM schema_version').get().value;",
89
+ "const authority = db.prepare('SELECT project_id, revision, authority_epoch FROM project_authority').get();",
90
+ "const legacy = db.prepare(\"SELECT title FROM milestones WHERE id = 'M-LEGACY'\").get();",
91
+ "console.log(JSON.stringify({ version, authority, legacy }));",
92
+ "closeDatabase();",
93
+ ].join("\n");
94
+ const env = { ...process.env };
95
+ delete env.NODE_TEST_CONTEXT;
96
+ const child = spawnSync(
97
+ process.execPath,
98
+ [
99
+ "--import",
100
+ "./src/resources/extensions/gsd/tests/resolve-ts.mjs",
101
+ "--experimental-strip-types",
102
+ "--input-type=module",
103
+ "-e",
104
+ script,
105
+ dbPath,
106
+ ],
107
+ { cwd: process.cwd(), encoding: "utf8", env, timeout: 30_000 },
108
+ );
109
+ assert.equal(child.status, 0, child.stderr || child.stdout);
110
+ return JSON.parse(child.stdout) as Record<string, unknown>;
111
+ }
112
+
113
+ afterEach(() => {
114
+ _setMigrationFaultForTest(false);
115
+ closeDatabase();
116
+ for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
117
+ tempDirs.clear();
118
+ });
119
+
120
+ test("fresh database creates the v31 authority root and linked operation journal", () => {
121
+ assert.equal(SCHEMA_VERSION, 31);
122
+ const dbPath = createDatabasePath();
123
+ assert.equal(openDatabase(dbPath), true);
124
+ const db = _getAdapter();
125
+ assert.ok(db);
126
+
127
+ const authority = db.prepare(`
128
+ SELECT singleton, project_id, project_root_realpath, revision, authority_epoch
129
+ FROM project_authority
130
+ `).get();
131
+ assert.deepEqual(
132
+ {
133
+ singleton: authority?.singleton,
134
+ projectIdLength: String(authority?.project_id ?? "").length,
135
+ root: authority?.project_root_realpath,
136
+ revision: authority?.revision,
137
+ epoch: authority?.authority_epoch,
138
+ },
139
+ { singleton: 1, projectIdLength: 32, root: "", revision: 0, epoch: 0 },
140
+ );
141
+
142
+ assert.throws(
143
+ () => db.exec("INSERT INTO project_authority (singleton, project_id) VALUES (2, 'second')"),
144
+ /CHECK constraint failed/i,
145
+ );
146
+ assert.throws(
147
+ () => db.exec("UPDATE project_authority SET revision = -1 WHERE singleton = 1"),
148
+ /CHECK constraint failed/i,
149
+ );
150
+ assert.throws(
151
+ () => db.exec("UPDATE project_authority SET authority_epoch = -1 WHERE singleton = 1"),
152
+ /CHECK constraint failed/i,
153
+ );
154
+
155
+ db.exec(`
156
+ INSERT INTO workflow_operations (
157
+ operation_id, project_id, operation_type, idempotency_key,
158
+ expected_revision, resulting_revision,
159
+ expected_authority_epoch, resulting_authority_epoch,
160
+ actor_type, actor_id, source_transport, request_hash, created_at
161
+ )
162
+ SELECT
163
+ 'op-1', project_id, 'test.operation', 'idem-1',
164
+ 0, 1, 0, 0, 'agent', 'test-agent', 'test', 'request-1', '2026-07-12T00:00:01.000Z'
165
+ FROM project_authority WHERE singleton = 1;
166
+
167
+ INSERT INTO workflow_domain_events (
168
+ event_id, operation_id, event_index, project_id, project_revision,
169
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
170
+ )
171
+ SELECT
172
+ 'event-1', 'op-1', 0, project_id, 1,
173
+ 0, 'test.started', 'project', project_id, '{"step":1}', '2026-07-12T00:00:02.000Z'
174
+ FROM project_authority WHERE singleton = 1;
175
+
176
+ INSERT INTO workflow_domain_events (
177
+ event_id, operation_id, event_index, project_id, project_revision,
178
+ authority_epoch, event_type, entity_type, entity_id, caused_by_event_id,
179
+ payload_json, created_at
180
+ )
181
+ SELECT
182
+ 'event-2', 'op-1', 1, project_id, 1,
183
+ 0, 'test.finished', 'project', project_id, 'event-1',
184
+ '{"step":2}', '2026-07-12T00:00:03.000Z'
185
+ FROM project_authority WHERE singleton = 1;
186
+
187
+ INSERT INTO workflow_outbox (event_id, destination, available_at)
188
+ VALUES ('event-2', 'projection', '2026-07-12T00:00:04.000Z');
189
+ `);
190
+
191
+ const chain = db.prepare(`
192
+ SELECT
193
+ p.revision AS authority_revision,
194
+ o.expected_revision,
195
+ o.resulting_revision,
196
+ COUNT(DISTINCT e.event_id) AS event_count,
197
+ COUNT(DISTINCT x.outbox_id) AS outbox_count
198
+ FROM project_authority p
199
+ JOIN workflow_operations o ON o.project_id = p.project_id
200
+ JOIN workflow_domain_events e ON e.operation_id = o.operation_id
201
+ LEFT JOIN workflow_outbox x ON x.event_id = e.event_id
202
+ GROUP BY p.revision, o.expected_revision, o.resulting_revision
203
+ `).get();
204
+ assert.deepEqual(chain, {
205
+ authority_revision: 0,
206
+ expected_revision: 0,
207
+ resulting_revision: 1,
208
+ event_count: 2,
209
+ outbox_count: 1,
210
+ });
211
+
212
+ assert.throws(
213
+ () => db.exec(`
214
+ INSERT INTO workflow_operations (
215
+ operation_id, project_id, operation_type, idempotency_key,
216
+ expected_revision, resulting_revision,
217
+ expected_authority_epoch, resulting_authority_epoch,
218
+ actor_type, source_transport, request_hash, created_at
219
+ )
220
+ SELECT 'op-duplicate', project_id, 'test.operation', 'idem-1', 1, 2, 0, 0,
221
+ 'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
222
+ FROM project_authority WHERE singleton = 1
223
+ `),
224
+ /UNIQUE constraint failed/i,
225
+ );
226
+ assert.throws(
227
+ () => db.exec(`
228
+ INSERT INTO workflow_operations (
229
+ operation_id, project_id, operation_type, idempotency_key,
230
+ expected_revision, resulting_revision,
231
+ expected_authority_epoch, resulting_authority_epoch,
232
+ actor_type, source_transport, request_hash, created_at
233
+ )
234
+ SELECT 'op-same-revision', project_id, 'test.operation', 'idem-2', 0, 1, 0, 0,
235
+ 'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
236
+ FROM project_authority WHERE singleton = 1
237
+ `),
238
+ /UNIQUE constraint failed/i,
239
+ );
240
+ assert.throws(
241
+ () => db.exec(`
242
+ INSERT INTO workflow_domain_events (
243
+ event_id, operation_id, event_index, project_id, project_revision,
244
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
245
+ )
246
+ SELECT 'event-duplicate-index', 'op-1', 1, project_id, 1, 0,
247
+ 'test.duplicate', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
248
+ FROM project_authority WHERE singleton = 1
249
+ `),
250
+ /UNIQUE constraint failed/i,
251
+ );
252
+ assert.throws(
253
+ () => db.exec(`
254
+ INSERT INTO workflow_domain_events (
255
+ event_id, operation_id, event_index, project_id, project_revision,
256
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
257
+ )
258
+ SELECT 'event-wrong-revision', 'op-1', 2, project_id, 2, 0,
259
+ 'test.wrong-revision', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
260
+ FROM project_authority WHERE singleton = 1
261
+ `),
262
+ /FOREIGN KEY constraint failed/i,
263
+ );
264
+ assert.throws(
265
+ () => db.exec(`
266
+ INSERT INTO workflow_outbox (event_id, destination, available_at)
267
+ VALUES ('missing-event', 'projection', '2026-07-12T00:00:07.000Z')
268
+ `),
269
+ /FOREIGN KEY constraint failed/i,
270
+ );
271
+ assert.throws(
272
+ () => db.exec(`
273
+ INSERT INTO workflow_outbox (event_id, destination, available_at)
274
+ VALUES ('event-2', 'projection', '2026-07-12T00:00:08.000Z')
275
+ `),
276
+ /UNIQUE constraint failed/i,
277
+ );
278
+ });
279
+
280
+ test("operations record the resulting authority epoch used by their events", () => {
281
+ const dbPath = createDatabasePath();
282
+ assert.equal(openDatabase(dbPath), true);
283
+ const db = _getAdapter();
284
+ assert.ok(db);
285
+
286
+ db.exec(`
287
+ INSERT INTO workflow_operations (
288
+ operation_id, project_id, operation_type, idempotency_key,
289
+ expected_revision, resulting_revision,
290
+ expected_authority_epoch, resulting_authority_epoch,
291
+ actor_type, source_transport, request_hash, created_at
292
+ )
293
+ SELECT 'op-cutover', project_id, 'authority.cutover', 'idem-cutover',
294
+ 0, 1, 0, 1, 'agent', 'test', 'request-cutover', '2026-07-12T00:00:01.000Z'
295
+ FROM project_authority WHERE singleton = 1;
296
+
297
+ INSERT INTO workflow_domain_events (
298
+ event_id, operation_id, event_index, project_id, project_revision,
299
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
300
+ )
301
+ SELECT 'event-cutover', 'op-cutover', 0, project_id, 1, 1,
302
+ 'authority.cutover', 'project', project_id, '{}', '2026-07-12T00:00:02.000Z'
303
+ FROM project_authority WHERE singleton = 1;
304
+
305
+ INSERT INTO workflow_operations (
306
+ operation_id, project_id, operation_type, idempotency_key,
307
+ expected_revision, resulting_revision,
308
+ expected_authority_epoch, resulting_authority_epoch,
309
+ actor_type, source_transport, request_hash, created_at
310
+ )
311
+ SELECT 'op-unchanged-epoch', project_id, 'test.operation', 'idem-unchanged-epoch',
312
+ 1, 2, 1, 1, 'agent', 'test', 'request-unchanged', '2026-07-12T00:00:03.000Z'
313
+ FROM project_authority WHERE singleton = 1;
314
+ `);
315
+
316
+ assert.throws(
317
+ () => db.exec(`
318
+ INSERT INTO workflow_domain_events (
319
+ event_id, operation_id, event_index, project_id, project_revision,
320
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
321
+ )
322
+ SELECT 'event-stale-epoch', 'op-cutover', 1, project_id, 1, 0,
323
+ 'authority.stale', 'project', project_id, '{}', '2026-07-12T00:00:04.000Z'
324
+ FROM project_authority WHERE singleton = 1
325
+ `),
326
+ /FOREIGN KEY constraint failed/i,
327
+ );
328
+ assert.throws(
329
+ () => db.exec(`
330
+ INSERT INTO workflow_operations (
331
+ operation_id, project_id, operation_type, idempotency_key,
332
+ expected_revision, resulting_revision,
333
+ expected_authority_epoch, resulting_authority_epoch,
334
+ actor_type, source_transport, request_hash, created_at
335
+ )
336
+ SELECT 'op-epoch-jump', project_id, 'authority.cutover', 'idem-epoch-jump',
337
+ 2, 3, 1, 3, 'agent', 'test', 'request-jump', '2026-07-12T00:00:05.000Z'
338
+ FROM project_authority WHERE singleton = 1
339
+ `),
340
+ /CHECK constraint failed/i,
341
+ );
342
+ });
343
+
344
+ test("domain events reject updates and deletes", () => {
345
+ const dbPath = createDatabasePath();
346
+ assert.equal(openDatabase(dbPath), true);
347
+ const db = _getAdapter();
348
+ assert.ok(db);
349
+
350
+ db.exec(`
351
+ INSERT INTO workflow_operations (
352
+ operation_id, project_id, operation_type, idempotency_key,
353
+ expected_revision, resulting_revision,
354
+ expected_authority_epoch, resulting_authority_epoch,
355
+ actor_type, source_transport, request_hash, created_at
356
+ )
357
+ SELECT 'op-immutable', project_id, 'test.operation', 'idem-immutable',
358
+ 0, 1, 0, 0, 'agent', 'test', 'request-immutable', '2026-07-12T00:00:01.000Z'
359
+ FROM project_authority WHERE singleton = 1;
360
+
361
+ INSERT INTO workflow_domain_events (
362
+ event_id, operation_id, event_index, project_id, project_revision,
363
+ authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
364
+ )
365
+ SELECT 'event-immutable', 'op-immutable', 0, project_id, 1, 0,
366
+ 'test.recorded', 'project', project_id, '{"original":true}', '2026-07-12T00:00:02.000Z'
367
+ FROM project_authority WHERE singleton = 1;
368
+ `);
369
+
370
+ assert.throws(
371
+ () => db.exec(`UPDATE workflow_domain_events SET payload_json = '{}' WHERE event_id = 'event-immutable'`),
372
+ /workflow domain events are immutable/i,
373
+ );
374
+ assert.throws(
375
+ () => db.exec(`DELETE FROM workflow_domain_events WHERE event_id = 'event-immutable'`),
376
+ /workflow domain events are immutable/i,
377
+ );
378
+ });
379
+
380
+ test("v30 upgrade preserves legacy rows and creates an independently healthy backup", () => {
381
+ const dbPath = createDatabasePath();
382
+ rewindToV30(dbPath);
383
+
384
+ assert.equal(openDatabase(dbPath), true);
385
+ closeDatabase();
386
+
387
+ let projectId = "";
388
+ const upgraded = openRawDatabase(dbPath);
389
+ try {
390
+ assert.equal(maxSchemaVersion(upgraded), 31);
391
+ assert.equal(tableExists(upgraded, "project_authority"), true);
392
+ assert.equal(upgraded.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
393
+ assert.equal(upgraded.prepare("SELECT payload_json FROM audit_events WHERE event_id = 'audit-legacy'").get()?.payload_json, '{"kept":true}');
394
+ assert.equal(upgraded.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
395
+ projectId = String(upgraded.prepare("SELECT project_id FROM project_authority").get()?.project_id);
396
+ } finally {
397
+ upgraded.close();
398
+ }
399
+
400
+ const backup = openRawDatabase(`${dbPath}.backup-v30`);
401
+ try {
402
+ assert.equal(maxSchemaVersion(backup), 30);
403
+ assert.equal(tableExists(backup, "project_authority"), false);
404
+ assert.equal(backup.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
405
+ assert.equal(backup.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
406
+ } finally {
407
+ backup.close();
408
+ }
409
+
410
+ const reopened = inspectFromFreshProcess(dbPath);
411
+ assert.deepEqual(reopened, {
412
+ version: 31,
413
+ authority: { project_id: projectId, revision: 0, authority_epoch: 0 },
414
+ legacy: { title: "Preserved legacy milestone" },
415
+ });
416
+
417
+ const restoredPath = join(dirname(dbPath), "restored.db");
418
+ copyFileSync(`${dbPath}.backup-v30`, restoredPath);
419
+ const restored = inspectFromFreshProcess(restoredPath);
420
+ assert.equal(restored.version, 31);
421
+ assert.deepEqual(restored.legacy, { title: "Preserved legacy milestone" });
422
+ assert.deepEqual(
423
+ Object.fromEntries(
424
+ Object.entries(restored.authority as Record<string, unknown>).filter(([key]) => key !== "project_id"),
425
+ ),
426
+ { revision: 0, authority_epoch: 0 },
427
+ );
428
+ });
429
+
430
+ test("faulted v30 migration leaves no v31 state and retries cleanly", () => {
431
+ const dbPath = createDatabasePath();
432
+ rewindToV30(dbPath);
433
+
434
+ _setMigrationFaultForTest(true);
435
+ assert.throws(() => openDatabase(dbPath), /migration fault injected/);
436
+ _setMigrationFaultForTest(false);
437
+
438
+ const rolledBack = openRawDatabase(dbPath);
439
+ try {
440
+ assert.equal(maxSchemaVersion(rolledBack), 30);
441
+ assert.equal(tableExists(rolledBack, "project_authority"), false);
442
+ assert.equal(tableExists(rolledBack, "workflow_operations"), false);
443
+ assert.equal(tableExists(rolledBack, "workflow_domain_events"), false);
444
+ assert.equal(tableExists(rolledBack, "workflow_outbox"), false);
445
+ } finally {
446
+ rolledBack.close();
447
+ }
448
+
449
+ assert.equal(openDatabase(dbPath), true);
450
+ closeDatabase();
451
+ const retried = openRawDatabase(dbPath);
452
+ try {
453
+ assert.equal(maxSchemaVersion(retried), 31);
454
+ assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM project_authority").get()?.count, 1);
455
+ assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM milestones WHERE id = 'M-LEGACY'").get()?.count, 1);
456
+ } finally {
457
+ retried.close();
458
+ }
459
+ });
@@ -101,9 +101,9 @@ describe('db-engine migrate guards', () => {
101
101
  assert.ok(openDatabase(dbPath), 'fresh open should succeed');
102
102
  closeDatabase();
103
103
 
104
- // Rewind the DB to v(SCHEMA_VERSION-1): stamp the prior version and drop
105
- // the objects the last migration adds, so the re-run has real work to redo.
106
- const startVersion = SCHEMA_VERSION - 2;
104
+ // Rewind to v28 and remove a v29 column so the multi-step migration has
105
+ // real work to redo even as later schema versions are added.
106
+ const startVersion = 28;
107
107
  const raw = openRawSqliteForTest(dbPath);
108
108
  raw.exec('DELETE FROM schema_version');
109
109
  raw.exec(`INSERT INTO schema_version (version, applied_at) VALUES (${startVersion}, '2026-01-01T00:00:00Z')`);
@@ -206,6 +206,38 @@ test("checkEngineHealth ignores stale suffixed flat-phase duplicate when bare mi
206
206
  );
207
207
  });
208
208
 
209
+ test("checkEngineHealth reads task checkboxes from the canonical milestone worktree", async (t) => {
210
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-worktree-"));
211
+ t.after(() => rmSync(base, { recursive: true, force: true }));
212
+
213
+ const gsdDir = join(base, ".gsd");
214
+ mkdirSync(gsdDir, { recursive: true });
215
+
216
+ openDatabase(join(gsdDir, "gsd.db"));
217
+ insertMilestone({ id: "M003", title: "Milestone", status: "active" });
218
+ insertSlice({ id: "S01", milestoneId: "M003", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
219
+ insertTask({ id: "T03", milestoneId: "M003", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
220
+
221
+ const plan = await renderPlanFromDb(base, "M003", "S01");
222
+ const checkedPlan = readFileSync(plan.planPath, "utf-8");
223
+ writeFileSync(plan.planPath, checkedPlan.replace("- [x] **T03**:", "- [ ] **T03**:"), "utf-8");
224
+
225
+ const worktree = join(base, ".gsd-worktrees", "M003");
226
+ const worktreePlan = join(worktree, ".gsd", "phases", "03-milestone", "03-01-PLAN.md");
227
+ mkdirSync(join(worktree, ".gsd", "phases", "03-milestone"), { recursive: true });
228
+ writeFileSync(join(worktree, ".git"), `gitdir: ${join(base, ".git", "worktrees", "M003")}\n`);
229
+ writeFileSync(worktreePlan, checkedPlan, "utf-8");
230
+
231
+ const issues: any[] = [];
232
+ await checkEngineHealth(base, issues, []);
233
+
234
+ assert.deepEqual(
235
+ issues.filter((issue) => issue.code === "checkbox_db_status_divergence").map((issue) => issue.unitId),
236
+ [],
237
+ "the stale project-root PLAN must not compete with the live milestone worktree projection",
238
+ );
239
+ });
240
+
209
241
  test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
210
242
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
211
243
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -375,6 +407,38 @@ test("checkEngineHealth reports artifact rows whose files are missing on disk",
375
407
  assert.equal(missing.fixable, false);
376
408
  });
377
409
 
410
+ test("checkEngineHealth resolves artifact rows through the canonical milestone worktree", async (t) => {
411
+ const base = mkdtempSync(join(tmpdir(), "gsd-doctor-artifact-worktree-"));
412
+ t.after(() => rmSync(base, { recursive: true, force: true }));
413
+
414
+ const gsdDir = join(base, ".gsd");
415
+ const artifactPath = "phases/03-milestone/03-01-PLAN.md";
416
+ const worktree = join(base, ".gsd-worktrees", "M003");
417
+ mkdirSync(gsdDir, { recursive: true });
418
+ mkdirSync(join(worktree, ".gsd", "phases", "03-milestone"), { recursive: true });
419
+ writeFileSync(join(worktree, ".git"), `gitdir: ${join(base, ".git", "worktrees", "M003")}\n`);
420
+ writeFileSync(join(worktree, ".gsd", artifactPath), "# Plan\n", "utf-8");
421
+
422
+ openDatabase(join(gsdDir, "gsd.db"));
423
+ insertArtifact({
424
+ path: artifactPath,
425
+ artifact_type: "PLAN",
426
+ milestone_id: "M003",
427
+ slice_id: "S01",
428
+ task_id: null,
429
+ full_content: "# Plan\n",
430
+ });
431
+
432
+ const issues: any[] = [];
433
+ await checkEngineHealth(base, issues, []);
434
+
435
+ assert.equal(
436
+ issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === artifactPath),
437
+ false,
438
+ "worktree-local artifact rows must not be reported missing from the project root",
439
+ );
440
+ });
441
+
378
442
  test("checkEngineHealth resolves escaped .gsd artifact rows against the project .gsd directory", async (t) => {
379
443
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-escaped-artifact-"));
380
444
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -74,7 +74,8 @@ test("#2885: cost table includes openai-codex provider models", () => {
74
74
  const ids = BUNDLED_COST_TABLE.map(e => e.id);
75
75
  const codexModels = [
76
76
  "gpt-5.1", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
77
- "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6",
77
+ "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
78
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
78
79
  ];
79
80
  for (const model of codexModels) {
80
81
  assert.ok(ids.includes(model), `cost table should include openai-codex model "${model}"`);
@@ -110,10 +111,24 @@ test("gpt-5.5 uses official OpenAI list pricing", () => {
110
111
  assert.equal(entry.updatedAt, "2026-04-23");
111
112
  });
112
113
 
113
- test("gpt-5.6 mirrors gpt-5.5 pricing until official pricing is published", () => {
114
- const entry = lookupModelCost("gpt-5.6");
115
- assert.ok(entry, "lookupModelCost should find gpt-5.6");
116
- assert.equal(entry.inputPer1k, 0.005);
117
- assert.equal(entry.outputPer1k, 0.03);
118
- assert.equal(entry.updatedAt, "2026-07-08");
114
+ test("gpt-5.6 bare alias is not treated as a real cost-table model", () => {
115
+ assert.equal(lookupModelCost("gpt-5.6"), undefined);
116
+ });
117
+
118
+ test("gpt-5.6 variants use published pricing", () => {
119
+ const sol = lookupModelCost("openai-codex/gpt-5.6-sol");
120
+ assert.ok(sol, "lookupModelCost should find gpt-5.6-sol");
121
+ assert.equal(sol.inputPer1k, 0.005);
122
+ assert.equal(sol.outputPer1k, 0.03);
123
+ assert.deepEqual(sol.tiers?.[0], { inputTokensAbove: 272000, inputPer1k: 0.01, outputPer1k: 0.045 });
124
+
125
+ const terra = lookupModelCost("gpt-5.6-terra");
126
+ assert.ok(terra, "lookupModelCost should find gpt-5.6-terra");
127
+ assert.equal(terra.inputPer1k, 0.0025);
128
+ assert.equal(terra.outputPer1k, 0.015);
129
+
130
+ const luna = lookupModelCost("gpt-5.6-luna");
131
+ assert.ok(luna, "lookupModelCost should find gpt-5.6-luna");
132
+ assert.equal(luna.inputPer1k, 0.001);
133
+ assert.equal(luna.outputPer1k, 0.006);
119
134
  });
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
10
10
  import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
+ import { isCustomProvider } from "../preferences-models.ts";
13
14
 
14
15
  // ─── Test helpers ─────────────────────────────────────────────────────────────
15
16
 
@@ -303,3 +304,57 @@ describe("custom provider session model overrides PREFERENCES.md (#4122)", () =>
303
304
  });
304
305
  });
305
306
 
307
+ describe("custom provider detection (#801)", () => {
308
+ let originalGsdHome: string | undefined;
309
+ let tmpGsdHome: string;
310
+
311
+ beforeEach(() => {
312
+ originalGsdHome = process.env.GSD_HOME;
313
+ tmpGsdHome = makeTmpDir("gsd-home");
314
+ process.env.GSD_HOME = tmpGsdHome;
315
+ });
316
+
317
+ afterEach(() => {
318
+ if (originalGsdHome === undefined) {
319
+ delete process.env.GSD_HOME;
320
+ } else {
321
+ process.env.GSD_HOME = originalGsdHome;
322
+ }
323
+ try { rmSync(tmpGsdHome, { recursive: true, force: true }); } catch {}
324
+ });
325
+
326
+ it("detects extension-registered providers that are not declared in models.json", () => {
327
+ const registry = {
328
+ getAllWithDiscovered: () => [
329
+ { provider: "pi-llama-cpp" },
330
+ ],
331
+ };
332
+
333
+ assert.equal(isCustomProvider("pi-llama-cpp", registry), true,
334
+ "runtime providers outside the built-in catalog should count as custom");
335
+ });
336
+
337
+ it("does not treat built-in providers as custom just because they are in the registry", () => {
338
+ const registry = {
339
+ getAll: () => [
340
+ { provider: "claude-code" },
341
+ ],
342
+ };
343
+
344
+ assert.equal(isCustomProvider("claude-code", registry), false,
345
+ "built-in providers must still defer to PREFERENCES.md");
346
+ });
347
+
348
+ it("does not treat bundled external-CLI providers as custom", () => {
349
+ // claude-code/cursor-agent/google-gemini-cli/google-antigravity register at
350
+ // runtime (so they are absent from the generated @gsd/pi-ai catalog), but
351
+ // they are first-party built-ins that PREFERENCES.md can route to. They must
352
+ // never be classified as custom, otherwise auto-mode silently ignores
353
+ // PREFERENCES.md for the default provider.
354
+ for (const provider of ["cursor-agent", "google-gemini-cli", "google-antigravity"]) {
355
+ const registry = { getAll: () => [{ provider }] };
356
+ assert.equal(isCustomProvider(provider, registry), false,
357
+ `${provider} is a built-in CLI provider and must defer to PREFERENCES.md`);
358
+ }
359
+ });
360
+ });
@@ -900,7 +900,7 @@ test("#2885: openai-codex standard-tier models are recognized", () => {
900
900
 
901
901
  test("#2885: openai-codex heavy-tier models are recognized", () => {
902
902
  const config = { ...defaultRoutingConfig(), enabled: true };
903
- const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6", "o4-mini", "o4-mini-deep-research"];
903
+ const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "o4-mini", "o4-mini-deep-research"];
904
904
  for (const model of heavyModels) {
905
905
  const result = resolveModelForComplexity(
906
906
  makeClassification("heavy"),