@event4u/agent-config 14.20.0 → 14.22.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 (242) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +441 -0
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/agents/templates/.ai-council.yml.example +10 -0
  8. package/dist/agent-src/commands/analyze/inbox.md +381 -6
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +251 -34
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  16. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  17. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  18. package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
  19. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  20. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  21. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  22. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  23. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  24. package/dist/agent-src/rules/token-efficiency.md +26 -3
  25. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  26. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  27. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  28. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  29. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  30. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  31. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  32. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  33. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  34. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  35. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  36. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  38. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  39. package/dist/agent-src/templates/agent-settings.md +0 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/features.md +5 -0
  42. package/dist/agent-src/templates/roadmaps.md +15 -0
  43. package/dist/cli-delegate/_cli_entry.js +8 -0
  44. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  45. package/dist/cli-delegate/archival_sweep.js +8 -0
  46. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  47. package/dist/cli-delegate/check_update_banner.js +923 -0
  48. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  49. package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
  50. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  51. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  52. package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-BIXEDQPZ.js} +7 -1
  53. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  54. package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
  55. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  56. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  57. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  58. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  59. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  60. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  61. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  62. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  63. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  64. package/dist/cli-delegate/cmd_conformance.js +19 -5
  65. package/dist/cli-delegate/cmd_doctor.js +3 -3
  66. package/dist/cli-delegate/cmd_export.js +2 -2
  67. package/dist/cli-delegate/cmd_handoff.js +27 -3
  68. package/dist/cli-delegate/cmd_prune.js +2 -2
  69. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  70. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  71. package/dist/cli-delegate/cmd_sync.js +2 -2
  72. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  73. package/dist/cli-delegate/cmd_validate.js +2 -2
  74. package/dist/cli-delegate/dashboard_mode.js +16 -0
  75. package/dist/cli-delegate/gate_budget.js +24 -0
  76. package/dist/cli-delegate/gate_execute.js +25 -0
  77. package/dist/cli-delegate/guarded_baseline.js +18 -0
  78. package/dist/cli-delegate/resume_probe.js +22 -0
  79. package/dist/cli-delegate/roadmap_gates.js +638 -0
  80. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  81. package/dist/cli-delegate/stub_queue.js +47 -0
  82. package/dist/cli-delegate/stubs_due.js +38 -0
  83. package/dist/cli-delegate/ticket_bundles.js +59 -0
  84. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  85. package/dist/discovery/deprecation-report.md +1 -1
  86. package/dist/discovery/discovery-manifest.json +60 -29
  87. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  88. package/dist/discovery/discovery-manifest.summary.md +3 -3
  89. package/dist/discovery/dormancy-report.md +1 -1
  90. package/dist/discovery/orphan-report.md +1 -1
  91. package/dist/discovery/packs.json +7 -6
  92. package/dist/discovery/trust-report.md +2 -2
  93. package/dist/discovery/workspaces.json +6 -5
  94. package/dist/hooks/dispatch.js +1522 -1008
  95. package/dist/install/claudeLayerCarriage.js +237 -0
  96. package/dist/install/claudePathsPlan.js +79 -0
  97. package/dist/install/hostLayerFingerprint.js +1 -1
  98. package/dist/install/install.mjs +10 -1
  99. package/dist/install/partitionEligibility.js +231 -117
  100. package/dist/install/ruleLayerPartition.js +10 -7
  101. package/dist/mcp/registry-manifest.json +3 -3
  102. package/dist/mcp/server.json +2 -2
  103. package/dist/mcp/server.mjs +39376 -38902
  104. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  105. package/dist/scripts/ai_council/seat_policy.js +106 -0
  106. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  107. package/dist/scripts/condense.js +32 -25
  108. package/docs/MIGRATION.md +49 -1
  109. package/docs/contracts/settings-classes.md +1 -1
  110. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  111. package/llms.txt +14 -14
  112. package/package.json +7 -4
  113. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  114. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  115. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  116. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  117. package/src/agent-src/scripts/stubs_due.ts +4 -18
  118. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  119. package/src/config/continuity-surface.json +509 -0
  120. package/src/config/estate-count-budget.json +1 -1
  121. package/src/config/gate-coverage.yml +29 -8
  122. package/src/config/gate-violation-baselines.json +8 -8
  123. package/src/config/gitignore-block.txt +19 -0
  124. package/src/config/host-injection-effect.json +73 -0
  125. package/src/config/packed-binary-manifest.json +33 -0
  126. package/src/config/preamble-payload-budget.json +112 -105
  127. package/src/config/publish-surface.json +4 -2
  128. package/src/config/release-gate-locality.yml +4 -3
  129. package/src/install/claudeLayerCarriage.ts +273 -0
  130. package/src/install/claudePathsPlan.ts +76 -0
  131. package/src/install/hostLayerFingerprint.ts +1 -1
  132. package/src/install/partitionEligibility.ts +261 -127
  133. package/src/install/ruleLayerPartition.ts +19 -23
  134. package/src/scripts/_dispatch.bash +77 -27
  135. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  136. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  137. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  138. package/src/scripts/_lib/continuity_surface.ts +296 -0
  139. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  140. package/src/scripts/_lib/hook_settings.ts +1 -1
  141. package/src/scripts/_lib/host_capability.ts +38 -1
  142. package/src/scripts/_lib/injection_effect.ts +151 -0
  143. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  144. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  145. package/src/scripts/_lib/pin_resolver.ts +13 -0
  146. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  147. package/src/scripts/_lib/release_highlights.ts +19 -211
  148. package/src/scripts/_lib/release_material.ts +27 -77
  149. package/src/scripts/_lib/retired_status.ts +74 -0
  150. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  151. package/src/scripts/_lib/structured_ask.ts +117 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  153. package/src/scripts/_lib/tty_prompt.ts +76 -0
  154. package/src/scripts/ai_council/api_transport.ts +168 -0
  155. package/src/scripts/ai_council/clients.ts +58 -63
  156. package/src/scripts/ai_council/config.ts +13 -0
  157. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  158. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  159. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  160. package/src/scripts/ai_council/prompts.ts +9 -0
  161. package/src/scripts/ai_council/seat_policy.ts +137 -0
  162. package/src/scripts/ai_council/status_surface.ts +79 -0
  163. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  164. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  165. package/src/scripts/ai_tells_rules.ts +446 -5
  166. package/src/scripts/ask_block_census.ts +499 -0
  167. package/src/scripts/bench_humanizer_eval.ts +264 -40
  168. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  169. package/src/scripts/capture_skill_catalogue.ts +24 -11
  170. package/src/scripts/check_branch_work_committed.ts +234 -0
  171. package/src/scripts/check_candidate_lines.ts +499 -0
  172. package/src/scripts/check_continuity_surface.ts +475 -0
  173. package/src/scripts/check_dependency_floors.ts +13 -6
  174. package/src/scripts/check_estate_count.ts +40 -79
  175. package/src/scripts/check_pack_size.ts +263 -24
  176. package/src/scripts/check_release_highlights.ts +0 -157
  177. package/src/scripts/check_roadmap_trackable.ts +19 -16
  178. package/src/scripts/check_rule_layer_partition.ts +54 -34
  179. package/src/scripts/check_single_delivery.ts +97 -31
  180. package/src/scripts/check_update_banner.ts +13 -0
  181. package/src/scripts/code_graph/build.ts +138 -15
  182. package/src/scripts/code_graph/cli.ts +76 -8
  183. package/src/scripts/code_graph/extract.ts +11 -0
  184. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  185. package/src/scripts/code_graph/loader.ts +88 -14
  186. package/src/scripts/code_graph/query.ts +121 -11
  187. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  188. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  189. package/src/scripts/code_graph/types.ts +66 -1
  190. package/src/scripts/code_graph/validate.ts +23 -0
  191. package/src/scripts/condense.ts +34 -22
  192. package/src/scripts/corpus_manifest.ts +5 -5
  193. package/src/scripts/council_cli.ts +29 -39
  194. package/src/scripts/count_intent_disagreement.ts +426 -0
  195. package/src/scripts/detect_ai_tells.ts +142 -20
  196. package/src/scripts/handoff_context_hook.ts +79 -0
  197. package/src/scripts/harvest_reference_tokens.ts +698 -0
  198. package/src/scripts/hook_effect_doctor.ts +82 -1
  199. package/src/scripts/hook_manifest.json +1 -1
  200. package/src/scripts/hook_manifest.yaml +94 -12
  201. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  202. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  203. package/src/scripts/hooks/concern_registry.ts +6 -2
  204. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  205. package/src/scripts/hooks/host_semantics.ts +1 -1
  206. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  207. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  208. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  209. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  210. package/src/scripts/inbox_source_census.ts +322 -0
  211. package/src/scripts/install-hooks.sh +134 -0
  212. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  213. package/src/scripts/lint_plan_risk_register.ts +13 -17
  214. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  215. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  216. package/src/scripts/mcp_server/tools.ts +2 -56
  217. package/src/scripts/media/lib/adapter-contract.md +7 -2
  218. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  219. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  220. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  221. package/src/scripts/migrate_command_suggestions.ts +1 -1
  222. package/src/scripts/probe_unblocked_ask.ts +135 -2
  223. package/src/scripts/rdp_candidates_delta.ts +276 -0
  224. package/src/scripts/rdp_quality_eval.ts +217 -19
  225. package/src/scripts/release.ts +26 -71
  226. package/src/scripts/release_drill.ts +6 -10
  227. package/src/scripts/release_env.ts +1 -0
  228. package/src/scripts/release_mix_taxonomy.json +3 -2
  229. package/src/scripts/release_publication.ts +34 -29
  230. package/src/scripts/report_host_injection_effect.ts +189 -0
  231. package/src/scripts/report_layer_overlap.ts +41 -0
  232. package/src/scripts/report_skill_menu_census.ts +341 -0
  233. package/src/scripts/self_review_gate.ts +409 -15
  234. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  235. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  236. package/src/vendor/grammars/README.md +61 -0
  237. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  238. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  239. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  240. package/src/scripts/_lib/carrier_status.ts +0 -118
  241. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  242. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -143,10 +143,68 @@ export const CAPSULE_SCHEMA_VERSION = 4;
