@event4u/agent-config 9.10.0 → 9.12.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +111 -0
- package/README.md +20 -16
- package/agents/templates/.ai-council.yml.example +481 -0
- package/agents/templates/.ai-video.xml.example +247 -0
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/design-fidelity.md +2 -2
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- package/dist/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/dist/cli/initRouting.js +74 -11
- package/dist/cli/initRouting.js.map +1 -1
- package/dist/cli/main.js +14 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
- package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
- package/dist/cli-delegate/chunk-A3HJ6NTI.js +1084 -0
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/chunk-FTAISGDQ.js +117 -0
- package/dist/cli-delegate/chunk-GME3Q44L.js +82 -0
- package/dist/cli-delegate/chunk-GRRPEPRN.js +336 -0
- package/dist/cli-delegate/chunk-HJ6DAJHR.js +502 -0
- package/dist/cli-delegate/chunk-HYWSRNUX.js +179 -0
- package/dist/cli-delegate/chunk-J55WI3RR.js +47 -0
- package/dist/cli-delegate/chunk-MCJYCQPF.js +35 -0
- package/dist/cli-delegate/chunk-NDJLNC6Q.js +169 -0
- package/dist/cli-delegate/chunk-OMIVVCG2.js +26 -0
- package/dist/cli-delegate/chunk-OY53J6II.js +7336 -0
- package/dist/cli-delegate/chunk-QFKQLQ7X.js +10835 -0
- package/dist/cli-delegate/chunk-TEGJU2JX.js +6908 -0
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
- package/dist/cli-delegate/cmd_analyze_session.js +294 -0
- package/dist/cli-delegate/cmd_conformance.js +643 -0
- package/dist/cli-delegate/cmd_converge.js +326 -0
- package/dist/cli-delegate/cmd_doctor.js +142 -0
- package/dist/cli-delegate/cmd_explain.js +2576 -0
- package/dist/cli-delegate/cmd_export.js +298 -0
- package/dist/cli-delegate/cmd_fleet.js +333 -0
- package/dist/cli-delegate/cmd_memory_get.js +3878 -0
- package/dist/cli-delegate/cmd_migrate.js +11 -0
- package/dist/cli-delegate/cmd_preflight.js +153 -0
- package/dist/cli-delegate/cmd_prune.js +446 -0
- package/dist/cli-delegate/cmd_refresh.js +28 -0
- package/dist/cli-delegate/cmd_settings_check.js +574 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +225 -0
- package/dist/cli-delegate/cmd_sync.js +266 -0
- package/dist/cli-delegate/cmd_uninstall.js +657 -0
- package/dist/cli-delegate/cmd_update.js +425 -0
- package/dist/cli-delegate/cmd_upgrade.js +444 -0
- package/dist/cli-delegate/cmd_validate.js +307 -0
- package/dist/cli-delegate/cmd_versions.js +370 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +45 -16
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -7
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/install/emit_host_rules_cli.js +2 -1
- package/dist/install/emit_host_rules_cli.js.map +1 -1
- package/dist/install/install.mjs +634 -409
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +7 -3
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/toolDetection.js +40 -12
- package/dist/install/toolDetection.js.map +1 -1
- package/dist/install/wizard-plan.js +12 -3
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1553 -2728
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +6 -22
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +14 -0
- package/package.json +4 -2
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- package/src/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/src/config/discovery/packs.yml +7 -0
- package/src/config/evaluator-budgets.json +1 -1
- package/src/config/pack-size-budget.json +27 -0
- package/src/install/emit_host_rules_cli.ts +2 -1
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +7 -3
- package/src/install/toolDetection.ts +39 -8
- package/src/install/wizard-plan.ts +13 -3
- package/src/scripts/_cli/cmd_conformance.ts +2 -1
- package/src/scripts/_cli/cmd_converge.ts +2 -1
- package/src/scripts/_cli/cmd_doctor.ts +214 -13
- package/src/scripts/_cli/cmd_export.ts +2 -1
- package/src/scripts/_cli/cmd_fleet.ts +2 -1
- package/src/scripts/_cli/cmd_migrate.ts +2 -1
- package/src/scripts/_cli/cmd_preflight.ts +2 -1
- package/src/scripts/_cli/cmd_refresh.ts +2 -1
- package/src/scripts/_cli/cmd_update.ts +2 -1
- package/src/scripts/_cli/cmd_upgrade.ts +2 -1
- package/src/scripts/_cli/cmd_versions.ts +2 -1
- package/src/scripts/_cli/detection_report.ts +480 -0
- package/src/scripts/_dispatch.bash +52 -14
- package/src/scripts/_lib/changelog_eras.ts +103 -9
- package/src/scripts/_lib/environment_detector.ts +441 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- package/src/scripts/_lib/package_root.ts +72 -0
- package/src/scripts/ai_council/config.ts +15 -7
- package/src/scripts/ai_council/modes.ts +68 -2
- package/src/scripts/ai_council/transport_resolver.ts +331 -0
- package/src/scripts/check_bundle_path_leakage.ts +10 -4
- package/src/scripts/check_dependency_floors.ts +153 -0
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/check_pack_size.ts +188 -0
- package/src/scripts/condense.ts +7 -28
- package/src/scripts/council_cli.ts +6 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_eval_fixture_citations.ts +113 -0
- package/src/scripts/lint_ui_stack_bundles.ts +201 -0
|
@@ -69,8 +69,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
69
69
|
import * as cli_wrapper from '../_lib/cli_wrapper.js';
|
|
70
70
|
import * as sync_gitignore from '../sync_gitignore.js';
|
|
71
71
|
import * as sync_gitattributes from '../sync_gitattributes.js';
|
|
72
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
72
73
|
|
|
73
|
-
const PACKAGE_ROOT =
|
|
74
|
+
const PACKAGE_ROOT = resolvePackageRoot(import.meta.url);
|
|
74
75
|
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
76
77
|
// Parity primitives (ADR-200).
|
|
@@ -67,6 +67,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
67
67
|
|
|
68
68
|
import * as installed_lock from '../_lib/installed_lock.js';
|
|
69
69
|
import * as update_check from '../_lib/update_check.js';
|
|
70
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
70
71
|
import {
|
|
71
72
|
DEFAULT_PROJECT_FILE,
|
|
72
73
|
LOCAL_PROJECT_FILE,
|
|
@@ -550,7 +551,7 @@ function _refresh_global_lockfile(version: string, out: OutSink): void {
|
|
|
550
551
|
|
|
551
552
|
/** Read `version` from the package's own `package.json`. */
|
|
552
553
|
function _detect_installed_version(): string {
|
|
553
|
-
const pkg_json = path.join(
|
|
554
|
+
const pkg_json = path.join(resolvePackageRoot(import.meta.url), 'package.json');
|
|
554
555
|
try {
|
|
555
556
|
const data = JSON.parse(fs.readFileSync(pkg_json, { encoding: 'utf-8' })) as Record<
|
|
556
557
|
string,
|
|
@@ -93,9 +93,10 @@ import * as claude_plugin from '../_lib/claude_plugin.js';
|
|
|
93
93
|
import { event4u_root, write_target } from '../_lib/user_global_paths.js';
|
|
94
94
|
import { project_settings_path } from '../_lib/agent_settings.js';
|
|
95
95
|
import { _is_source_repo } from './cmd_refresh.js';
|
|
96
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
96
97
|
|
|
97
98
|
const PACKAGE_NAME = '@event4u/agent-config';
|
|
98
|
-
const PACKAGE_ROOT =
|
|
99
|
+
const PACKAGE_ROOT = resolvePackageRoot(import.meta.url);
|
|
99
100
|
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
// Parity primitives (ADR-200).
|
|
@@ -47,6 +47,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
47
47
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
48
48
|
|
|
49
49
|
import { project_settings_path, resolve_project_root } from '../_lib/agent_settings.js';
|
|
50
|
+
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
50
51
|
|
|
51
52
|
const PACKAGE_NAME = '@event4u/agent-config';
|
|
52
53
|
|
|
@@ -171,7 +172,7 @@ function _project_root(): string {
|
|
|
171
172
|
/** Return `version` from the local `package.json`, or `""` if absent. */
|
|
172
173
|
function _local_package_version(): string {
|
|
173
174
|
const candidates = [
|
|
174
|
-
path.join(
|
|
175
|
+
path.join(resolvePackageRoot(import.meta.url), 'package.json'),
|
|
175
176
|
path.join(_project_root(), 'package.json'),
|
|
176
177
|
];
|
|
177
178
|
for (const p of candidates) {
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detection report — one builder, one renderer, three surfaces (Phases 3
|
|
3
|
+
* and 4 of `road-to-zero-ceremony-detection`).
|
|
4
|
+
*
|
|
5
|
+
* `doctor`'s detection section, its `--json` payload, and the first-invocation
|
|
6
|
+
* spend disclosure are all produced from the SAME rows built here. That is the
|
|
7
|
+
* point: a disclosure rendered by its own code path drifts from the report the
|
|
8
|
+
* user just read, and then the number they were shown is not the number that
|
|
9
|
+
* governs the call.
|
|
10
|
+
*
|
|
11
|
+
* ## What a row says — and what it deliberately does not
|
|
12
|
+
*
|
|
13
|
+
* Per provider: `detected` · `authenticated` · `authSource` · `billing` ·
|
|
14
|
+
* `enabledInConfig`, plus the transport `auto` WOULD select and, when the
|
|
15
|
+
* provider is unavailable, a one-line reason and a one-line fix.
|
|
16
|
+
*
|
|
17
|
+
* The gap between "could work" and "allowed to spend" is reported, never
|
|
18
|
+
* closed. `enabledInConfig: false` on a fully detected provider is not a
|
|
19
|
+
* defect — it is a recorded consent decision, and this module's job is to make
|
|
20
|
+
* that visible instead of mysterious. See the roadmap's
|
|
21
|
+
* `council-availability-semantics` blocker for why detection does not confer
|
|
22
|
+
* permission.
|
|
23
|
+
*
|
|
24
|
+
* ## Purity
|
|
25
|
+
*
|
|
26
|
+
* `buildDetectionReport` is pure over its inputs: an `EnvironmentReport` from
|
|
27
|
+
* `_lib/environment_detector.ts` plus a `CouncilFacts` struct. The impure read
|
|
28
|
+
* lives in `readCouncilFacts`, and the CLI-call counters are injected by the
|
|
29
|
+
* caller — so this module never imports the council client layer and cannot
|
|
30
|
+
* reach a billable call.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
classifyBilling,
|
|
37
|
+
knownProviders,
|
|
38
|
+
strongestAuth,
|
|
39
|
+
PROVIDER_CLI_META,
|
|
40
|
+
type AuthSource,
|
|
41
|
+
type BillingClass,
|
|
42
|
+
type DetectedHost,
|
|
43
|
+
type EnvironmentReport,
|
|
44
|
+
} from '../_lib/environment_detector.js';
|
|
45
|
+
import { resolveTransport, type Transport } from '../ai_council/transport_resolver.js';
|
|
46
|
+
|
|
47
|
+
export const DETECTION_SCHEMA_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
/** The council-config facts a row needs. Read separately so the builder is pure. */
|
|
50
|
+
export interface CouncilFacts {
|
|
51
|
+
/** Resolved user-global config path, whether or not it exists. */
|
|
52
|
+
readonly configPath: string;
|
|
53
|
+
readonly configPresent: boolean;
|
|
54
|
+
/** Council master switch. `false` when no config exists. */
|
|
55
|
+
readonly enabled: boolean;
|
|
56
|
+
/** Effective `defaults.mode`, or `null` when no config exists. */
|
|
57
|
+
readonly defaultsMode: string | null;
|
|
58
|
+
readonly members: Readonly<Record<string, CouncilMemberFacts>>;
|
|
59
|
+
/** `cli_call_budget.max_calls_per_day` per provider. */
|
|
60
|
+
readonly cliCaps: Readonly<Record<string, number>>;
|
|
61
|
+
/** `cost_budget.max_total_usd`, or `null` when no config exists. */
|
|
62
|
+
readonly costBudgetMaxUsd: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CouncilMemberFacts {
|
|
66
|
+
readonly enabled: boolean;
|
|
67
|
+
/** Per-member `mode:` override, or `null`. */
|
|
68
|
+
readonly mode: string | null;
|
|
69
|
+
/** Per-member `binary:` override, or `null`. */
|
|
70
|
+
readonly binary: string | null;
|
|
71
|
+
readonly apiKeyRef: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProviderRow {
|
|
75
|
+
readonly provider: string;
|
|
76
|
+
/** A CLI binary resolves OR any credential is present. */
|
|
77
|
+
readonly detected: boolean;
|
|
78
|
+
/** Any credential is present. Presence — never a claim that it works. */
|
|
79
|
+
readonly authenticated: boolean;
|
|
80
|
+
readonly authSource: AuthSource | null;
|
|
81
|
+
readonly billing: BillingClass;
|
|
82
|
+
/** The transport `auto` would select right now, or `null` when none can be. */
|
|
83
|
+
readonly transport: Transport | null;
|
|
84
|
+
/**
|
|
85
|
+
* The mode this member is actually CONFIGURED with — per-member override,
|
|
86
|
+
* else `defaults.mode`. Distinct from `transport`: that answers "what could
|
|
87
|
+
* work", this answers "what will run". A member pinned to `manual` makes no
|
|
88
|
+
* provider call no matter what `auto` could have reached.
|
|
89
|
+
*/
|
|
90
|
+
readonly configuredMode: string | null;
|
|
91
|
+
readonly available: boolean;
|
|
92
|
+
readonly enabledInConfig: boolean;
|
|
93
|
+
/** Non-null exactly when `available` is false. */
|
|
94
|
+
readonly reason: string | null;
|
|
95
|
+
/** One-line fix. Non-null whenever the capability is unusable as configured. */
|
|
96
|
+
readonly fix: string | null;
|
|
97
|
+
/** Per-day CLI cap, or `null` when unset (unlimited from the loader's view). */
|
|
98
|
+
readonly cliCap: number | null;
|
|
99
|
+
/** CLI calls already spent today, when the caller supplied counters. */
|
|
100
|
+
readonly cliUsed: number | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface DetectionReport {
|
|
104
|
+
readonly schema_version: number;
|
|
105
|
+
readonly council_config_path: string;
|
|
106
|
+
readonly council_config_present: boolean;
|
|
107
|
+
readonly council_enabled: boolean;
|
|
108
|
+
readonly defaults_mode: string | null;
|
|
109
|
+
readonly hosts: readonly DetectedHost[];
|
|
110
|
+
readonly providers: readonly ProviderRow[];
|
|
111
|
+
readonly budgets: {
|
|
112
|
+
readonly cost_budget_max_total_usd: number | null;
|
|
113
|
+
readonly cli_call_budget: Readonly<Record<string, number>>;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BuildDetectionReportOptions {
|
|
118
|
+
readonly environment: EnvironmentReport;
|
|
119
|
+
readonly council: CouncilFacts;
|
|
120
|
+
/** `provider -> calls spent today`. Injected so this module stays client-free. */
|
|
121
|
+
readonly cliCallsUsed?: Readonly<Record<string, number>>;
|
|
122
|
+
/** Environment map for resolving `env:` key refs. Defaults to `process.env`. */
|
|
123
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* True when `ref` is an `env:<VAR>` reference whose variable actually holds a
|
|
128
|
+
* value. A `file:` ref is deliberately NOT resolved here — the detector already
|
|
129
|
+
* probes the standard key-file names, and re-resolving arbitrary paths would be
|
|
130
|
+
* the third auth-probe shape this work exists to avoid.
|
|
131
|
+
*/
|
|
132
|
+
function envRefResolves(
|
|
133
|
+
ref: string | null,
|
|
134
|
+
env?: Readonly<Record<string, string | undefined>>,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (ref === null || !ref.startsWith('env:')) return false;
|
|
137
|
+
const name = ref.slice('env:'.length).trim();
|
|
138
|
+
if (name === '') return false;
|
|
139
|
+
return ((env ?? process.env)[name] ?? '').trim() !== '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The one-line edit that grants a detected provider permission to spend.
|
|
144
|
+
*
|
|
145
|
+
* This is an EDIT, not an invented command: no settings-mutation CLI exists
|
|
146
|
+
* (`settings` is a GUI alias; a `settings set` verb is greenfield), so emitting
|
|
147
|
+
* `agent-config settings set …` would print a command that does not run. The
|
|
148
|
+
* file and the exact key path are what remove the work of finding it, which is
|
|
149
|
+
* the actual goal — consent stays an explicit act either way.
|
|
150
|
+
*/
|
|
151
|
+
export function enableInstruction(provider: string, configPath: string): string {
|
|
152
|
+
return `set \`members.${provider}.enabled: true\` in ${configPath}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Build every row. Pure over `opts`. */
|
|
156
|
+
export function buildDetectionReport(opts: BuildDetectionReportOptions): DetectionReport {
|
|
157
|
+
const { environment, council } = opts;
|
|
158
|
+
const used = opts.cliCallsUsed ?? {};
|
|
159
|
+
const providers: ProviderRow[] = [];
|
|
160
|
+
|
|
161
|
+
for (const provider of knownProviders()) {
|
|
162
|
+
const member = council.members[provider];
|
|
163
|
+
const enabledInConfig = member?.enabled ?? false;
|
|
164
|
+
const auth = strongestAuth(environment, provider);
|
|
165
|
+
const authSource = auth?.source ?? null;
|
|
166
|
+
const binaryOverride = member?.binary ?? null;
|
|
167
|
+
|
|
168
|
+
// What `auto` would pick — the honest "could this work right now"
|
|
169
|
+
// answer, independent of the mode actually configured.
|
|
170
|
+
//
|
|
171
|
+
// A CONFIGURED `api_key_ref` is not a RESOLVED key: every member in the
|
|
172
|
+
// shipped template carries one (`env:XAI_API_KEY` and friends) whether
|
|
173
|
+
// or not the variable is set. Treating configuration as presence made
|
|
174
|
+
// every provider report `auto→api` while showing `detected ❌`. So the
|
|
175
|
+
// detector's records decide, plus the one case configuration does prove:
|
|
176
|
+
// an `env:` ref whose variable actually holds a value.
|
|
177
|
+
const resolved = resolveTransport({
|
|
178
|
+
provider,
|
|
179
|
+
mode: 'auto',
|
|
180
|
+
report: environment,
|
|
181
|
+
binaryOverride,
|
|
182
|
+
...(envRefResolves(member?.apiKeyRef ?? null, opts.env)
|
|
183
|
+
? { apiKeyPresent: true }
|
|
184
|
+
: {}),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const binaryPresent = cliBinaryPresent(environment, provider, binaryOverride);
|
|
188
|
+
const detected = binaryPresent || authSource !== null;
|
|
189
|
+
const capRaw = council.cliCaps[provider];
|
|
190
|
+
|
|
191
|
+
providers.push({
|
|
192
|
+
provider,
|
|
193
|
+
detected,
|
|
194
|
+
authenticated: authSource !== null,
|
|
195
|
+
authSource,
|
|
196
|
+
billing: classifyBilling(provider, authSource),
|
|
197
|
+
transport: resolved.transport,
|
|
198
|
+
configuredMode: member?.mode ?? council.defaultsMode,
|
|
199
|
+
available: resolved.available,
|
|
200
|
+
enabledInConfig,
|
|
201
|
+
reason: resolved.reason,
|
|
202
|
+
fix: fixFor({
|
|
203
|
+
provider,
|
|
204
|
+
detected,
|
|
205
|
+
available: resolved.available,
|
|
206
|
+
configuredMode: member?.mode ?? council.defaultsMode,
|
|
207
|
+
enabledInConfig,
|
|
208
|
+
councilEnabled: council.enabled,
|
|
209
|
+
configPresent: council.configPresent,
|
|
210
|
+
configPath: council.configPath,
|
|
211
|
+
reason: resolved.reason,
|
|
212
|
+
}),
|
|
213
|
+
cliCap: capRaw ?? null,
|
|
214
|
+
cliUsed: Object.prototype.hasOwnProperty.call(used, provider)
|
|
215
|
+
? (used[provider] ?? 0)
|
|
216
|
+
: null,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
schema_version: DETECTION_SCHEMA_VERSION,
|
|
222
|
+
council_config_path: council.configPath,
|
|
223
|
+
council_config_present: council.configPresent,
|
|
224
|
+
council_enabled: council.enabled,
|
|
225
|
+
defaults_mode: council.defaultsMode,
|
|
226
|
+
hosts: environment.hosts,
|
|
227
|
+
providers,
|
|
228
|
+
budgets: {
|
|
229
|
+
cost_budget_max_total_usd: council.costBudgetMaxUsd,
|
|
230
|
+
cli_call_budget: council.cliCaps,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cliBinaryPresent(
|
|
236
|
+
environment: EnvironmentReport,
|
|
237
|
+
provider: string,
|
|
238
|
+
binaryOverride: string | null,
|
|
239
|
+
): boolean {
|
|
240
|
+
if (binaryOverride !== null && binaryOverride.trim() !== '') return true;
|
|
241
|
+
const expected = PROVIDER_CLI_META[provider]?.[0];
|
|
242
|
+
if (expected === undefined) return false;
|
|
243
|
+
return environment.hosts.some(
|
|
244
|
+
(h) => h.binary !== null && stripExe(path.basename(h.binary)) === expected,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stripExe(name: string): string {
|
|
249
|
+
return name.replace(/\.(exe|cmd)$/i, '');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* One-line fix for whatever makes this provider unusable, most-blocking first:
|
|
254
|
+
* no config file → no master switch → nothing detected → detected-but-not-
|
|
255
|
+
* enabled → detected-and-enabled-but-no-transport. `null` when it works.
|
|
256
|
+
*/
|
|
257
|
+
function fixFor(o: {
|
|
258
|
+
provider: string;
|
|
259
|
+
detected: boolean;
|
|
260
|
+
available: boolean;
|
|
261
|
+
configuredMode: string | null;
|
|
262
|
+
enabledInConfig: boolean;
|
|
263
|
+
councilEnabled: boolean;
|
|
264
|
+
configPresent: boolean;
|
|
265
|
+
configPath: string;
|
|
266
|
+
reason: string | null;
|
|
267
|
+
}): string | null {
|
|
268
|
+
if (!o.configPresent) {
|
|
269
|
+
return (
|
|
270
|
+
`no council config — copy \`agents/templates/.ai-council.yml.example\` ` +
|
|
271
|
+
`to ${o.configPath}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (!o.councilEnabled) {
|
|
275
|
+
return `set \`enabled: true\` in ${o.configPath}`;
|
|
276
|
+
}
|
|
277
|
+
// A manual member needs neither a binary nor a key — the human is the
|
|
278
|
+
// transport. Telling them to install a CLI would be advice for a transport
|
|
279
|
+
// they explicitly opted out of.
|
|
280
|
+
if (o.configuredMode === 'manual') {
|
|
281
|
+
return o.enabledInConfig ? null : enableInstruction(o.provider, o.configPath);
|
|
282
|
+
}
|
|
283
|
+
if (!o.detected) {
|
|
284
|
+
const bin = PROVIDER_CLI_META[o.provider]?.[0] ?? o.provider;
|
|
285
|
+
return (
|
|
286
|
+
`nothing detected — install the \`${bin}\` CLI and log in, or add an ` +
|
|
287
|
+
`\`api_key_ref\` for ${o.provider}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!o.enabledInConfig) {
|
|
291
|
+
// Detected but not permitted: the gap this report exists to surface.
|
|
292
|
+
return enableInstruction(o.provider, o.configPath);
|
|
293
|
+
}
|
|
294
|
+
if (!o.available) {
|
|
295
|
+
return o.reason;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── rendering ───────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
const GLYPH_OK = '✅';
|
|
303
|
+
const GLYPH_NO = '❌';
|
|
304
|
+
const GLYPH_WARN = '⚠️';
|
|
305
|
+
|
|
306
|
+
/** `detected · authenticated · source · billing · enabled` for one provider. */
|
|
307
|
+
export function renderProviderLine(row: ProviderRow, configPath: string): string {
|
|
308
|
+
const parts = [
|
|
309
|
+
`detected ${row.detected ? GLYPH_OK : GLYPH_NO}`,
|
|
310
|
+
`authenticated ${row.authenticated ? GLYPH_OK : GLYPH_NO}`,
|
|
311
|
+
`source ${row.authSource ?? '—'}`,
|
|
312
|
+
`billing ${row.billing}`,
|
|
313
|
+
`enabled-in-config ${row.enabledInConfig ? GLYPH_OK : GLYPH_NO}`,
|
|
314
|
+
];
|
|
315
|
+
parts.push(`mode ${row.configuredMode ?? '—'}`);
|
|
316
|
+
if (row.transport !== null) {
|
|
317
|
+
parts.push(`auto→${row.transport}`);
|
|
318
|
+
}
|
|
319
|
+
if (row.cliCap !== null) {
|
|
320
|
+
const used = row.cliUsed ?? 0;
|
|
321
|
+
const glyph = used >= row.cliCap ? GLYPH_WARN : GLYPH_OK;
|
|
322
|
+
parts.push(`cli-quota ${used}/${row.cliCap} ${glyph}`);
|
|
323
|
+
}
|
|
324
|
+
// A detected-but-unpermitted provider is the case the report exists for —
|
|
325
|
+
// name the consent gap explicitly rather than leaving a bare ❌.
|
|
326
|
+
if (row.detected && !row.enabledInConfig) {
|
|
327
|
+
parts.push(`not allowed to spend — ${enableInstruction(row.provider, configPath)}`);
|
|
328
|
+
}
|
|
329
|
+
return `${row.provider}: ${parts.join(' · ')}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The full detection section, as lines. Hosts + versions, the provider →
|
|
334
|
+
* transport → billing table, budgets, and a reason + fix per unavailable
|
|
335
|
+
* capability.
|
|
336
|
+
*/
|
|
337
|
+
export function renderDetectionLines(report: DetectionReport): string[] {
|
|
338
|
+
const lines: string[] = ['detection:'];
|
|
339
|
+
|
|
340
|
+
const installed = report.hosts.filter((h) => h.installed);
|
|
341
|
+
lines.push(` hosts: ${installed.length}/${report.hosts.length} installed`);
|
|
342
|
+
for (const h of installed) {
|
|
343
|
+
const version = h.version === null ? 'no binary' : h.version;
|
|
344
|
+
lines.push(` ${h.id}: ${version}${h.binary === null ? '' : ` (${h.binary})`}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
lines.push(
|
|
348
|
+
` council config: ${report.council_config_present ? report.council_config_path : `absent (${report.council_config_path})`}` +
|
|
349
|
+
`${report.council_config_present ? ` · enabled ${report.council_enabled ? GLYPH_OK : GLYPH_NO} · defaults.mode ${report.defaults_mode ?? '—'}` : ''}`,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
lines.push(' providers:');
|
|
353
|
+
for (const row of report.providers) {
|
|
354
|
+
lines.push(` ${renderProviderLine(row, report.council_config_path)}`);
|
|
355
|
+
if (row.fix !== null) {
|
|
356
|
+
lines.push(` fix: ${row.fix}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const caps = Object.entries(report.budgets.cli_call_budget);
|
|
361
|
+
lines.push(
|
|
362
|
+
` budgets: cost_budget.max_total_usd ${report.budgets.cost_budget_max_total_usd ?? '—'}` +
|
|
363
|
+
` · cli_call_budget ${caps.length === 0 ? 'unset' : caps.map(([p, c]) => `${p}=${c}`).join(', ')}`,
|
|
364
|
+
);
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The first-invocation spend disclosure — built from the SAME rows as the table
|
|
370
|
+
* above, so the two can never disagree about which members spend money.
|
|
371
|
+
*
|
|
372
|
+
* Only enabled AND available members can spend, so only they appear. A member
|
|
373
|
+
* that is detected but not enabled is named as explicitly excluded, because
|
|
374
|
+
* "why is my provider not being consulted" is the question this answers.
|
|
375
|
+
*/
|
|
376
|
+
export function renderSpendDisclosure(report: DetectionReport): string {
|
|
377
|
+
// Bucket by the CONFIGURED mode, not by what `auto` could reach: a member
|
|
378
|
+
// pinned to `manual` makes no provider call regardless of what is installed,
|
|
379
|
+
// and telling the user it will hit an API would be exactly the drift this
|
|
380
|
+
// single-renderer design exists to prevent.
|
|
381
|
+
const spending = report.providers.filter(
|
|
382
|
+
(r) => r.enabledInConfig && (r.available || r.configuredMode === 'manual'),
|
|
383
|
+
);
|
|
384
|
+
const excludedTail = renderExcludedTail(report);
|
|
385
|
+
if (spending.length === 0) {
|
|
386
|
+
// The excluded list matters MOST here: "no call will be made" without it
|
|
387
|
+
// leaves the obvious next question ("but my provider IS installed?")
|
|
388
|
+
// unanswered.
|
|
389
|
+
return (
|
|
390
|
+
'council spend disclosure · no member is both enabled and available — ' +
|
|
391
|
+
`no call will be made.${excludedTail}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const free = spending.filter((r) => r.configuredMode === 'manual');
|
|
395
|
+
const billed = spending.filter((r) => r.configuredMode !== 'manual');
|
|
396
|
+
const metered = billed.filter((r) => r.billing === 'per-token');
|
|
397
|
+
const subscription = billed.filter((r) => r.billing === 'subscription');
|
|
398
|
+
|
|
399
|
+
const segments: string[] = [];
|
|
400
|
+
if (metered.length > 0) {
|
|
401
|
+
segments.push(
|
|
402
|
+
`${metered.length} metered (per-token): ` +
|
|
403
|
+
metered.map((r) => `${r.provider}→${r.transport}`).join(', '),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (subscription.length > 0) {
|
|
407
|
+
segments.push(
|
|
408
|
+
`${subscription.length} on subscription quota: ` +
|
|
409
|
+
subscription
|
|
410
|
+
.map(
|
|
411
|
+
(r) =>
|
|
412
|
+
`${r.provider}→${r.transport}` +
|
|
413
|
+
(r.cliCap === null ? '' : ` (${r.cliUsed ?? 0}/${r.cliCap} today)`),
|
|
414
|
+
)
|
|
415
|
+
.join(', '),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (free.length > 0) {
|
|
419
|
+
segments.push(`${free.length} manual (no provider call): ${free.map((r) => r.provider).join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return `council spend disclosure · ${segments.join(' · ')}${excludedTail}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The `detected but not allowed to spend` tail, or `''` when there is none. */
|
|
426
|
+
function renderExcludedTail(report: DetectionReport): string {
|
|
427
|
+
const excluded = report.providers.filter((r) => r.detected && !r.enabledInConfig);
|
|
428
|
+
if (excluded.length === 0) return '';
|
|
429
|
+
return ` · detected but not allowed to spend: ${excluded.map((r) => r.provider).join(', ')}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The `--json` payload for the detection section. Stable field order. */
|
|
433
|
+
export function detectionJson(report: DetectionReport): Record<string, unknown> {
|
|
434
|
+
return {
|
|
435
|
+
schema_version: report.schema_version,
|
|
436
|
+
council_config_path: report.council_config_path,
|
|
437
|
+
council_config_present: report.council_config_present,
|
|
438
|
+
council_enabled: report.council_enabled,
|
|
439
|
+
defaults_mode: report.defaults_mode,
|
|
440
|
+
hosts: report.hosts.map((h) => ({
|
|
441
|
+
id: h.id,
|
|
442
|
+
installed: h.installed,
|
|
443
|
+
binary: h.binary,
|
|
444
|
+
version: h.version,
|
|
445
|
+
})),
|
|
446
|
+
providers: report.providers.map((r) => ({
|
|
447
|
+
provider: r.provider,
|
|
448
|
+
detected: r.detected,
|
|
449
|
+
authenticated: r.authenticated,
|
|
450
|
+
auth_source: r.authSource,
|
|
451
|
+
billing: r.billing,
|
|
452
|
+
transport: r.transport,
|
|
453
|
+
configured_mode: r.configuredMode,
|
|
454
|
+
available: r.available,
|
|
455
|
+
enabled_in_config: r.enabledInConfig,
|
|
456
|
+
reason: r.reason,
|
|
457
|
+
fix: r.fix,
|
|
458
|
+
cli_cap: r.cliCap,
|
|
459
|
+
cli_used: r.cliUsed,
|
|
460
|
+
})),
|
|
461
|
+
budgets: {
|
|
462
|
+
cost_budget_max_total_usd: report.budgets.cost_budget_max_total_usd,
|
|
463
|
+
cli_call_budget: report.budgets.cli_call_budget,
|
|
464
|
+
},
|
|
465
|
+
spend_disclosure: renderSpendDisclosure(report),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Facts for a machine with no council config at all. */
|
|
470
|
+
export function absentCouncilFacts(configPath: string): CouncilFacts {
|
|
471
|
+
return {
|
|
472
|
+
configPath,
|
|
473
|
+
configPresent: false,
|
|
474
|
+
enabled: false,
|
|
475
|
+
defaultsMode: null,
|
|
476
|
+
members: {},
|
|
477
|
+
cliCaps: {},
|
|
478
|
+
costBudgetMaxUsd: null,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -346,16 +346,20 @@ print_version() {
|
|
|
346
346
|
fi
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
# Resolve the tsx runner (Python→TypeScript migration). tsx
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
#
|
|
349
|
+
# Resolve the tsx runner (Python→TypeScript migration). tsx is a DEV-ONLY
|
|
350
|
+
# dependency since the road-to-credible-install Phase 1 flip — the consumer
|
|
351
|
+
# runtime tree ships no tsx, so every consumer-reachable surface must resolve
|
|
352
|
+
# a precompiled bundle instead (hooks → dist/hooks/dispatch.js, mcp:run →
|
|
353
|
+
# dist/mcp/server.mjs, the `_cli` delegate commands → dist/cli-delegate/,
|
|
354
|
+
# see exec_ts below). This resolver therefore only ever fires in a DEV tree
|
|
355
|
+
# (node_modules/.bin/tsx present) or as a genuine last-resort fallback.
|
|
356
|
+
# `npx tsx` is that last resort: npx runs against the CONSUMER project's cwd,
|
|
357
|
+
# so its npm config and devEngines/engines constraints apply — a consumer
|
|
358
|
+
# pinning e.g. `node <24` hard-fails with EBADDEVENGINES (the 8.1.0
|
|
359
|
+
# regression that silently broke hooks + roadmap:progress in consumer
|
|
360
|
+
# projects). Reaching it from a consumer-tier command is a packaging bug, not
|
|
361
|
+
# a supported path. Exits 127 when nothing works — the runtime is Node/tsx,
|
|
362
|
+
# there is no Python fallback.
|
|
359
363
|
require_tsx() {
|
|
360
364
|
if [[ -x "$PACKAGE_ROOT/node_modules/.bin/tsx" ]]; then
|
|
361
365
|
TSX_BIN="$PACKAGE_ROOT/node_modules/.bin/tsx"
|
|
@@ -378,14 +382,48 @@ require_tsx() {
|
|
|
378
382
|
exit 127
|
|
379
383
|
}
|
|
380
384
|
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
+
# Map a `src/scripts/_cli/<name>.ts` path to its precompiled bundle entry
|
|
386
|
+
# under `dist/cli-delegate/<name>.js`, echoing the bundle path when it exists
|
|
387
|
+
# and node is available. Empty output means "no bundle — use the tsx path".
|
|
388
|
+
#
|
|
389
|
+
# Scoped to `_cli/` on purpose: that directory is the consumer-tier delegate
|
|
390
|
+
# command surface (`sync`, `doctor`, `validate`, `upgrade`, …) and its
|
|
391
|
+
# basenames are unique, so the mapping is collision-free. Other exec_ts
|
|
392
|
+
# callers (resolve_script results, the work engine, …) are unaffected.
|
|
393
|
+
cli_delegate_bundle() {
|
|
394
|
+
local ts_abs="$1"
|
|
395
|
+
case "$ts_abs" in
|
|
396
|
+
"$PACKAGE_ROOT/src/scripts/_cli/"*) ;;
|
|
397
|
+
*) return 0 ;;
|
|
398
|
+
esac
|
|
399
|
+
local name
|
|
400
|
+
name="$(basename "${ts_abs%.ts}")"
|
|
401
|
+
local bundle="$PACKAGE_ROOT/dist/cli-delegate/$name.js"
|
|
402
|
+
if [[ -f "$bundle" ]] && command -v node >/dev/null 2>&1; then
|
|
403
|
+
printf '%s' "$bundle"
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# Run an absolute script path. The argument is an absolute path that may carry
|
|
408
|
+
# a `.py` (legacy) or `.ts` extension; we resolve the `.ts` twin.
|
|
409
|
+
#
|
|
410
|
+
# A precompiled `dist/cli-delegate/` bundle wins when present — the consumer
|
|
411
|
+
# tree ships no tsx (see require_tsx), and the bundle is also ~5.7x faster
|
|
412
|
+
# than the npx path it replaces (p50 56-71 ms vs 346-392 ms, measured
|
|
413
|
+
# 2026-07-31; see docs/decisions/ADR-204). The tsx path stays as the dev-tree
|
|
414
|
+
# route and as the fallback for every non-`_cli` caller.
|
|
415
|
+
#
|
|
416
|
+
# Argv/stdin/stdout/stderr/exit-code pass through unchanged in both routes —
|
|
417
|
+
# mirrors src/scripts/run.ts resolution.
|
|
385
418
|
exec_ts() {
|
|
386
419
|
local script="$1"; shift
|
|
387
420
|
local ts_abs="${script%.py}"
|
|
388
421
|
ts_abs="${ts_abs%.ts}.ts"
|
|
422
|
+
local bundle
|
|
423
|
+
bundle="$(cli_delegate_bundle "$ts_abs")"
|
|
424
|
+
if [[ -n "$bundle" ]]; then
|
|
425
|
+
exec node "$bundle" "$@"
|
|
426
|
+
fi
|
|
389
427
|
if [[ ! -f "$ts_abs" ]]; then
|
|
390
428
|
echo "❌ agent-config: script not found: $ts_abs" >&2
|
|
391
429
|
exit 127
|