@event4u/agent-config 12.1.0 → 14.0.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- 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 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +101 -164
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -78,6 +78,8 @@ export function _resetModelCeilingMemo(): void {
|
|
|
78
78
|
_modelCeilingMemo.clear();
|
|
79
79
|
}
|
|
80
80
|
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
81
|
+
import { jsonDumpsIndent2 as _jsonDumpsIndent2 } from './_py_json.js';
|
|
82
|
+
import * as budget from './cli_call_budget.js';
|
|
81
83
|
import { appendEvent } from './events_log.js';
|
|
82
84
|
|
|
83
85
|
export const ANTHROPIC_KEY_FILENAME = 'anthropic.key';
|
|
@@ -453,6 +455,12 @@ interface CliClientOptions {
|
|
|
453
455
|
max_calls_per_day?: number | null | undefined;
|
|
454
456
|
warn_at?: number | undefined;
|
|
455
457
|
cli_calls_path?: string | null | undefined;
|
|
458
|
+
/**
|
|
459
|
+
* Who is booking these calls — a `CLI_CONSUMER_*` constant, declared by the
|
|
460
|
+
* construction site because the client cannot know its caller. Omitted →
|
|
461
|
+
* `CLI_CONSUMER_UNKNOWN`, which is a finding rather than a default.
|
|
462
|
+
*/
|
|
463
|
+
consumer?: string | undefined;
|
|
456
464
|
}
|
|
457
465
|
|
|
458
466
|
/**
|
|
@@ -987,6 +995,17 @@ export class PerplexityClient extends _OpenAICompatibleClient {
|
|
|
987
995
|
|
|
988
996
|
export const CLI_CALLS_FILENAME = 'cli-calls.json';
|
|
989
997
|
|
|
998
|
+
// Durability, attribution and cap resolution live in `cli_call_budget.ts` — see
|
|
999
|
+
// its header for why the dependency runs one way only.
|
|
1000
|
+
export {
|
|
1001
|
+
CLI_CALLS_ATTRIBUTION_SUFFIX,
|
|
1002
|
+
CLI_CONSUMER_COUNCIL,
|
|
1003
|
+
CLI_CONSUMER_TEAM,
|
|
1004
|
+
CLI_CONSUMER_UNKNOWN,
|
|
1005
|
+
QUOTA_SOURCE_LOCAL_BUDGET,
|
|
1006
|
+
QUOTA_SOURCE_PROVIDER,
|
|
1007
|
+
} from './cli_call_budget.js';
|
|
1008
|
+
|
|
990
1009
|
// Default subprocess timeout (seconds) for a single CLI call. Long enough for
|
|
991
1010
|
// the largest frontier models to think; short enough to surface a hung
|
|
992
1011
|
// subprocess without freezing the council run.
|
|
@@ -1056,18 +1075,39 @@ export function load_cli_call_counts(p: string | null = null): Record<string, nu
|
|
|
1056
1075
|
return out;
|
|
1057
1076
|
}
|
|
1058
1077
|
|
|
1059
|
-
/**
|
|
1060
|
-
|
|
1078
|
+
/**
|
|
1079
|
+
* Return today's `provider → consumer → count` attribution. Fail-soft: an
|
|
1080
|
+
* absent, stale, or malformed sidecar reads as `{}` and never gates a call.
|
|
1081
|
+
*/
|
|
1082
|
+
export function load_cli_call_attribution(
|
|
1083
|
+
p: string | null = null,
|
|
1084
|
+
): Record<string, Record<string, number>> {
|
|
1061
1085
|
const target = p !== null ? p : _cliCallsStatePath();
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1086
|
+
return budget.readAttribution(target, _today_utc_iso());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Increment today's call count for `provider`. Returns the new total.
|
|
1091
|
+
*
|
|
1092
|
+
* `consumer` names who booked it and lands in the sidecar, never in the counter
|
|
1093
|
+
* the gate reads. It defaults to `CLI_CONSUMER_UNKNOWN` so an undeclared call
|
|
1094
|
+
* path is visible as a finding rather than attributed to whoever happens to be
|
|
1095
|
+
* first in the enumeration.
|
|
1096
|
+
*/
|
|
1097
|
+
export function record_cli_call(
|
|
1098
|
+
provider: string,
|
|
1099
|
+
p: string | null = null,
|
|
1100
|
+
consumer: string = budget.CLI_CONSUMER_UNKNOWN,
|
|
1101
|
+
): number {
|
|
1102
|
+
const target = p !== null ? p : _cliCallsStatePath();
|
|
1103
|
+
const today = _today_utc_iso();
|
|
1104
|
+
return budget.withStateLock(target, () => {
|
|
1105
|
+
const counts = load_cli_call_counts(target);
|
|
1106
|
+
counts[provider] = (counts[provider] ?? 0) + 1;
|
|
1107
|
+
budget.writeStateAtomically(target, _jsonDumpsIndent2({ date: today, counts }));
|
|
1108
|
+
budget.recordAttribution(target, provider, consumer, today, _jsonDumpsIndent2);
|
|
1109
|
+
return counts[provider] as number;
|
|
1110
|
+
});
|
|
1071
1111
|
}
|
|
1072
1112
|
|
|
1073
1113
|
/**
|
|
@@ -1082,57 +1122,29 @@ export function reset_cli_call_counts(
|
|
|
1082
1122
|
p: string | null = null,
|
|
1083
1123
|
): Record<string, number> {
|
|
1084
1124
|
const target = p !== null ? p : _cliCallsStatePath();
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
counts =
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
target,
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
);
|
|
1097
|
-
return counts;
|
|
1125
|
+
const today = _today_utc_iso();
|
|
1126
|
+
return budget.withStateLock(target, () => {
|
|
1127
|
+
let counts = load_cli_call_counts(target);
|
|
1128
|
+
if (provider === null) {
|
|
1129
|
+
counts = {};
|
|
1130
|
+
} else {
|
|
1131
|
+
delete counts[provider];
|
|
1132
|
+
}
|
|
1133
|
+
budget.writeStateAtomically(target, _jsonDumpsIndent2({ date: today, counts }));
|
|
1134
|
+
budget.resetAttribution(target, provider, today, _jsonDumpsIndent2);
|
|
1135
|
+
return counts;
|
|
1136
|
+
});
|
|
1098
1137
|
}
|
|
1099
1138
|
|
|
1100
1139
|
/**
|
|
1101
|
-
* Build the pre-run quota summary line (step-8 P1, D1 + D4).
|
|
1102
|
-
*
|
|
1103
|
-
* Returns `[summary, warn_providers]` where `summary` is the formatted
|
|
1104
|
-
* one-liner (empty string when no CLI member has a configured cap) and
|
|
1105
|
-
* `warn_providers` is the subset whose `used / max_calls_per_day` ratio crossed
|
|
1106
|
-
* `warn_at`. Uncapped providers (`max_calls_per_day is None`) are omitted from
|
|
1107
|
-
* the summary entirely — they cannot exceed a threshold that does not exist.
|
|
1140
|
+
* Build the pre-run quota summary line (step-8 P1, D1 + D4). Formatting lives in
|
|
1141
|
+
* `cli_call_budget.ts`; this reads the counter and delegates.
|
|
1108
1142
|
*/
|
|
1109
1143
|
export function quota_summary_line(
|
|
1110
1144
|
clients: CliClient[],
|
|
1111
1145
|
opts: { cli_calls_path?: string | null } = {},
|
|
1112
1146
|
): [string, string[]] {
|
|
1113
|
-
|
|
1114
|
-
// Python: [c for c in clients if getattr(c, "max_calls_per_day", None)]
|
|
1115
|
-
// → truthy check (None or 0 both falsy).
|
|
1116
|
-
const capped = clients.filter((c) => _pyTruthy(_getattr(c, 'max_calls_per_day', null)));
|
|
1117
|
-
if (capped.length === 0) {
|
|
1118
|
-
return ['', []];
|
|
1119
|
-
}
|
|
1120
|
-
const counts = load_cli_call_counts(cliCallsPath);
|
|
1121
|
-
const parts: string[] = [];
|
|
1122
|
-
const warn: string[] = [];
|
|
1123
|
-
for (const c of capped) {
|
|
1124
|
-
const name = _getattr(c, 'name', '?') as string;
|
|
1125
|
-
const used = _pyInt(counts[name] ?? 0);
|
|
1126
|
-
const limit = _pyInt((c.max_calls_per_day as number));
|
|
1127
|
-
parts.push(`${name} ${used}/${limit}`);
|
|
1128
|
-
const ratio = limit > 0 ? used / limit : 0.0;
|
|
1129
|
-
const warnAt = Number(_getattr(c, 'warn_at', 0.8));
|
|
1130
|
-
if (ratio >= warnAt) {
|
|
1131
|
-
warn.push(name);
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
const prefix = warn.length > 0 ? '⚠️ ' : '';
|
|
1135
|
-
return [`${prefix}council:quota · ${parts.join(' · ')}`, warn];
|
|
1147
|
+
return budget.quotaSummaryLine(clients, load_cli_call_counts(opts.cli_calls_path ?? null));
|
|
1136
1148
|
}
|
|
1137
1149
|
|
|
1138
1150
|
/** Transport-seam result, shaped like a finished `subprocess.run`. */
|
|
@@ -1196,6 +1208,8 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1196
1208
|
warn_at: number;
|
|
1197
1209
|
binary!: string;
|
|
1198
1210
|
protected _cli_calls_path: string | null;
|
|
1211
|
+
/** Attribution label for every booking this client makes. */
|
|
1212
|
+
protected _consumer: string;
|
|
1199
1213
|
|
|
1200
1214
|
static _AUTH_FAILURE_PATTERNS: readonly string[] = [
|
|
1201
1215
|
'authentication',
|
|
@@ -1239,6 +1253,7 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1239
1253
|
this.max_calls_per_day = opts.max_calls_per_day ?? null;
|
|
1240
1254
|
this.warn_at = opts.warn_at ?? 0.8;
|
|
1241
1255
|
this._cli_calls_path = opts.cli_calls_path ?? null;
|
|
1256
|
+
this._consumer = opts.consumer ?? budget.CLI_CONSUMER_UNKNOWN;
|
|
1242
1257
|
const binary = opts.binary ?? null;
|
|
1243
1258
|
if (binary !== null) {
|
|
1244
1259
|
this.binary = binary;
|
|
@@ -1287,7 +1302,7 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1287
1302
|
*/
|
|
1288
1303
|
protected _recordCallQuietly(): void {
|
|
1289
1304
|
try {
|
|
1290
|
-
record_cli_call(this.name, this._cli_calls_path);
|
|
1305
|
+
record_cli_call(this.name, this._cli_calls_path, this._consumer);
|
|
1291
1306
|
} catch {
|
|
1292
1307
|
// state-file write failure is non-fatal here.
|
|
1293
1308
|
}
|
|
@@ -1441,6 +1456,9 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1441
1456
|
cli: true,
|
|
1442
1457
|
cli_calls_used: used,
|
|
1443
1458
|
cli_calls_max: this.max_calls_per_day,
|
|
1459
|
+
// Our counter refused before any spawn — nothing sent,
|
|
1460
|
+
// nothing booked for this call.
|
|
1461
|
+
quota_source: budget.QUOTA_SOURCE_LOCAL_BUDGET,
|
|
1444
1462
|
},
|
|
1445
1463
|
});
|
|
1446
1464
|
}
|
|
@@ -1549,6 +1567,13 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1549
1567
|
cli: true,
|
|
1550
1568
|
returncode: proc.returncode,
|
|
1551
1569
|
stderr_tail: (proc.stderr || '').slice(-500),
|
|
1570
|
+
// The vendor refused us; the local-budget refusal above
|
|
1571
|
+
// returns before spawning. Opposite remedies, one error
|
|
1572
|
+
// string — so discriminate explicitly rather than leaving it
|
|
1573
|
+
// inferable from which metadata keys happen to be absent.
|
|
1574
|
+
...(code === 'cli_quota_exhausted'
|
|
1575
|
+
? { quota_source: budget.QUOTA_SOURCE_PROVIDER }
|
|
1576
|
+
: {}),
|
|
1552
1577
|
},
|
|
1553
1578
|
});
|
|
1554
1579
|
}
|
|
@@ -1679,23 +1704,23 @@ export class AnthropicCliClient extends CliClient {
|
|
|
1679
1704
|
// documented "disable all tools" value.
|
|
1680
1705
|
//
|
|
1681
1706
|
// It also repairs a live failure, which is how the grant was found
|
|
1682
|
-
// rather than reasoned about.
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
// In this package that overflowed the model's context window
|
|
1688
|
-
// outright: `is_error: true`, `"the request is ~214331 tokens
|
|
1707
|
+
// rather than reasoned about. AT THE TIME (2026-08-12) the spawn
|
|
1708
|
+
// inherited the caller's cwd, so invoking the council from inside a
|
|
1709
|
+
// repository made the child load that project's own instructions AND
|
|
1710
|
+
// every tool definition. In this package that overflowed the context
|
|
1711
|
+
// window outright: `is_error: true`, `"the request is ~214331 tokens
|
|
1689
1712
|
// (limit 200000) … the rest is system prompt, tool definitions"`,
|
|
1690
|
-
// exit 1, empty stderr —
|
|
1691
|
-
//
|
|
1692
|
-
//
|
|
1693
|
-
// returns the worktree call to exit 0.
|
|
1713
|
+
// exit 1, empty stderr — the member reported unavailable with no
|
|
1714
|
+
// diagnosable cause. Identical call from `/tmp` exit 0, from the
|
|
1715
|
+
// worktree exit 1, and this one flag returns the worktree to exit 0.
|
|
1694
1716
|
//
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1717
|
+
// TENSE CORRECTED 2026-08-16. This block read "`_runSubprocess` sets
|
|
1718
|
+
// none, deliberately" and "dropping the tools RATHER THAN
|
|
1719
|
+
// neutralising the cwd" — both false since `_runSubprocess` set
|
|
1720
|
+
// `cwd: os.tmpdir()` for a DIFFERENT contamination (a member
|
|
1721
|
+
// answering this repo's `end-review-nudge`). Two repairs, one
|
|
1722
|
+
// cause, neither aware of the other, so a reader here concluded
|
|
1723
|
+
// the cwd is inherited. Both are live; this flag bounds what a member may DO.
|
|
1699
1724
|
'--tools',
|
|
1700
1725
|
'',
|
|
1701
1726
|
'--append-system-prompt',
|
|
@@ -1836,10 +1861,10 @@ export class OpenAICliClient extends CliClient {
|
|
|
1836
1861
|
`"The '${this.model}' model is not supported when using Codex with a ` +
|
|
1837
1862
|
'ChatGPT account." The CLI publishes no list of models it DOES accept, ' +
|
|
1838
1863
|
'so there is no supported set to offer here — the one configuration ' +
|
|
1839
|
-
'measured to work is no pin at all. Fix:
|
|
1840
|
-
'the openai member
|
|
1841
|
-
'
|
|
1842
|
-
'
|
|
1864
|
+
'measured to work is no pin at all. Fix: set `model: codex-default` on ' +
|
|
1865
|
+
'the openai member. Do NOT delete the `model:` line — an enabled member ' +
|
|
1866
|
+
'with no model fails config load, so that takes EVERY provider to NOT ' +
|
|
1867
|
+
'CONFIGURED. A pin is reachable only on the API transport.',
|
|
1843
1868
|
};
|
|
1844
1869
|
}
|
|
1845
1870
|
|
|
@@ -1985,8 +2010,8 @@ export class OpenAICliClient extends CliClient {
|
|
|
1985
2010
|
? {
|
|
1986
2011
|
hint:
|
|
1987
2012
|
'the codex subscription transport serves no explicitly pinned ' +
|
|
1988
|
-
'model measured so far —
|
|
1989
|
-
'openai member
|
|
2013
|
+
'model measured so far — set `model: codex-default` on the ' +
|
|
2014
|
+
'openai member; deleting the line fails config load',
|
|
1990
2015
|
}
|
|
1991
2016
|
: {}),
|
|
1992
2017
|
},
|
|
@@ -2719,94 +2744,6 @@ function _excMessage(exc: unknown): string {
|
|
|
2719
2744
|
return String(exc);
|
|
2720
2745
|
}
|
|
2721
2746
|
|
|
2722
|
-
/** `json.dumps(obj, indent=2)` — default ensure_ascii=True, insertion-order keys. */
|
|
2723
|
-
function _jsonDumpsIndent2(value: unknown): string {
|
|
2724
|
-
return _jsonDumpsIndented(value, 2, 0);
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
function _jsonDumpsIndented(value: unknown, indent: number, level: number): string {
|
|
2728
|
-
if (value === null || value === undefined) {
|
|
2729
|
-
return 'null';
|
|
2730
|
-
}
|
|
2731
|
-
switch (typeof value) {
|
|
2732
|
-
case 'boolean':
|
|
2733
|
-
return value ? 'true' : 'false';
|
|
2734
|
-
case 'number':
|
|
2735
|
-
return _pyJsonNumber(value);
|
|
2736
|
-
case 'string':
|
|
2737
|
-
return _pyJsonStringAscii(value);
|
|
2738
|
-
case 'object':
|
|
2739
|
-
break;
|
|
2740
|
-
default:
|
|
2741
|
-
throw new TypeError(`Object of type ${typeof value} is not JSON serializable`);
|
|
2742
|
-
}
|
|
2743
|
-
const pad = ' '.repeat(indent * (level + 1));
|
|
2744
|
-
const closePad = ' '.repeat(indent * level);
|
|
2745
|
-
if (Array.isArray(value)) {
|
|
2746
|
-
if (value.length === 0) {
|
|
2747
|
-
return '[]';
|
|
2748
|
-
}
|
|
2749
|
-
const items = value.map((v) => pad + _jsonDumpsIndented(v, indent, level + 1));
|
|
2750
|
-
return `[\n${items.join(',\n')}\n${closePad}]`;
|
|
2751
|
-
}
|
|
2752
|
-
const obj = value as Record<string, unknown>;
|
|
2753
|
-
const keys = Object.keys(obj);
|
|
2754
|
-
if (keys.length === 0) {
|
|
2755
|
-
return '{}';
|
|
2756
|
-
}
|
|
2757
|
-
const items = keys.map(
|
|
2758
|
-
(k) => `${pad}${_pyJsonStringAscii(k)}: ${_jsonDumpsIndented(obj[k], indent, level + 1)}`,
|
|
2759
|
-
);
|
|
2760
|
-
return `{\n${items.join(',\n')}\n${closePad}}`;
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
/** Render a number like Python `json.dumps` (int vs float; JS has one type). */
|
|
2764
|
-
function _pyJsonNumber(n: number): string {
|
|
2765
|
-
if (!Number.isFinite(n)) {
|
|
2766
|
-
if (Number.isNaN(n)) {
|
|
2767
|
-
return 'NaN';
|
|
2768
|
-
}
|
|
2769
|
-
return n > 0 ? 'Infinity' : '-Infinity';
|
|
2770
|
-
}
|
|
2771
|
-
return String(n);
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
/** Escape a string like Python `json.dumps(..., ensure_ascii=True)` (default). */
|
|
2775
|
-
function _pyJsonStringAscii(s: string): string {
|
|
2776
|
-
let out = '"';
|
|
2777
|
-
for (const ch of s) {
|
|
2778
|
-
const code = ch.codePointAt(0) ?? 0;
|
|
2779
|
-
if (ch === '"') {
|
|
2780
|
-
out += '\\"';
|
|
2781
|
-
} else if (ch === '\\') {
|
|
2782
|
-
out += '\\\\';
|
|
2783
|
-
} else if (ch === '\b') {
|
|
2784
|
-
out += '\\b';
|
|
2785
|
-
} else if (ch === '\f') {
|
|
2786
|
-
out += '\\f';
|
|
2787
|
-
} else if (ch === '\n') {
|
|
2788
|
-
out += '\\n';
|
|
2789
|
-
} else if (ch === '\r') {
|
|
2790
|
-
out += '\\r';
|
|
2791
|
-
} else if (ch === '\t') {
|
|
2792
|
-
out += '\\t';
|
|
2793
|
-
} else if (code < 0x20) {
|
|
2794
|
-
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
2795
|
-
} else if (code < 0x7f) {
|
|
2796
|
-
out += ch;
|
|
2797
|
-
} else if (code <= 0xffff) {
|
|
2798
|
-
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
2799
|
-
} else {
|
|
2800
|
-
// Astral plane → surrogate pair, matching Python json.dumps default.
|
|
2801
|
-
const c = code - 0x10000;
|
|
2802
|
-
const hi = 0xd800 + (c >> 10);
|
|
2803
|
-
const lo = 0xdc00 + (c & 0x3ff);
|
|
2804
|
-
out += `\\u${hi.toString(16).padStart(4, '0')}\\u${lo.toString(16).padStart(4, '0')}`;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
return out + '"';
|
|
2808
|
-
}
|
|
2809
|
-
|
|
2810
2747
|
/** Python `shutil.which(name)` — resolve an executable on PATH. */
|
|
2811
2748
|
function _which(name: string): string | null {
|
|
2812
2749
|
// Absolute / relative path with a separator: check directly (Python's which
|
|
@@ -76,6 +76,7 @@ import * as path from 'node:path';
|
|
|
76
76
|
import { parse as parseYaml } from 'yaml';
|
|
77
77
|
|
|
78
78
|
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
79
|
+
import * as budget from './cli_call_budget.js';
|
|
79
80
|
// LAYERING CONSTRAINT, load-bearing: this is a VALUE import from quorum, while
|
|
80
81
|
// `quorum.ts` imports `QuorumSetting` back from here. That back-edge is
|
|
81
82
|
// `import type` and is erased at build, so there is no runtime cycle today.
|
|
@@ -1927,13 +1928,23 @@ function _build_member(
|
|
|
1927
1928
|
* Generous per-provider guard for `mode: cli` / `mode: auto` members
|
|
1928
1929
|
* (road-to-always-on-orchestration Phase 3.4). Before this default the map
|
|
1929
1930
|
* shipped empty and an unlisted provider ran uncapped — a plan-quota guard
|
|
1930
|
-
* that only existed if the user remembered to opt in.
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1931
|
+
* that only existed if the user remembered to opt in.
|
|
1932
|
+
*
|
|
1933
|
+
* Re-exported from `cli_call_budget.ts`, which owns it: `build_members` and
|
|
1934
|
+
* `cmd_quota` both need the same number, and re-deriving it at either site is
|
|
1935
|
+
* how the reported cap and the enforced cap drift apart.
|
|
1935
1936
|
*/
|
|
1936
|
-
const
|
|
1937
|
+
export const DEFAULT_CLI_CALLS_PER_DAY = budget.DEFAULT_CLI_CALLS_PER_DAY;
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Resolve the per-provider daily cap map — the SINGLE authority, shared with the
|
|
1941
|
+
* gate and the report. Thin wrapper that supplies this module's provider set;
|
|
1942
|
+
* the contract (defaults on omission, lenient where this module is strict) is
|
|
1943
|
+
* documented on `budget.resolveCliCallCaps`.
|
|
1944
|
+
*/
|
|
1945
|
+
export function resolve_cli_call_caps(raw: unknown): Record<string, number> {
|
|
1946
|
+
return budget.resolveCliCallCaps(raw, _VALID_PROVIDERS);
|
|
1947
|
+
}
|
|
1937
1948
|
|
|
1938
1949
|
function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
|
|
1939
1950
|
if (!_isDict(d)) {
|
|
@@ -1945,10 +1956,11 @@ function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
|
|
|
1945
1956
|
'`cli_call_budget.max_calls_per_day` must be a mapping.',
|
|
1946
1957
|
);
|
|
1947
1958
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1959
|
+
// Validate FIRST, then resolve. Splitting the two is what lets the strict
|
|
1960
|
+
// builder and the lenient `resolve_cli_call_caps` share one seeding
|
|
1961
|
+
// implementation without either weakening the other: every rejection below
|
|
1962
|
+
// still throws, and what survives is handed to the shared resolver rather
|
|
1963
|
+
// than seeded a second time here.
|
|
1952
1964
|
for (const [provider, value] of Object.entries(raw_caps)) {
|
|
1953
1965
|
if (!_VALID_PROVIDERS.has(provider)) {
|
|
1954
1966
|
throw new CouncilConfigError(
|
|
@@ -1962,8 +1974,10 @@ function _build_cli_call_budget(d: Dict): CliCallBudgetConfig {
|
|
|
1962
1974
|
`non-negative integer (got ${_pyRepr(value)}).`,
|
|
1963
1975
|
);
|
|
1964
1976
|
}
|
|
1965
|
-
caps.set(provider, value);
|
|
1966
1977
|
}
|
|
1978
|
+
const caps = new Map<string, number>(
|
|
1979
|
+
Object.entries(resolve_cli_call_caps(raw_caps)),
|
|
1980
|
+
);
|
|
1967
1981
|
const warn_at_raw = _get(d, 'warn_at', 0.8);
|
|
1968
1982
|
if (_isBool(warn_at_raw) || !_isNumber(warn_at_raw)) {
|
|
1969
1983
|
throw new CouncilConfigError(
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The observation half of provider qualification (road-to-release-review-p0
|
|
3
|
+
* Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* `qualification.ts` is pure and answers "given what has been observed, what
|
|
6
|
+
* may this seat claim". This module is the only thing that observes. It exists
|
|
7
|
+
* because the qualification ladder's strongest rung — has a real exchange with
|
|
8
|
+
* this provider ever succeeded — cannot be answered from configuration, and
|
|
9
|
+
* answering it with a fresh probe would mean spending money every time anyone
|
|
10
|
+
* asked whether the council was healthy.
|
|
11
|
+
*
|
|
12
|
+
* So nothing here probes. A council run that receives a non-empty answer from a
|
|
13
|
+
* seat has already performed the exchange; this records that it happened. The
|
|
14
|
+
* cost of the observation is zero because the observation is a by-product of
|
|
15
|
+
* work the operator asked for anyway, and a seat therefore decays out of
|
|
16
|
+
* `unknown` through normal use rather than through a maintenance ritual.
|
|
17
|
+
*
|
|
18
|
+
* **The store is deliberately runtime-local and gitignored.** `agents/runtime/`
|
|
19
|
+
* is not tracked, which has a consequence worth stating rather than
|
|
20
|
+
* discovering: a fresh worktree starts with no observations, so every seat
|
|
21
|
+
* reads `unknown` there until a run happens in it. That is the honest reading —
|
|
22
|
+
* a checkout that has never talked to a provider genuinely has no evidence that
|
|
23
|
+
* it can — and it is strictly better than the alternative of committing a
|
|
24
|
+
* machine-specific health claim into a shared tree, where it would be stale for
|
|
25
|
+
* everyone but the machine that wrote it.
|
|
26
|
+
*
|
|
27
|
+
* Corrupt or unreadable state is treated as "no observations", never as an
|
|
28
|
+
* error: the same tolerant-reader doctrine the settings surface follows. A
|
|
29
|
+
* status command that crashes because a JSON file has a stray byte is worse
|
|
30
|
+
* than one that reports `unknown`, which is what the absence of evidence
|
|
31
|
+
* actually means.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
|
|
37
|
+
import { isEnvKillSwitchActive } from '../_lib/env_kill_switch.js';
|
|
38
|
+
import type { CliFailureClass } from './transport_resolver.js';
|
|
39
|
+
import type { ProbeRecord } from './qualification.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The same kill-switch the council event log honours.
|
|
43
|
+
*
|
|
44
|
+
* Sharing it rather than minting a second name is deliberate: both write
|
|
45
|
+
* council observational state into the worktree, and the suite that needs one
|
|
46
|
+
* suppressed needs the other suppressed for the identical reason. The
|
|
47
|
+
* read-only witness in `tests/scripts/witness/` fails on ANY worktree path
|
|
48
|
+
* appearing while it runs — including a gitignored one — so a test that
|
|
49
|
+
* exercises `cmd_run` and silently dropped a probe row here would break that
|
|
50
|
+
* witness whenever sharding put the two files together. Measured on this
|
|
51
|
+
* branch: the first version of this module had no switch and wrote
|
|
52
|
+
* `agents/runtime/state/council-probes.json` during the council suite.
|
|
53
|
+
*
|
|
54
|
+
* The coupling has a real cost worth stating: an operator who suppresses the
|
|
55
|
+
* event log also stops accumulating observations, so seats stay `unknown`.
|
|
56
|
+
* That is the correct direction — a run whose telemetry is switched off did
|
|
57
|
+
* not record evidence, and pretending otherwise is the over-claim this whole
|
|
58
|
+
* module exists to remove.
|
|
59
|
+
*/
|
|
60
|
+
const _KILL_SWITCH_ENV = 'AGENT_CONFIG_NO_EVENTS_LOG';
|
|
61
|
+
|
|
62
|
+
/** Repo-relative location. Runtime state, gitignored, never committed. */
|
|
63
|
+
export const PROBE_STORE_RELPATH = path.join('agents', 'runtime', 'state', 'council-probes.json');
|
|
64
|
+
|
|
65
|
+
export interface ProbeStore {
|
|
66
|
+
readonly schema: 1;
|
|
67
|
+
readonly members: Readonly<Record<string, ProbeRecord>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const EMPTY: ProbeStore = { schema: 1, members: {} };
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The closed set a stored `outcome` may hold — `ok` plus every
|
|
74
|
+
* `CliFailureClass`. Duplicated as a runtime value because the type erases,
|
|
75
|
+
* and this is a trust boundary: the file is hand-editable.
|
|
76
|
+
*/
|
|
77
|
+
const VALID_OUTCOMES: ReadonlySet<string> = new Set<'ok' | CliFailureClass>([
|
|
78
|
+
'ok',
|
|
79
|
+
'binary_missing',
|
|
80
|
+
'auth_rejected',
|
|
81
|
+
'cli_unsupported',
|
|
82
|
+
'model_unservable',
|
|
83
|
+
'timeout',
|
|
84
|
+
'server_error',
|
|
85
|
+
'quota_exhausted',
|
|
86
|
+
'other',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export function probeStorePath(root: string): string {
|
|
90
|
+
return path.join(root, PROBE_STORE_RELPATH);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read the store. Any failure — missing, unreadable, malformed, wrong schema —
|
|
95
|
+
* yields the empty store.
|
|
96
|
+
*
|
|
97
|
+
* Individual member entries are validated one by one rather than trusting the
|
|
98
|
+
* object wholesale: a single corrupt row must not discard the other seats'
|
|
99
|
+
* evidence, because that would silently downgrade healthy seats to `unknown`
|
|
100
|
+
* and the operator would have no way to tell the two causes apart.
|
|
101
|
+
*/
|
|
102
|
+
export function readProbeStore(root: string): ProbeStore {
|
|
103
|
+
let raw: string;
|
|
104
|
+
try {
|
|
105
|
+
raw = fs.readFileSync(probeStorePath(root), 'utf8');
|
|
106
|
+
} catch {
|
|
107
|
+
return EMPTY;
|
|
108
|
+
}
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
return EMPTY;
|
|
114
|
+
}
|
|
115
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
116
|
+
return EMPTY;
|
|
117
|
+
}
|
|
118
|
+
const obj = parsed as Record<string, unknown>;
|
|
119
|
+
if (obj['schema'] !== 1) {
|
|
120
|
+
return EMPTY;
|
|
121
|
+
}
|
|
122
|
+
const membersRaw = obj['members'];
|
|
123
|
+
if (typeof membersRaw !== 'object' || membersRaw === null) {
|
|
124
|
+
return EMPTY;
|
|
125
|
+
}
|
|
126
|
+
const members: Record<string, ProbeRecord> = {};
|
|
127
|
+
for (const [name, value] of Object.entries(membersRaw as Record<string, unknown>)) {
|
|
128
|
+
if (typeof value !== 'object' || value === null) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const v = value as Record<string, unknown>;
|
|
132
|
+
const at = v['at'];
|
|
133
|
+
const outcome = v['outcome'];
|
|
134
|
+
if (typeof at !== 'string' || at === '' || typeof outcome !== 'string') {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Validated against the closed set, not cast into it. A bare
|
|
138
|
+
// non-empty-string check let a corrupt or hand-edited value through,
|
|
139
|
+
// where `_liveProbe` read it as `!== 'ok'` and not impaired — i.e. a
|
|
140
|
+
// typo silently downgraded a healthy seat to `unavailable`
|
|
141
|
+
// (R2 finding 9). Dropping the row instead yields `unknown`, which is
|
|
142
|
+
// the honest reading of "this record is unreadable".
|
|
143
|
+
if (!VALID_OUTCOMES.has(outcome)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
members[name] = { at, outcome: outcome as 'ok' | CliFailureClass };
|
|
147
|
+
}
|
|
148
|
+
return { schema: 1, members };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Record one member's outcome, merging into whatever is already stored.
|
|
153
|
+
*
|
|
154
|
+
* Best-effort by construction: a write failure is swallowed. Telemetry that can
|
|
155
|
+
* break a council run is worse than telemetry that occasionally misses a row —
|
|
156
|
+
* the miss costs one `unknown`, the throw costs the run.
|
|
157
|
+
*/
|
|
158
|
+
export function recordProbe(
|
|
159
|
+
root: string,
|
|
160
|
+
name: string,
|
|
161
|
+
outcome: 'ok' | CliFailureClass,
|
|
162
|
+
at: string,
|
|
163
|
+
): void {
|
|
164
|
+
recordProbes(root, [{ name, outcome, at }]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One observation, as a caller collects them during a pass. */
|
|
168
|
+
export interface ProbeEntry {
|
|
169
|
+
readonly name: string;
|
|
170
|
+
readonly outcome: 'ok' | CliFailureClass;
|
|
171
|
+
readonly at: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Batch form — one read/write for a whole pass rather than one per member. */
|
|
175
|
+
export function recordProbes(root: string, entries: readonly ProbeEntry[]): void {
|
|
176
|
+
if (entries.length === 0 || isEnvKillSwitchActive(_KILL_SWITCH_ENV)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const current = readProbeStore(root);
|
|
181
|
+
const members: Record<string, ProbeRecord> = { ...current.members };
|
|
182
|
+
for (const e of entries) {
|
|
183
|
+
members[e.name] = { at: e.at, outcome: e.outcome };
|
|
184
|
+
}
|
|
185
|
+
const target = probeStorePath(root);
|
|
186
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
187
|
+
fs.writeFileSync(target, `${JSON.stringify({ schema: 1, members }, null, 2)}\n`, 'utf8');
|
|
188
|
+
} catch {
|
|
189
|
+
// Intentionally silent — see the docstring.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The record for one member, or null when nothing was ever observed. */
|
|
194
|
+
export function probeFor(store: ProbeStore, name: string): ProbeRecord | null {
|
|
195
|
+
return store.members[name] ?? null;
|
|
196
|
+
}
|