143
143
  */
144
144
  export const ACCEPTED_CAPSULE_VERSIONS: readonly number[] = [3, 4];
145
145
 
146
- /** The two CHECKPOINT variants sharing this schema. */
147
- export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
146
+ /** The three CHECKPOINT variants sharing this schema. */
147
+ export const CAPSULE_VARIANTS = ['worker', 'main_session', 'continuity_record'] as const;
148
148
  export type CapsuleVariant = (typeof CAPSULE_VARIANTS)[number];
149
149
 
150
+ /**
151
+ * `continuity_record` — the DETERMINISTIC main-session variant.
152
+ *
153
+ * Why a variant and not a relaxed `main_session`. `main_session` requires
154
+ * `failed_approaches` and `successful_approaches` with at least one entry each,
155
+ * and requires the explicit token `none` when nothing failed or worked — the
156
+ * whole point of those fields being that silence and "nothing was abandoned"
157
+ * stay distinguishable. A writer that runs without model spend cannot know
158
+ * either. It could only write `none`, which asserts a judgement nobody made.
159
+ *
160
+ * The AI council of 2026-09-07 ruled exactly this: add a variant rather than
161
+ * stuffing absence markers into `main_session`, because explicit absence
162
+ * markers "misrepresent unavailable model judgments as values". This constant
163
+ * set is that ruling in code.
164
+ *
165
+ * The fields below are FORBIDDEN on this variant rather than optional, and the
166
+ * distinction is load-bearing. Optional would leave the door open for a later
167
+ * writer to fill one with a guess and for a reader to be unable to tell a
168
+ * derived record from a judged one. Forbidden makes the variant's meaning
169
+ * checkable: everything a `continuity_record` carries was computed from disk.
170
+ *
171
+ * Nothing here is invented — every name is an existing key of this same schema.
172
+ * The council's constraint was explicit: the required fields "must be derived
173
+ * from the existing validated envelope contract; this ruling does not authorize
174
+ * inventing parallel identity or directive fields."
175
+ *
176
+ * Version compatibility is unchanged and envelope-wide: a `continuity_record`
177
+ * declares the same `capsule_version`, and a reader that does not know the
178
+ * variant refuses it by name (see `validateRecycleEnvelope`) rather than
179
+ * guessing at its required set.
180
+ */
181
+ export const CONTINUITY_RECORD_FORBIDDEN_KEYS: ReadonlySet<string> = new Set([
182
+ 'decisions',
183
+ 'constraints',
184
+ 'do_not_touch',
185
+ 'assumptions',
186
+ 'failed_approaches',
187
+ 'successful_approaches',
188
+ 'open_questions',
189
+ 'not_carried_forward',
190
+ 'status_summary',
191
+ 'suggested_skills',
192
+ 'open_worker_envelopes',
193
+ ]);
194
+
195
+ /** Required on a `continuity_record` — each one computable from on-disk state. */
196
+ export const CONTINUITY_RECORD_REQUIRED_KEYS: readonly string[] = [
197
+ 'capsule_version',
198
+ 'variant',
199
+ 'summary',
200
+ 'task',
201
+ 'workspace',
202
+ 'written_at',
203
+ 'acceptance_criteria',
204
+ 'remaining',
205
+ 'predecessor',
206
+ ];
207
+
150
208
  /** A single-line token no longer than `max` chars. Rejects any body-shaped value. */
