@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,129 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Additive v31 authority, operation provenance, domain event, and outbox schema.
3
+
4
+ import type { DbAdapter } from "./db-adapter.js";
5
+
6
+ /**
7
+ * v31 mapping:
8
+ * - schema_version remains DDL compatibility, not the domain revision.
9
+ * - audit_events remains optional operational telemetry, not domain history.
10
+ * - milestone_commit_attributions remains Git-specific, not operation provenance.
11
+ * - command_queue and runtime_kv remain coordination/cache surfaces, not an outbox.
12
+ */
13
+ export function createCanonicalFoundationSchemaV31(db: DbAdapter): void {
14
+ db.exec(`
15
+ CREATE TABLE IF NOT EXISTS project_authority (
16
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
17
+ project_id TEXT NOT NULL UNIQUE,
18
+ project_root_realpath TEXT NOT NULL DEFAULT '',
19
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
20
+ authority_epoch INTEGER NOT NULL DEFAULT 0 CHECK (authority_epoch >= 0),
21
+ created_at TEXT NOT NULL DEFAULT '',
22
+ updated_at TEXT NOT NULL DEFAULT ''
23
+ )
24
+ `);
25
+
26
+ db.exec(`
27
+ CREATE TABLE IF NOT EXISTS workflow_operations (
28
+ operation_id TEXT PRIMARY KEY,
29
+ project_id TEXT NOT NULL,
30
+ operation_type TEXT NOT NULL,
31
+ idempotency_key TEXT NOT NULL,
32
+ expected_revision INTEGER NOT NULL CHECK (expected_revision >= 0),
33
+ resulting_revision INTEGER NOT NULL CHECK (resulting_revision = expected_revision + 1),
34
+ expected_authority_epoch INTEGER NOT NULL CHECK (expected_authority_epoch >= 0),
35
+ resulting_authority_epoch INTEGER NOT NULL CHECK (
36
+ resulting_authority_epoch = expected_authority_epoch OR
37
+ resulting_authority_epoch = expected_authority_epoch + 1
38
+ ),
39
+ actor_type TEXT NOT NULL,
40
+ actor_id TEXT DEFAULT NULL,
41
+ source_transport TEXT NOT NULL,
42
+ trace_id TEXT DEFAULT NULL,
43
+ turn_id TEXT DEFAULT NULL,
44
+ request_hash TEXT NOT NULL,
45
+ created_at TEXT NOT NULL,
46
+ UNIQUE (project_id, idempotency_key),
47
+ UNIQUE (project_id, resulting_revision),
48
+ UNIQUE (operation_id, project_id, resulting_revision, resulting_authority_epoch),
49
+ FOREIGN KEY (project_id) REFERENCES project_authority(project_id)
50
+ )
51
+ `);
52
+
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS workflow_domain_events (
55
+ event_id TEXT PRIMARY KEY,
56
+ operation_id TEXT NOT NULL,
57
+ event_index INTEGER NOT NULL DEFAULT 0 CHECK (event_index >= 0),
58
+ project_id TEXT NOT NULL,
59
+ project_revision INTEGER NOT NULL CHECK (project_revision > 0),
60
+ authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 0),
61
+ event_type TEXT NOT NULL,
62
+ entity_type TEXT NOT NULL,
63
+ entity_id TEXT NOT NULL,
64
+ caused_by_event_id TEXT DEFAULT NULL,
65
+ payload_json TEXT NOT NULL DEFAULT '{}',
66
+ created_at TEXT NOT NULL,
67
+ UNIQUE (operation_id, event_index),
68
+ FOREIGN KEY (operation_id, project_id, project_revision, authority_epoch)
69
+ REFERENCES workflow_operations(
70
+ operation_id, project_id, resulting_revision, resulting_authority_epoch
71
+ ),
72
+ FOREIGN KEY (caused_by_event_id) REFERENCES workflow_domain_events(event_id)
73
+ )
74
+ `);
75
+
76
+ db.exec(`
77
+ CREATE TRIGGER IF NOT EXISTS trg_workflow_domain_events_immutable_update
78
+ BEFORE UPDATE ON workflow_domain_events
79
+ BEGIN
80
+ SELECT RAISE(ABORT, 'workflow domain events are immutable');
81
+ END
82
+ `);
83
+ db.exec(`
84
+ CREATE TRIGGER IF NOT EXISTS trg_workflow_domain_events_immutable_delete
85
+ BEFORE DELETE ON workflow_domain_events
86
+ BEGIN
87
+ SELECT RAISE(ABORT, 'workflow domain events are immutable');
88
+ END
89
+ `);
90
+
91
+ db.exec(`
92
+ CREATE TABLE IF NOT EXISTS workflow_outbox (
93
+ outbox_id INTEGER PRIMARY KEY AUTOINCREMENT,
94
+ event_id TEXT NOT NULL,
95
+ destination TEXT NOT NULL,
96
+ available_at TEXT NOT NULL DEFAULT '',
97
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
98
+ claimed_by TEXT DEFAULT NULL,
99
+ claim_expires_at TEXT DEFAULT NULL,
100
+ delivered_at TEXT DEFAULT NULL,
101
+ last_error TEXT DEFAULT NULL,
102
+ UNIQUE (event_id, destination),
103
+ FOREIGN KEY (event_id) REFERENCES workflow_domain_events(event_id)
104
+ )
105
+ `);
106
+
107
+ db.exec(`
108
+ CREATE INDEX IF NOT EXISTS idx_workflow_operations_created
109
+ ON workflow_operations(project_id, created_at, operation_id)
110
+ `);
111
+ db.exec(`
112
+ CREATE INDEX IF NOT EXISTS idx_workflow_domain_events_entity
113
+ ON workflow_domain_events(project_id, entity_type, entity_id, project_revision, event_index)
114
+ `);
115
+ db.exec(`
116
+ CREATE INDEX IF NOT EXISTS idx_workflow_outbox_pending
117
+ ON workflow_outbox(delivered_at, available_at, outbox_id)
118
+ `);
119
+
120
+ db.exec(`
121
+ INSERT OR IGNORE INTO project_authority (
122
+ singleton, project_id, project_root_realpath,
123
+ revision, authority_epoch, created_at, updated_at
124
+ ) VALUES (
125
+ 1, lower(hex(randomblob(16))), '',
126
+ 0, 0, '', ''
127
+ )
128
+ `);
129
+ }
@@ -2,6 +2,7 @@
2
2
  // File Purpose: Schema migration DDL steps for the GSD database facade.
3
3
 
4
4
  import type { DbAdapter } from "./db-adapter.js";
5
+ import { createCanonicalFoundationSchemaV31 } from "./db-canonical-foundation-schema.js";
5
6
  import { ensureColumn } from "./db-schema-metadata.js";
6
7
 
7
8
  export function applyMigrationV2Artifacts(db: DbAdapter): void {
@@ -494,3 +495,7 @@ export function applyMigrationV30ReworkBriefs(db: DbAdapter): void {
494
495
  db.exec("CREATE INDEX IF NOT EXISTS idx_rework_briefs_task ON rework_briefs(milestone_id, slice_id, task_id)");
495
496
  db.exec("CREATE INDEX IF NOT EXISTS idx_rework_findings_status ON rework_brief_findings(brief_id, severity, status)");
496
497
  }
498
+
499
+ export function applyMigrationV31CanonicalFoundation(db: DbAdapter): void {
500
+ createCanonicalFoundationSchemaV31(db);
501
+ }
@@ -124,11 +124,11 @@ Diagnostics record the file path, scope (global/project), severity (error/warnin
124
124
  - `validation` — used for gate evaluation, roadmap reassessment, milestone validation, and doc rewrites. Falls back to `planning` if unset.
125
125
  - `uat` — used for UAT runs. Falls back to `completion` if unset.
126
126
 
127
- - `thinking`: per-phase reasoning effort (ADR-026), separate from `models`. Same phase keys as `models`. Values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. Thinking travels with the model — model choice and reasoning effort are independent controls, so you can run one model across phases at different reasoning levels.
127
+ - `thinking`: per-phase reasoning effort (ADR-026), separate from `models`. Same phase keys as `models`. Values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Thinking travels with the model — model choice and reasoning effort are independent controls, so you can run one model across phases at different reasoning levels.
128
128
  - Two equivalent ways to set it: inline as `models.<phase>.thinking`, or as a separate `thinking:` block keyed by phase. For the same phase, the inline value wins over the block; project preferences win over global; a phase that's unset inherits via the same sibling chain as `models` (e.g. `discuss → planning`).
129
129
  - If no thinking is configured for a phase, the session level (set via `/model`) is used, exactly as before — this is fully backward-compatible.
130
130
  - `execute-task` (code-writing) has a measured reasoning floor of `medium`: at lower levels models stop planning edits and thrash on file re-reads. The floor applies to the session/default path. An **explicit** `execution` thinking level bypasses the floor and is honored verbatim (with a one-time advisory) — set `execution: low` deliberately if you want it.
131
- - Levels a model can't support are clamped to the nearest supported level at dispatch and never sent to the provider, so a model/level mismatch never fails a unit mid-run. `xhigh` support is model-dependent; current examples include OpenAI Codex Max models and Claude Sonnet 5 on Amazon Bedrock.
131
+ - Levels a model can't support are clamped to the nearest supported level at dispatch and never sent to the provider, so a model/level mismatch never fails a unit mid-run. `xhigh` and `max` support are model-dependent; current examples include `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`, which advertise both.
132
132
 
133
133
  - `skill_staleness_days`: number — skills unused for this many days get deprioritized during discovery. Set to `0` to disable staleness tracking. Default: `60`.
134
134
 
@@ -357,24 +357,33 @@ In `"parent"` mode, slice/task `targetRepositories` default to the declared chil
357
357
  - `"replace"` requires `prompt`.
358
358
  - `"skip"` is valid with no additional fields.
359
359
 
360
+ - `planning_subagent_registry`: object — optional classification for custom planning agents. Each key is an agent id and must set:
361
+ - `read_only_specialist`: boolean — set to `true` to classify the custom agent as safe for planning dispatch.
362
+
360
363
  - `planning_subagents`: object — project-local widening for read-only planning subagent dispatch. Supported unit keys are `plan-milestone` and `plan-slice`; each has:
361
364
  - `allowed`: string[] — additional planning-safe agents for that unit.
362
365
 
363
- Agents must already be classified by GSD as read-only planning specialists: `mnemo`, `scout`, `planner`, `reviewer`, `security`, or `tester`. The write gate still blocks source writes outside `.gsd/**`, unrestricted bash, stale subagent calls without agent identities, agents outside the global read-only registry, and agents not listed for the active unit.
366
+ Agents must be built-in read-only planning specialists (`mnemo`, `scout`, `planner`, `reviewer`, `security`, or `tester`) or registered in `planning_subagent_registry` with `read_only_specialist: true`. The write gate still blocks source writes outside `.gsd/**`, unrestricted bash, stale subagent calls without agent identities, agents outside the read-only registry, and agents not listed for the active unit.
364
367
 
365
368
  Example:
366
369
 
367
370
  ```yaml
371
+ planning_subagent_registry:
372
+ my-custom-planner:
373
+ read_only_specialist: true
374
+
368
375
  planning_subagents:
369
376
  plan-milestone:
370
377
  allowed:
371
378
  - scout
372
379
  - planner
380
+ - my-custom-planner
373
381
  - security
374
382
  plan-slice:
375
383
  allowed:
376
384
  - scout
377
385
  - planner
386
+ - my-custom-planner
378
387
  - reviewer
379
388
  - security
380
389
  ```
@@ -32,6 +32,7 @@ import { parseRoadmapSlices } from "./roadmap-slices.js";
32
32
  import { parsePlan } from "./parsers-legacy.js";
33
33
  import { parseSummary } from "./files.js";
34
34
  import { LAYOUT_SEGMENTS } from "./layout-policy.js";
35
+ import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
35
36
 
36
37
  const USER_AUTHORED_ARTIFACT_TYPES = new Set(["CONTEXT", "RESEARCH"]);
37
38
 
@@ -116,7 +117,8 @@ function checkboxDbStatusMilestoneIds(basePath: string, milestoneIds: string[]):
116
117
 
117
118
  function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[], issues: DoctorIssue[]): void {
118
119
  for (const milestoneId of milestoneIds) {
119
- const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
120
+ const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, milestoneId);
121
+ const roadmapPath = resolveMilestoneFile(artifactBasePath, milestoneId, "ROADMAP");
120
122
  const slices = getMilestoneSlices(milestoneId);
121
123
 
122
124
  if (roadmapPath && existsSync(roadmapPath)) {
@@ -142,7 +144,7 @@ function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[
142
144
  }
143
145
 
144
146
  for (const slice of slices) {
145
- const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
147
+ const planPath = resolveSliceFile(artifactBasePath, milestoneId, slice.id, "PLAN");
146
148
  if (!planPath || !existsSync(planPath)) continue;
147
149
  try {
148
150
  const plan = readFileSync(planPath, "utf-8");
@@ -192,7 +194,7 @@ function artifactExistsOnDisk(basePath: string, artifactPath: string, row?: Arti
192
194
  return resolveArtifactDiskPath(basePath, artifactPath, row) !== null;
193
195
  }
194
196
 
195
- function resolveLiteralArtifactDiskPath(basePath: string, artifactPath: string): string | null {
197
+ function resolveLiteralArtifactDiskPath(basePath: string, artifactPath: string, row?: ArtifactRow): string | null {
196
198
  const relativeArtifactPath = artifactPathRelativeToGsd(artifactPath);
197
199
  if (isAbsolute(relativeArtifactPath)) {
198
200
  return existsSync(relativeArtifactPath) ? relativeArtifactPath : null;
@@ -201,6 +203,15 @@ function resolveLiteralArtifactDiskPath(basePath: string, artifactPath: string):
201
203
  const candidate = join(root, relativeArtifactPath);
202
204
  if (isPathInside(root, candidate) && existsSync(candidate)) return candidate;
203
205
  }
206
+ if (row?.milestone_id) {
207
+ const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, row.milestone_id);
208
+ if (artifactBasePath !== basePath) {
209
+ for (const root of [gsdProjectionRoot(artifactBasePath), gsdRoot(artifactBasePath)]) {
210
+ const candidate = join(root, relativeArtifactPath);
211
+ if (isPathInside(root, candidate) && existsSync(candidate)) return candidate;
212
+ }
213
+ }
214
+ }
204
215
  return null;
205
216
  }
206
217
 
@@ -243,7 +254,7 @@ function resolveArtifactDiskPath(basePath: string, artifactPath: string, row?: A
243
254
  const flatPath = resolveFlatPhaseArtifactDiskPath(basePath, row);
244
255
  if (flatPath) return flatPath;
245
256
  }
246
- return resolveLiteralArtifactDiskPath(basePath, artifactPath);
257
+ return resolveLiteralArtifactDiskPath(basePath, artifactPath, row);
247
258
  }
248
259
 
249
260
  function taskExistsInPlan(basePath: string, milestoneId: string, sliceId: string, taskId: string): boolean {
@@ -12,6 +12,12 @@ export interface ModelCostEntry {
12
12
  inputPer1k: number;
13
13
  /** Approximate cost per 1K output tokens in USD */
