@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +441 -0
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/analyze/inbox.md +381 -6
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +251 -34
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +15 -0
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-BIXEDQPZ.js} +7 -1
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +19 -5
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +27 -3
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +60 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +1522 -1008
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +10 -1
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39376 -38902
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/llms.txt +14 -14
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/config/release-gate-locality.yml +4 -3
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/host_capability.ts +38 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +168 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +263 -24
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +29 -39
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +94 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +6 -2
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +134 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release.ts +26 -71
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/release_publication.ts +34 -29
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/self_review_gate.ts +409 -15
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- 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
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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,
|
|
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,
|
|
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
|
|
167
|
-
// `/v1/chat/completions` shape,
|
|
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
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
* `
|
|
832
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
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: {
|
|
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
|
-
|
|
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({
|
|
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 `
|
|
1032
|
-
//
|
|
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
|
|