151
209
  function isShortLine(value: unknown, max: number): value is string {
152
210
  return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
@@ -757,6 +815,31 @@ export function validateRecycleEnvelope(input: unknown): string[] {
757
815
  }
758
816
  }
759
817
 
818
+ // VARIANT FIRST, then version. The order is the contract, not a style
819
+ // choice. Required fields are variant-specific, so a reader that does not
820
+ // recognise the variant cannot know what to demand — and a reader that
821
+ // reports twenty field violations against a variant it never understood has
822
+ // told the operator nothing useful and, worse, has implied the record was
823
+ // malformed when the truth is that the reader is old. One refusal naming
824
+ // the variant is the honest output, and it is what makes an old reader
825
+ // meeting a new record a legible event rather than a pile of noise.
826
+ const variant = e['variant'];
827
+ if (typeof variant !== 'string' || !(CAPSULE_VARIANTS as readonly string[]).includes(variant)) {
828
+ return [
829
+ `unknown variant ${JSON.stringify(variant)} — this reader knows ` +
830
+ `${CAPSULE_VARIANTS.join(' | ')}. A reader that does not know a variant must not ` +
831
+ 'guess at its required fields, so no further field checks were run.',
832
+ ];
833
+ }
834
+ if (variant === 'worker') {
835
+ return [
836
+ "variant 'worker' is a subagent generation handover and is validated by the worker " +
837
+ 'path, not by the main-session recycle validator — routing it here would apply ' +
838
+ 'the wrong required set to a well-formed record.',
839
+ ];
840
+ }
841
+ const isDerived = variant === 'continuity_record';
842
+
760
843
  const version = e['capsule_version'];