14
14
  outputPer1k: number;
15
+ /** Input-token-based long-context pricing tiers, when published */
16
+ tiers?: Array<{
17
+ inputTokensAbove: number;
18
+ inputPer1k: number;
19
+ outputPer1k: number;
20
+ }>;
15
21
  /** Last updated date */
16
22
  updatedAt: string;
17
23
  }
@@ -62,8 +68,9 @@ export const BUNDLED_COST_TABLE: ModelCostEntry[] = [
62
68
  // GPT-5.5 API list price, also used for live Codex OAuth routing.
63
69
  // Source: https://openai.com/api/pricing/
64
70
  { id: "gpt-5.5", inputPer1k: 0.005, outputPer1k: 0.03, updatedAt: "2026-04-23" },
65
- // Mirrors gpt-5.5 list pricing until OpenAI publishes official gpt-5.6 pricing.
66
- { id: "gpt-5.6", inputPer1k: 0.005, outputPer1k: 0.03, updatedAt: "2026-07-08" },
71
+ { id: "gpt-5.6-sol", inputPer1k: 0.005, outputPer1k: 0.03, tiers: [{ inputTokensAbove: 272000, inputPer1k: 0.01, outputPer1k: 0.045 }], updatedAt: "2026-07-11" },
72
+ { id: "gpt-5.6-terra", inputPer1k: 0.0025, outputPer1k: 0.015, tiers: [{ inputTokensAbove: 272000, inputPer1k: 0.005, outputPer1k: 0.0225 }], updatedAt: "2026-07-11" },
73
+ { id: "gpt-5.6-luna", inputPer1k: 0.001, outputPer1k: 0.006, tiers: [{ inputTokensAbove: 272000, inputPer1k: 0.002, outputPer1k: 0.009 }], updatedAt: "2026-07-11" },
67
74
 
68
75
  // Google
69
76
  { id: "gemini-2.0-flash", inputPer1k: 0.0001, outputPer1k: 0.0004, updatedAt: "2025-03-15" },
@@ -80,7 +87,11 @@ export const BUNDLED_COST_TABLE: ModelCostEntry[] = [
80
87
  export function lookupModelCost(modelId: string): ModelCostEntry | undefined {
81
88
  const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
82
89
  return BUNDLED_COST_TABLE.find(e => e.id === bareId)
83
- ?? BUNDLED_COST_TABLE.find(e => bareId.includes(e.id) || e.id.includes(bareId));
90
+ ?? BUNDLED_COST_TABLE.find(e =>
91
+ bareId.startsWith(`${e.id}-`) ||
92
+ bareId.startsWith(`${e.id}:`) ||
93
+ bareId.startsWith(`${e.id}@`)
94
+ );
84
95
  }
85
96
 
86
97
  /**
@@ -111,7 +111,9 @@ export const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
111
111
  "gpt-5.3-codex": "heavy",
112
112
  "gpt-5.4": "heavy",
113
113
  "gpt-5.5": "heavy",
114
- "gpt-5.6": "heavy",
114
+ "gpt-5.6-sol": "heavy",
115
+ "gpt-5.6-terra": "heavy",
116
+ "gpt-5.6-luna": "heavy",
115
117
  "o1": "heavy",
116
118
  "o3": "heavy",
117
119
  "o4-mini": "heavy",
@@ -151,7 +153,9 @@ const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
151
153
  "gpt-5.3-codex-spark": 0.0003,
152
154
  "gpt-5.4": 0.005,
153
155
  "gpt-5.5": 0.005,
154
- "gpt-5.6": 0.005,
156
+ "gpt-5.6-sol": 0.005,
157
+ "gpt-5.6-terra": 0.0025,
158
+ "gpt-5.6-luna": 0.001,
155
159
  "o4-mini": 0.005,
156
160
  "o4-mini-deep-research": 0.005,
157
161
  "gemini-2.0-flash": 0.0001,
@@ -202,9 +206,10 @@ export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
202
206
  // OpenAI's 2026-04-23 published eval deltas across coding, tool use, and long context.
203
207
  // Source: https://openai.com/index/introducing-gpt-5-5/
204
208
  "gpt-5.5": { coding: 96, debugging: 93, research: 89, reasoning: 95, speed: 42, longContext: 90, instruction: 93 },
205
- // GPT-5.6 profile is provisional: a small uniform step over gpt-5.5, to be
206
- // recalibrated once OpenAI publishes official eval results.
207
- "gpt-5.6": { coding: 97, debugging: 94, research: 90, reasoning: 96, speed: 42, longContext: 91, instruction: 94 },
209
+ // GPT-5.6 profiles are provisional until official eval results are available.
210
+ "gpt-5.6-sol": { coding: 97, debugging: 94, research: 90, reasoning: 96, speed: 42, longContext: 91, instruction: 94 },
211
+ "gpt-5.6-terra": { coding: 96, debugging: 93, research: 89, reasoning: 95, speed: 48, longContext: 91, instruction: 93 },
212
+ "gpt-5.6-luna": { coding: 92, debugging: 88, research: 84, reasoning: 91, speed: 72, longContext: 91, instruction: 90 },
208
213
 
209
214
  // ── OpenAI o-series (reasoning-first) ──────────────────────────────────────
210
215
  "o1": { coding: 78, debugging: 82, research: 78, reasoning: 90, speed: 20, longContext: 65, instruction: 82 },
@@ -6,6 +6,8 @@
6
6
  * preference validation before any configured planning allowlist takes effect.
7
7
  */
8
8
 
9
+ import type { PlanningSubagentRegistryConfig } from "./preferences-types.js";
10
+
9
11
  const PLANNING_DISPATCH_AGENT_REGISTRY = {
10
12
  mnemo: { readOnlySpecialist: true },
11
13
  scout: { readOnlySpecialist: true },
@@ -21,11 +23,23 @@ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set<string>(
21
23
  .map(([agentId]) => agentId),
22
24
  );
23
25
 
24
- export function isReadOnlyPlanningDispatchAgent(agentId: string): boolean {
26
+ function configuredPlanningDispatchAgents(registry?: PlanningSubagentRegistryConfig): string[] {
27
+ return Object.entries(registry ?? {})
28
+ .filter(([, metadata]) => metadata.read_only_specialist === true)
29
+ .map(([agentId]) => agentId);
30
+ }
31
+
32
+ export function isReadOnlyPlanningDispatchAgent(
33
+ agentId: string,
34
+ registry?: PlanningSubagentRegistryConfig,
35
+ ): boolean {
25
36
  const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId as keyof typeof PLANNING_DISPATCH_AGENT_REGISTRY];
26
- return metadata?.readOnlySpecialist === true;
37
+ return metadata?.readOnlySpecialist === true || registry?.[agentId]?.read_only_specialist === true;
27
38
  }
28
39
 
29
- export function allowedPlanningDispatchAgentsList(): string {
30
- return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
40
+ export function allowedPlanningDispatchAgentsList(registry?: PlanningSubagentRegistryConfig): string {
41
+ return Array.from(new Set([
42
+ ...ALLOWED_PLANNING_DISPATCH_AGENTS,
43
+ ...configuredPlanningDispatchAgents(registry),
44
+ ])).join(", ");
31
45
  }
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { getProviders } from "@gsd/pi-ai";
12
13
  import { gsdHome } from "./gsd-home.js";
13
14
  import type { DynamicRoutingConfig } from "./model-router.js";
14
15
  import { canonicalModelForTier, defaultRoutingConfig, resolveModelForTier } from "./model-router.js";
@@ -31,6 +32,33 @@ import { getUnitPhaseChain } from "./unit-registry.js";
31
32
  // Re-export types so existing consumers of ./preferences-models.js keep working
32
33
  export type { GSDPhaseModelConfig, GSDModelConfig, GSDModelConfigV2, ResolvedModelConfig } from "./preferences-types.js";
33
34
 
35
+ type ProviderModelRegistry = {
36
+ getAll?: () => ReadonlyArray<{ provider: string }>;
37
+ getAvailable?: () => ReadonlyArray<{ provider: string }>;
38
+ getAllWithDiscovered?: () => ReadonlyArray<{ provider: string }>;
39
+ };
40
+
41
+ /**
42
+ * First-party providers shipped as bundled extensions that delegate to a
43
+ * vendor CLI (`authMode: "externalCli"`). They are registered into the live
44
+ * model registry at runtime, so they are absent from the generated `@gsd/pi-ai`
45
+ * catalog (`getProviders()`), but they are NOT custom: `PREFERENCES.md` can
46
+ * route to them, so they must defer to `PREFERENCES.md` like any other built-in
47
+ * provider. Without this exclusion the registry check below would classify the
48
+ * default `claude-code` provider (and the other CLI providers) as custom and
49
+ * silently skip `PREFERENCES.md` in auto-mode (#801, extends #4122).
50
+ *
51
+ * Kept in sync with the `registerProvider(...)` calls in the bundled CLI
52
+ * extensions (claude-code-cli, cursor-cli, google-cli). Names are lowercase for
53
+ * direct comparison against normalized provider ids.
54
+ */
55
+ const BUILTIN_EXTENSION_PROVIDERS: ReadonlySet<string> = new Set([
56
+ "claude-code",
57
+ "cursor-agent",
58
+ "google-gemini-cli",
59
+ "google-antigravity",
60
+ ]);
61
+
34
62
  /**
35
63
  * Resolve which model ID to use for a given auto-mode unit type.
36
64
  * Returns undefined if no model preference is set for this unit type.
@@ -321,9 +349,8 @@ export function resolveDefaultSessionModel(
321
349
  }
322
350
 
323
351
  /**
324
- * Returns true if `provider` is defined as a custom provider in the user's
325
- * `~/.gsd/agent/models.json` (Ollama, vLLM, LM Studio, OpenAI-compatible
326
- * proxies, etc.).
352
+ * Returns true if `provider` is a custom provider. Custom providers may be
353
+ * declared in `models.json` or registered dynamically by a Pi extension.
327
354
  *
328
355
  * Used by auto-mode bootstrap to decide whether the session model
329
356
  * (set via `/gsd model`) should override `PREFERENCES.md`. Custom providers
@@ -332,13 +359,16 @@ export function resolveDefaultSessionModel(
332
359
  * priority — otherwise auto-mode tries to start the built-in provider from
333
360
  * PREFERENCES.md and fails with "Not logged in · Please run /login" (#4122).
334
361
  *
335
- * Reads models.json directly with a lightweight JSON parse to avoid
336
- * pulling in the full model-registry at this call site. Falls back to
337
- * `~/.pi/agent/models.json` for parity with `resolveModelsJsonPath()`.
338
- * Any read or parse error yields `false` (treat as not-custom) so a
339
- * malformed models.json never breaks the session bootstrap.
362
+ * Reads models.json directly with a lightweight JSON parse, then checks the
363
+ * live model registry for extension-registered providers that are not in the
364
+ * generated built-in provider catalog (`getProviders()`) and not one of the
365
+ * bundled external-CLI providers (`BUILTIN_EXTENSION_PROVIDERS`, e.g.
366
+ * `claude-code`), which register at runtime but are still built-in. Falls back
367
+ * to `~/.pi/agent/models.json` for parity with `resolveModelsJsonPath()`. Any
368
+ * read, parse, or registry error yields `false` (treat as not-custom) so
369
+ * bootstrap stays non-fatal.
340
370
  */
341
- export function isCustomProvider(provider: string | undefined): boolean {
371
+ export function isCustomProvider(provider: string | undefined, registry?: ProviderModelRegistry): boolean {
342
372
  if (!provider) return false;
343
373
  const candidates = [
344
374
  join(gsdHome(), "agent", "models.json"),
@@ -356,7 +386,21 @@ export function isCustomProvider(provider: string | undefined): boolean {
356
386
  // Ignore — malformed models.json must not break bootstrap.
357
387
  }
358
388
  }
359
- return false;
389
+ try {
390
+ const normalizedProvider = provider.trim().toLowerCase();
391
+ if (!normalizedProvider) return false;
392
+ if (BUILTIN_EXTENSION_PROVIDERS.has(normalizedProvider)) return false;
393
+ const builtInProviders = new Set(getProviders().map((p) => p.toLowerCase()));
394
+ if (builtInProviders.has(normalizedProvider)) return false;
395
+ const registryModels =
396
+ registry?.getAllWithDiscovered?.()
397
+ ?? registry?.getAll?.()
398
+ ?? registry?.getAvailable?.()
399
+ ?? [];
400
+ return registryModels.some((m) => m.provider.toLowerCase() === normalizedProvider);
401
+ } catch {
402
+ return false;
403
+ }
360
404
  }
361
405
 
362
406
  /**
@@ -141,6 +141,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
141
141
  "git",
142
142
  "post_unit_hooks",
143
143
  "pre_dispatch_hooks",
144
+ "planning_subagent_registry",
144
145
  "planning_subagents",
145
146
  "dynamic_routing",
146
147
  "disabled_model_providers",
@@ -232,6 +233,12 @@ export type PlanningSubagentsConfig = Partial<
232
233
  Record<ConfigurablePlanningSubagentUnit, PlanningSubagentUnitConfig>
233
234
  >;
234
235
 
236
+ export interface PlanningSubagentRegistryEntry {
237
+ read_only_specialist?: boolean;
238
+ }
239
+
240
+ export type PlanningSubagentRegistryConfig = Record<string, PlanningSubagentRegistryEntry>;
241
+
235
242
 
236
243
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
237
244
 
@@ -245,10 +252,10 @@ export interface GSDSkillRule {
245
252
  /**
246
253
  * Reasoning effort levels. Mirrors `ThinkingLevel` in `@gsd/pi-agent-core`;
247
254
  * declared locally so preference parsing/validation does not import the agent
248
- * core package. `xhigh` is only honored by models whose `thinkingLevelMap`
255
+ * core package. `xhigh` and `max` are only honored by models whose `thinkingLevelMap`
249
256
  * advertises it — unsupported levels are clamped at dispatch (ADR-026).
250
257
  */
251
- export type GSDThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
258
+ export type GSDThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
252
259
 
253
260
  /** The nine model-routing phase buckets. */
254
261
  export const GSD_MODEL_PHASE_KEYS = [
@@ -491,6 +498,7 @@ export interface GSDPreferences {
491
498
  git?: GitPreferences;
492
499
  post_unit_hooks?: PostUnitHookConfig[];
493
500
  pre_dispatch_hooks?: PreDispatchHookConfig[];
501
+ planning_subagent_registry?: PlanningSubagentRegistryConfig;
494
502
  planning_subagents?: PlanningSubagentsConfig;
495
503
  dynamic_routing?: DynamicRoutingConfig;
496
504
  /** Provider IDs to exclude from /model and automatic model routing while leaving tool auth intact. */
@@ -28,8 +28,8 @@ import {
28
28
  type GSDThinkingLevel,
29
29
  } from "./preferences-types.js";
30
30
  import {
31
- ALLOWED_PLANNING_DISPATCH_AGENTS,
32
31
  allowedPlanningDispatchAgentsList,
32
+ isReadOnlyPlanningDispatchAgent,
33
33
  } from "./planning-subagent-registry.js";
34
34
 
35
35
  const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality", "burn-max"]);
@@ -40,6 +40,7 @@ const VALID_THINKING_LEVELS = new Set<GSDThinkingLevel>([
40
40
  "medium",
41
41
  "high",
42
42
  "xhigh",
43
+ "max",
43
44
  ]);
44
45
  const KNOWN_MODEL_PHASE_KEYS = new Set<string>(GSD_MODEL_PHASE_KEYS);
45
46
  const VALID_UOK_TURN_ACTIONS = new Set<"commit" | "snapshot" | "status-only">([
@@ -93,7 +94,7 @@ function sanitizeModelField(
93
94
  } else {
94
95
  warnings?.push(
95
96
  `${label} thinking "${String(obj.thinking)}" is not a valid thinking level ` +
96
- `(off, minimal, low, medium, high, xhigh) — ignored`,
97
+ `(off, minimal, low, medium, high, xhigh, max) — ignored`,
97
98
  );
98
99
  }
99
100
  }
@@ -486,7 +487,7 @@ export function validatePreferences(preferences: GSDPreferences): {
486
487
  if (level !== undefined && !VALID_THINKING_LEVELS.has(level as GSDThinkingLevel)) {
487
488
  warnings.push(
488
489
  `models.${phase}.thinking "${String(level)}" is not a valid thinking level ` +
489
- `(off, minimal, low, medium, high, xhigh) — ignored`,
490
+ `(off, minimal, low, medium, high, xhigh, max) — ignored`,
490
491
  );
491
492
  const { thinking: _ignored, ...rest } = entry as Record<string, unknown>;
492
493
  // If stripping the bad thinking leaves no usable model, drop the
@@ -521,7 +522,7 @@ export function validatePreferences(preferences: GSDPreferences): {
521
522
  if (!VALID_THINKING_LEVELS.has(level as GSDThinkingLevel)) {
522
523
  warnings.push(
523
524
  `thinking.${phase} "${String(level)}" is not a valid thinking level ` +
524
- `(off, minimal, low, medium, high, xhigh) — ignored`,
525
+ `(off, minimal, low, medium, high, xhigh, max) — ignored`,
525
526
  );
526
527
  continue;
527
528
  }
@@ -752,6 +753,46 @@ export function validatePreferences(preferences: GSDPreferences): {
752
753
  }
753
754
  }
754
755
 
756
+ // ─── Planning Subagent Registry ──────────────────────────────────────────
757
+ if (preferences.planning_subagent_registry !== undefined) {
758
+ if (
759
+ !preferences.planning_subagent_registry ||
760
+ typeof preferences.planning_subagent_registry !== "object" ||
761
+ Array.isArray(preferences.planning_subagent_registry)
762
+ ) {
763
+ errors.push("planning_subagent_registry must be an object keyed by agent id");
764
+ } else {
765
+ const validRegistry: NonNullable<GSDPreferences["planning_subagent_registry"]> = {};
766
+
767
+ for (const [rawAgentId, rawConfig] of Object.entries(preferences.planning_subagent_registry as Record<string, unknown>)) {
768
+ const agentId = rawAgentId.trim();
769
+ if (!agentId) {
770
+ errors.push("planning_subagent_registry includes an empty agent id");
771
+ continue;
772
+ }
773
+ if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
774
+ errors.push(`planning_subagent_registry.${agentId} must be an object with read_only_specialist: true`);
775
+ continue;
776
+ }
777
+
778
+ const readOnlySpecialist = (rawConfig as Record<string, unknown>).read_only_specialist;
779
+ if (readOnlySpecialist !== true) {
780
+ errors.push(
781
+ `planning_subagent_registry.${agentId}.read_only_specialist must be true to classify ` +
782
+ `"${agentId}" as a read-only planning specialist`,
783
+ );
784
+ continue;
785
+ }
786
+
787
+ validRegistry[agentId] = { read_only_specialist: true };
788
+ }
789
+
790
+ if (Object.keys(validRegistry).length > 0) {
791
+ validated.planning_subagent_registry = validRegistry;
792
+ }
793
+ }
794
+ }
795
+
755
796
  // ─── Planning Subagents ─────────────────────────────────────────────────
756
797
  if (preferences.planning_subagents !== undefined) {
757
798
  if (
@@ -785,10 +826,12 @@ export function validatePreferences(preferences: GSDPreferences): {
785
826
 
786
827
  const sanitizedAllowed: string[] = [];
787
828
  for (const agentId of allowed) {
788
- if (!ALLOWED_PLANNING_DISPATCH_AGENTS.has(agentId)) {
829
+ if (!isReadOnlyPlanningDispatchAgent(agentId, validated.planning_subagent_registry)) {
789
830
  errors.push(
790
831
  `planning_subagents.${unitType}.allowed includes "${agentId}", which is not classified as a ` +
791
- `read-only planning specialist. Allowed agents: ${allowedPlanningDispatchAgentsList()}`,
832
+ `read-only planning specialist. Allowed agents: ` +
833
+ `${allowedPlanningDispatchAgentsList(validated.planning_subagent_registry)}. ` +
834
+ `For custom planning agents, add planning_subagent_registry.${agentId}.read_only_specialist: true`,
792
835
  );
793
836
  continue;
794
837
  }
@@ -817,6 +817,10 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
817
817
  : undefined,
818
818
  post_unit_hooks: mergePostUnitHooks(base.post_unit_hooks, override.post_unit_hooks),
819
819
  pre_dispatch_hooks: mergePreDispatchHooks(base.pre_dispatch_hooks, override.pre_dispatch_hooks),
820
+ planning_subagent_registry: mergePlanningSubagentRegistry(
821
+ base.planning_subagent_registry,
822
+ override.planning_subagent_registry,
823
+ ),
820
824
  planning_subagents: mergePlanningSubagents(base.planning_subagents, override.planning_subagents),
821
825
  dynamic_routing: (base.dynamic_routing || override.dynamic_routing)
822
826
  ? { ...(base.dynamic_routing ?? {}), ...(override.dynamic_routing ?? {}) } as DynamicRoutingConfig
@@ -968,6 +972,15 @@ function mergePlanningSubagents(
968
972
  return Object.keys(merged).length > 0 ? merged : undefined;
969
973
  }
970
974
 
975
+ function mergePlanningSubagentRegistry(
976
+ base?: GSDPreferences["planning_subagent_registry"],
977
+ override?: GSDPreferences["planning_subagent_registry"],
978
+ ): GSDPreferences["planning_subagent_registry"] | undefined {
979
+ if (!base && !override) return undefined;
980
+ const merged = { ...(base ?? {}), ...(override ?? {}) };
981
+ return Object.keys(merged).length > 0 ? merged : undefined;
982
+ }
983
+
971
984
  // ─── System Prompt Rendering ──────────────────────────────────────────────────
972
985
 
973
986
  export function renderPreferencesForSystemPrompt(
@@ -92,6 +92,9 @@ remote_questions:
92
92
  uat_dispatch:
93
93
  post_unit_hooks: []
94
94
  pre_dispatch_hooks: []
95
+ # planning_subagent_registry:
96
+ # my-custom-planner:
97
+ # read_only_specialist: true
95
98
  # planning_subagents:
96
99
  # plan-milestone:
97
100
  # allowed: [scout, planner, security]