761
844
  if (typeof version !== 'number' || !ACCEPTED_CAPSULE_VERSIONS.includes(version)) {
762
845
  errors.push(`capsule_version must be one of ${ACCEPTED_CAPSULE_VERSIONS.join(' | ')}`);
@@ -766,9 +849,35 @@ export function validateRecycleEnvelope(input: unknown): string[] {
766
849
  // Demanding the new fields of an already-written record would be a
767
850
  // retroactive requirement, which is the one thing the recorded schema lock
768
851
  // forbids ("add fields or variants, never repurpose or remove one").
769
- const atV4 = version === 4;
770
- if (e['variant'] !== 'main_session') {
771
- errors.push("variant must be 'main_session'");
852
+ //
853
+ // A `continuity_record` is exempt from the two v4 judgement fields for the
854
+ // opposite reason: it is forbidden from carrying them at all (below), so
855
+ // requiring them would make the variant unsatisfiable.
856
+ const atV4 = version === 4 && !isDerived;
857
+
858
+ if (isDerived) {
859
+ // Forbidden, not merely absent: a derived record that carries a
860
+ // judgement field is either a `main_session` mislabelled, or a
861
+ // deterministic writer that invented a value. Both are worth refusing
862
+ // loudly, and neither is distinguishable after the fact.
863
+ for (const key of Object.keys(e)) {
864
+ if (CONTINUITY_RECORD_FORBIDDEN_KEYS.has(key)) {
865
+ errors.push(
866
+ `field "${key}" is forbidden on variant 'continuity_record' — it carries a ` +
867
+ 'model judgement, and a record written without model spend cannot have made ' +
868
+ "one. Use variant 'main_session' for a judged record.",
869
+ );
870
+ }
871
+ }
872
+ for (const key of CONTINUITY_RECORD_REQUIRED_KEYS) {
873
+ if (e[key] === undefined) {
874
+ errors.push(
875
+ `field "${key}" is required on variant 'continuity_record' — it is computable ` +
876
+ 'from on-disk state, so its absence is a writer defect rather than an ' +
877
+ 'unavailable judgement.',
878
+ );
879
+ }
880
+ }
772
881
  }
773
882
  if (!isShortLine(e['summary'], MAX_LINE_CHARS)) {
774
883
  errors.push(`summary must be a single line of 1–${MAX_LINE_CHARS} chars`);
@@ -789,7 +898,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
789
898
  errors.push('acceptance_criteria must carry at least one entry — the successor cannot know "done" without it');
790
899
  }
791
900
  checkList(errors, 'remaining', e['remaining'], MAX_LINE_CHARS, true);
792
- checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, true);
901
+ checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, !isDerived);
793
902
  checkList(errors, 'decisions', e['decisions'], MAX_LINE_CHARS, false);
794
903
  errors.push(...decisionTagErrors(e['decisions']));
795
904
  checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
@@ -809,7 +918,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
809
918
 
810
919
  // Required with ≥ 1 entry (Phase 2.3): a session that abandoned nothing
811
920
  // writes `none`. Absence must never be readable as "nothing failed".
812
- checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, true);
921
+ checkList(errors, 'failed_approaches', e['failed_approaches'], MAX_LINE_CHARS, !isDerived);
813
922
  if (Array.isArray(e['failed_approaches']) && e['failed_approaches'].length === 0) {
814
923
  errors.push(
815
924
  'failed_approaches must carry at least one entry — write "none" explicitly, so ' +
@@ -835,7 +944,7 @@ export function validateRecycleEnvelope(input: unknown): string[] {
835
944
  // is a reader waiting for something that will not arrive.
836
945
  const predecessor = e['predecessor'];
837
946
  if (predecessor === undefined) {
838
- if (atV4) {
947
+ if (atV4 || isDerived) {
839
948
  errors.push(
840
949
  'predecessor is required — name the session this one continues, or write "none" ' +
841
950
  'explicitly for the first session in a workspace',
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Synchronous terminal prompting for the release pipeline.
3
+ *
4
+ * Its own module because `release.ts` sits 371 lines over the 1500-line source
5
+ * ratchet, where every added line is one unit of excess — and because a SECOND
6
+ * caller now needs these: `guard_release_curation` in `release_publication.ts`
7
+ * collects the governance answer it used to only refuse over.
8
+ *
9
+ * Extracted verbatim from `release.ts`, where the `/dev/tty` discipline below
10
+ * was established against a reproduced failure. Do not "simplify" it back to
11
+ * `readSync(0)`; the comment on `promptLine` says what that costs.
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import process from 'node:process';
15
+
16
+ /** Can we prompt at all — is fd 0 a TTY, or is a controlling terminal openable? */
17
+ export function canPrompt(): boolean {
18
+ if (process.env.CI) return false; // CI is non-interactive by contract → require --yes
19
+ if (process.stdin.isTTY) return true;
20
+ try {
21
+ const fd = fs.openSync('/dev/tty', 'r');
22
+ fs.closeSync(fd);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Mirror of Python `input(prompt)` — write the prompt, read one line from the
31
+ * controlling terminal. Returns `null` when no terminal is reachable.
32
+ *
33
+ * The answer is ALWAYS read from a freshly opened, blocking `/dev/tty`
34
+ * (`openSync(..., 'rs')`), NEVER from fd 0. `task release` / `./scripts-run`
35
+ * run this under go-task's `interactive: true`, which leaves fd 0 a TTY
36
+ * (`process.stdin.isTTY === true`) but one Node treats as NON-blocking — so a
37
+ * bare `readSync(0)` throws `EAGAIN`, the catch swallows it, and the `[y/N]`
38
+ * prompt "auto-aborts" without ever waiting (the exact failure this fixes;
39
+ * reproduced with `isTTY === true`). A fresh blocking `/dev/tty` descriptor
40
+ * blocks for real input regardless of how the script was invoked — the proven
41
+ * synchronous-prompt pattern.
42
+ */
43
+ export function promptLine(prompt: string): string | null {
44
+ process.stdout.write(prompt);
45
+ let fd: number;
46
+ try {
47
+ // 'rs' → O_RDONLY | O_SYNC: a fresh, blocking descriptor on the
48
+ // controlling terminal, unaffected by Node's non-blocking fd 0.
49
+ fd = fs.openSync('/dev/tty', 'rs');
50
+ } catch {
51
+ return null; // no controlling terminal (true non-interactive)
52
+ }
53
+ try {
54
+ const buf = Buffer.alloc(1);
55
+ const chars: number[] = [];
56
+ for (;;) {
57
+ let bytesRead: number;
58
+ try {
59
+ bytesRead = fs.readSync(fd, buf, 0, 1, null);
60
+ } catch (e) {
61
+ const code = (e as NodeJS.ErrnoException).code;
62
+ // A blocking /dev/tty should not yield these, but inherited
63
+ // descriptor flags can — retry rather than abort the prompt.
64
+ if (code === 'EAGAIN' || code === 'EINTR') continue;
65
+ break; // EOF / EIO → EOFError analogue; return what we have.
66
+ }
67
+ if (bytesRead === 0) break;
68
+ const b = buf[0] as number;
69
+ if (b === 0x0a) break; // newline terminates the line (stripped, like input()).
70
+ chars.push(b);
71
+ }
72
+ return Buffer.from(chars).toString('utf-8');
73
+ } finally {
74
+ fs.closeSync(fd);
75
+ }
76
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The council's synchronous HTTP transport, and the per-provider shims that
3
+ * turn it into the SDK-shaped callables `clients.ts::ask()` reads.
4
+ *
5
+ * Extracted from `clients.ts` (road-to-admissible-council-seats 1.1). Two
6
+ * reasons, and only the first is about tidiness:
7
+ *
8
+ * 1. `clients.ts` is ~1,370 lines past the 1,500-line ceiling
9
+ * `check_source_size_budget` ratchets, so every line added there costs one
10
+ * excess line. The three shims below are new code; putting them in a file
11
+ * UNDER the ceiling is what lets 1.1 land without raising a baseline.
12
+ * 2. Before 1.1 exactly one provider (OpenAI) had a live transport and the
13
+ * other four constructors threw `… package not installed. pip install …`
14
+ * after the api-key check — so `mode: api` was dead for gemini, xai and
15
+ * perplexity. A shared shim module is the honest home for "how this
16
+ * package actually reaches a provider over HTTP", rather than four
17
+ * copies of the same curl call inside four constructors.
18
+ *
19
+ * The injectable seam. Every shim takes a `JsonPost` rather than calling `curlJsonPost` directly.
20
+ * A test can then construct a REAL client with a REAL api_key — exercising
21
+ * the shim-building path a mock `client:` object skips entirely — and still
22
+ * make no network call. That is the difference between testing the transport
23
+ * and testing around it.
24
+ */
25
+
26
+ import { spawnSync } from 'node:child_process';
27
+
28
+ /** The one network primitive. Swappable so tests never reach the network. */
29
+ export type JsonPost = (url: string, extraHeaders: string[], body: unknown) => unknown;
30
+
31
+ export const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
32
+
33
+ /**
34
+ * Synchronous JSON POST via `curl` (no Node SDK, no Python). Matches the
35
+ * council's `spawnSync` transport model so the live-call clients stay
36
+ * synchronous. Returns the parsed response JSON (the provider HTTP APIs return
37
+ * exactly the SDK response shape `ask()` reads). Throws on transport failure or
38
+ * a non-2xx status so `ask()`'s catch surfaces it as a member error.
39
+ */
40
+ export function curlJsonPost(url: string, extraHeaders: string[], body: unknown): unknown {
41
+ const args = ['-sS', '-X', 'POST', url, '-H', 'content-type: application/json'];
42
+ for (const h of extraHeaders) {
43
+ args.push('-H', h);
44
+ }
45
+ // `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
46
+ // itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
47
+ // before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
48
+ // A full 16k-token generation legitimately runs several minutes, so the
49
+ // ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
50
+ // Both ceilings are PER CALL, never a run total. Members are iterated
51
+ // serially, so a run's worst case is members x passes x 290s, and nothing
52
+ // cancels a call mid-flight: `spawnSync` takes no signal, which is the
53
+ // synchronous contract the module docblock states. The only run-level bound
54
+ // is the USD cap, and it is checked AFTER a call returns.
55
+ args.push('--connect-timeout', '30', '--max-time', '290');
56
+ args.push('-w', '\n%{http_code}', '--data-binary', '@-');
57
+ const r = spawnSync('curl', args, {
58
+ input: JSON.stringify(body),
59
+ encoding: 'utf8',
60
+ maxBuffer: 64 * 1024 * 1024,
61
+ timeout: 300_000,
62
+ });
63
+ if (r.error) {
64
+ throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
65
+ }
66
+ if (r.status !== 0) {
67
+ throw new Error(
68
+ `curl exited ${r.status ?? 'null'}: ${(r.stderr ?? '').toString().slice(0, 500)}`,
69
+ );
70
+ }
71
+ const out = (r.stdout ?? '').toString();
72
+ const nl = out.lastIndexOf('\n');
73
+ const httpCode = (nl >= 0 ? out.slice(nl + 1) : out).trim();
74
+ const bodyText = nl >= 0 ? out.slice(0, nl) : out;
75
+ if (!/^2\d\d$/.test(httpCode)) {
76
+ throw new Error(`HTTP ${httpCode}: ${bodyText.slice(0, 500)}`);
77
+ }
78
+ return JSON.parse(bodyText);
79
+ }
80
+
81
+ /**
82
+ * `chat.completions.create` over any OpenAI-compatible `/v1/chat/completions`.
83
+ * Used by OpenAI itself and by the two OpenAI-compatible vendors (xAI,
84
+ * Perplexity), whose only difference is the base URL.
85
+ *
86
+ * `baseUrl` is passed rather than read off `this.base_url`: a subclass field
87
+ * initializer (`override base_url = XAI_BASE_URL`) runs AFTER the base
88
+ * constructor, so reading the field during construction would silently build
89
+ * every compatible client against the empty string.
90
+ */
91
+ export function openAiCompatibleClient(
92
+ baseUrl: string,
93
+ apiKey: string,
94
+ post: JsonPost = curlJsonPost,
95
+ ): unknown {
96
+ const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
97
+ return {
98
+ chat: {
99
+ completions: {
100
+ create: (kwargs: Record<string, unknown>): unknown =>
101
+ post(url, [`authorization: Bearer ${apiKey}`], kwargs),
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ /**
108
+ * `models.generate_content` over the Gemini REST API, normalised to the
109
+ * `google-genai` SDK response shape `GeminiClient.ask()` reads.
110
+ *
111
+ * The normalisation is the whole point and is not cosmetic: REST answers with
112
+ * `candidates[0].content.parts[*].text` and `usageMetadata.promptTokenCount`,
113
+ * while `ask()` reads `.text`, `.model_version` and
114
+ * `.usage_metadata.{prompt_token_count, candidates_token_count}`. Rewriting
115
+ * `ask()` to read REST instead would break every injected SDK-shaped mock in
116
+ * the existing tests, so the adapter lives here, at the transport boundary,
117
+ * where a shape mismatch is a transport concern.
118
+ */
119
+ export function geminiRestClient(
120
+ apiKey: string,
121
+ post: JsonPost = curlJsonPost,
122
+ baseUrl: string = GEMINI_BASE_URL,
123
+ ): unknown {
124
+ return {
125
+ models: {
126
+ generate_content: (kwargs: Record<string, unknown>): unknown => {
127
+ const model = String(kwargs['model'] ?? '');
128
+ const cfg = (kwargs['config'] ?? {}) as Record<string, unknown>;
129
+ const url = `${baseUrl.replace(/\/+$/, '')}/models/${model}:generateContent`;
130
+ const raw = post(
131
+ url,
132
+ [`x-goog-api-key: ${apiKey}`],
133
+ {
134
+ contents: [{ role: 'user', parts: [{ text: String(kwargs['contents'] ?? '') }] }],
135
+ generationConfig: { maxOutputTokens: cfg['max_output_tokens'] ?? null },
136
+ },
137
+ );
138
+ return normaliseGeminiRest(raw);
139
+ },
140
+ },
141
+ };
142
+ }
143
+
144
+ /** REST envelope → the `google-genai` SDK response shape. Exported for tests. */
145
+ export function normaliseGeminiRest(raw: unknown): {
146
+ text: string;
147
+ model_version: string;
148
+ usage_metadata: { prompt_token_count: number; candidates_token_count: number };
149
+ } {
150
+ const env = (raw ?? {}) as Record<string, unknown>;
151
+ const candidates = Array.isArray(env['candidates']) ? (env['candidates'] as unknown[]) : [];
152
+ const first = (candidates[0] ?? {}) as Record<string, unknown>;
153
+ const content = (first['content'] ?? {}) as Record<string, unknown>;
154
+ const parts = Array.isArray(content['parts']) ? (content['parts'] as unknown[]) : [];
155
+ const text = parts
156
+ .map((p) => String(((p ?? {}) as Record<string, unknown>)['text'] ?? ''))
157
+ .join('');
158
+ const usage = (env['usageMetadata'] ?? {}) as Record<string, unknown>;
159
+ const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
160
+ return {
161
+ text,
162
+ model_version: String(env['modelVersion'] ?? ''),
163
+ usage_metadata: {
164
+ prompt_token_count: num(usage['promptTokenCount']),
165
+ candidates_token_count: num(usage['candidatesTokenCount']),
166
+ },
167
+ };
168
+ }
@@ -47,6 +47,12 @@ import * as path from 'node:path';
47
47
 
48
48
  import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
49
49
  import { boundFor } from './cli_agency_bounds.js';
50
+ import {
51
+ curlJsonPost as _curlJsonPost,
52
+ geminiRestClient,
53
+ openAiCompatibleClient,
54
+ type JsonPost,
55
+ } from './api_transport.js';
50
56
  import { SESSION_ROLE_ENV } from '../_lib/session_role.js';
51
57
  import { load_agent_settings } from '../_lib/agent_settings.js';
52
58
 
@@ -163,9 +169,8 @@ export const DEFAULT_OPENAI_CLI_MODEL = OPENAI_CLI_VENDOR_DEFAULT;
163
169
  export const DEFAULT_ANTHROPIC_CLI_MODEL = 'sonnet';
164
170
  export const DEFAULT_GEMINI_CLI_MODEL = 'gemini-2.5-pro';
165
171
 
166
- // OpenAI-API-compatible endpoints. xAI and Perplexity both expose the
167
- // `/v1/chat/completions` shape, so their clients reuse the `openai` SDK with a
168
- // custom `base_url`. Gemini has its own SDK (`google-genai`).
172
+ // OpenAI-API-compatible endpoints — xAI and Perplexity both expose the
173
+ // `/v1/chat/completions` shape, reached by one curl shim in `api_transport.ts`.
169
174
  export const XAI_BASE_URL = 'https://api.x.ai/v1';
170
175
  export const PERPLEXITY_BASE_URL = 'https://api.perplexity.ai';
171
176
 
@@ -461,6 +466,12 @@ interface ApiClientOptions {
461
466
  // Phase 4). Default '5m' when omitted — see `DEFAULT_PROMPT_CACHE_TTL`.
462
467
  // Only meaningful when `enable_prompt_cache` is true.
463
468
  prompt_cache_ttl?: PromptCacheTtl | undefined;
469
+ /**
470
+ * HTTP seam. Omitted → the live `curl` post. A test passes a double
471
+ * TOGETHER with a real `api_key`, which exercises the shim-building
472
+ * path that an injected `client:` object skips entirely.
473
+ */
474
+ transport?: JsonPost | undefined;
464
475
  }
465
476
 
466
477
  /** Shared ctor-options shape for the CLI clients. */
@@ -479,47 +490,6 @@ interface CliClientOptions {
479
490
  consumer?: string | undefined;
480
491
  }
481
492
 
482
- /**
483
- * Synchronous JSON POST via `curl` (no Node SDK, no Python). Matches the
484
- * council's `spawnSync` transport model so the live-call clients stay
485
- * synchronous. Returns the parsed response JSON (the provider HTTP APIs return
486
- * exactly the SDK response shape `ask()` reads). Throws on transport failure or
487
- * a non-2xx status so `ask()`'s catch surfaces it as a member error.
488
- */
489
- function _curlJsonPost(url: string, extraHeaders: string[], body: unknown): unknown {
490
- const args = ['-sS', '-X', 'POST', url, '-H', 'content-type: application/json'];
491
- for (const h of extraHeaders) {
492
- args.push('-H', h);
493
- }
494
- // `--connect-timeout` fast-fails a dead host; `--max-time` lets curl abort
495
- // itself cleanly (surfacing a real `curl exited` error with stderr) ~10s
496
- // before the `spawnSync` timeout would kill it with an opaque ETIMEDOUT.
497
- // A full 16k-token generation legitimately runs several minutes, so the
498
- // ceiling is 300s, not 120s (the old value timed out long Anthropic calls).
499
- args.push('--connect-timeout', '30', '--max-time', '290');
500
- args.push('-w', '\n%{http_code}', '--data-binary', '@-');
501
- const r = spawnSync('curl', args, {
502
- input: JSON.stringify(body),
503
- encoding: 'utf8',
504
- maxBuffer: 64 * 1024 * 1024,
505
- timeout: 300_000,
506
- });
507
- if (r.error) {
508
- throw new Error(`curl spawn failed: ${(r.error as Error).message}`);
509
- }
510
- if (r.status !== 0) {
511
- throw new Error(`curl exited ${r.status ?? 'null'}: ${(r.stderr ?? '').toString().slice(0, 500)}`);
512
- }
513
- const out = (r.stdout ?? '').toString();
514
- const nl = out.lastIndexOf('\n');
515
- const httpCode = (nl >= 0 ? out.slice(nl + 1) : out).trim();
516
- const bodyText = nl >= 0 ? out.slice(0, nl) : out;
517
- if (!/^2\d\d$/.test(httpCode)) {
518
- throw new Error(`HTTP ${httpCode}: ${bodyText.slice(0, 500)}`);
519
- }
520
- return JSON.parse(bodyText);
521
- }
522
-
523
493
  export class AnthropicClient extends ExternalAIClient {
524
494
  override name = 'anthropic';
525
495
  override billable = true;
@@ -824,13 +794,12 @@ export class OpenAIClient extends ExternalAIClient {
824
794
  // ── Gemini / xAI / Perplexity (Phase 0 — Step 6) ─────────────────────
825
795
 
826
796
  /**
827
- * Google Gemini via the `google-genai` SDK.
828
- *
829
- * Lazy-imports `google.genai` on first `ask()` so disabled members do not
830
- * require the SDK to be installed. Tests inject a mock client shaped like
831
- * `genai.Client(api_key=...)` — `self._client.models.generate_content(...)`
832
- * returns an object with `.text` and
833
- * `.usage_metadata.{prompt_token_count, candidates_token_count}`.
797
+ * Google Gemini over the REST API (1.1 — there is no SDK dependency; the old
798
+ * `google-genai` lazy-import never existed in the TS port and the constructor
799
+ * simply threw). `ask()` still reads the SDK RESPONSE shape — `.text`,
800
+ * `.model_version`, `.usage_metadata.{prompt_token_count,
801
+ * candidates_token_count}` — which `geminiRestClient` normalises to, so every
802
+ * injected-mock test is unaffected.
834
803
  */
835
804
  export class GeminiClient extends ExternalAIClient {
836
805
  override name = 'gemini';
@@ -851,7 +820,12 @@ export class GeminiClient extends ExternalAIClient {
851
820
  'Use `api_key_ref: env:GEMINI_API_KEY` in ~/.event4u/agent-config/settings/.ai-council.yml.',
852
821
  );
853
822
  }
854
- throw new Error('google-genai package not installed. `pip install google-genai`.');
823
+ // Live transport (1.1). Was an unconditional `pip install google-genai`
824
+ // throw, which made `mode: api` dead for this provider however the key
825
+ // was configured. `geminiRestClient` normalises the REST envelope into
826
+ // the SDK shape `ask()` below already reads, so `ask()` is untouched
827
+ // and every existing injected-mock test keeps passing.
828
+ this._client = geminiRestClient(api_key, opts.transport ?? _curlJsonPost);
855
829
  }
856
830
 
857
831
  override ask(
@@ -901,19 +875,26 @@ export class GeminiClient extends ExternalAIClient {
901
875
  }
902
876
 
903
877
  /**
904
- * Shared shape for OpenAI-API-compatible providers (xAI, Perplexity).
905
- *
906
- * Both vendors implement `/v1/chat/completions` and accept the `openai` Python
907
- * SDK with a custom `base_url`. The reasoning-model branch from `OpenAIClient`
908
- * is intentionally omitted — neither xAI nor Perplexity ships a reasoning model
909
- * that requires `max_completion_tokens` as of 2026-05-14.
878
+ * Shared shape for OpenAI-API-compatible providers (xAI, Perplexity): both
879
+ * implement `/v1/chat/completions`, so 1.1 reaches them with one curl shim and
880
+ * a base URL. The reasoning-model branch from `OpenAIClient` is intentionally
881
+ * omitted — neither ships a reasoning model that requires
882
+ * `max_completion_tokens` as of 2026-05-14.
910
883
  */
911
884
  export class _OpenAICompatibleClient extends ExternalAIClient {
912
885
  override billable = true;
913
886
  base_url = '';
914
887
  protected _client: unknown;
915
888
 
916
- constructor(opts: { model: string; client?: unknown; api_key?: string | null | undefined }) {
889
+ constructor(opts: {
890
+ model: string;
891
+ client?: unknown;
892
+ api_key?: string | null | undefined;
893
+ transport?: JsonPost | undefined;
894
+ // Passed, never read off `this.base_url`: a subclass field initializer
895
+ // runs AFTER this constructor, so the field is still '' right here.
896
+ base_url: string;
897
+ }) {
917
898
  super();
918
899
  this.model = opts.model;
919
900
  if (opts.client !== undefined && opts.client !== null) {
@@ -926,7 +907,13 @@ export class _OpenAICompatibleClient extends ExternalAIClient {
926
907
  `${this.constructor.name} requires explicit api_key or injected client.`,
927
908
  );
928
909
  }
929
- throw new Error('openai package not installed. `pip install openai`.');
910
+ // Live transport (1.1) — replaces an unconditional `pip install openai`
911
+ // throw that made `mode: api` dead for xai and perplexity.
912
+ this._client = openAiCompatibleClient(
913
+ opts.base_url,
914
+ api_key,
915
+ opts.transport ?? _curlJsonPost,
916
+ );
930
917
  }
931
918
 
932
919
  override ask(
@@ -989,7 +976,13 @@ export class XAIClient extends _OpenAICompatibleClient {
989
976
  override base_url = XAI_BASE_URL;
990
977
 
991
978
  constructor(opts: ApiClientOptions = {}) {
992
- super({ model: opts.model ?? DEFAULT_XAI_MODEL, client: opts.client, api_key: opts.api_key });
979
+ super({
980
+ model: opts.model ?? DEFAULT_XAI_MODEL,
981
+ client: opts.client,
982
+ api_key: opts.api_key,
983
+ transport: opts.transport,
984
+ base_url: XAI_BASE_URL,
985
+ });
993
986
  }
994
987
  }
995
988
 
@@ -1003,6 +996,8 @@ export class PerplexityClient extends _OpenAICompatibleClient {
1003
996
  model: opts.model ?? DEFAULT_PERPLEXITY_MODEL,
1004
997
  client: opts.client,
1005
998
  api_key: opts.api_key,
999
+ transport: opts.transport,
1000
+ base_url: PERPLEXITY_BASE_URL,
1006
1001
  });
1007
1002
  }
1008
1003
  }
@@ -1028,8 +1023,8 @@ export {
1028
1023
  //
1029
1024
  // Raised 120 → 300 (2026-08-13) to match the API transport, which has carried
1030
1025
  // `--max-time 290` / `timeout: 300_000` since the 2026-06-24 repair of the same
1031
- // symptom. That repair landed in `_curlJsonPost` only, so when a member resolves
1032
- // to `cli · subscription` the old cap was still live — and a deep design run
1026
+ // symptom. That repair landed in `api_transport.ts::curlJsonPost` only, so with
1027
+ // a member on `cli · subscription` the old cap was still live — a deep run
1033
1028
  // reproduced it exactly: both members returned `error: timeout` at
1034
1029
  // `latency_ms: 122921` with `timeout_seconds: 120`, and the run reported
1035
1030
  // `0/2 present — INCONCLUSIVE`.
@@ -83,6 +83,8 @@ import * as budget from './cli_call_budget.js';
83
83
  // Converting it to a value import — or adding any other runtime import from
84
84
  // quorum into config — creates a real ESM cycle in the loader's init path.
85
85
  import { OPENAI_CLI_VENDOR_DEFAULT } from './clients.js';
86
+ import { parseSeatFields, type PolicyExclusion } from './seat_policy.js';
87
+ import type { ContentClass } from './content_ceiling.js';
86
88
  import { SOLO_FLOOR_MIN_PRESENT } from './quorum.js';
87
89
 
88
90
  const _VALID_PROVIDERS: ReadonlySet<string> = new Set([
@@ -330,6 +332,12 @@ export interface MemberConfig {
330
332
  * demanding a date nobody would refresh.
331
333
  */
332
334
  readonly verified_at: string | null;
335
+ /** Seat policy — parsed and documented by `seat_policy.ts::parseSeatFields`.
336
+ * All three are NOTES or REFUSALS: none can make a seat available that was
337
+ * not already available. */
338
+ readonly disabled_reason: string | null;
339
+ readonly policy_exclusion: PolicyExclusion | null;
340
+ readonly content_ceiling: ContentClass;
333
341
  }
334
342
 
335
343
  /**
@@ -1875,6 +1883,10 @@ function _build_member(
1875
1883
  }
1876
1884
  verified_at = coerced;
1877
1885
  }
1886
+ const seat = parseSeatFields(name, cfg as Record<string, unknown>);
1887
+ if ('error' in seat) {
1888
+ throw new CouncilConfigError(seat.error);
1889
+ }
1878
1890
  const prompt_cache_raw = _get(cfg, 'prompt_cache', null);
1879
1891
  let prompt_cache_ttl: '5m' | '1h' = '5m';
1880
1892
  if (prompt_cache_raw !== null && prompt_cache_raw !== undefined && !_isBool(prompt_cache_raw)) {
@@ -1907,6 +1919,7 @@ function _build_member(
1907
1919
  tier,
1908
1920
  prompt_cache_ttl,
1909
1921
  verified_at,
1922
+ ...seat.fields,
1910
1923
  };
1911
1924
  }
1912
1925