@event4u/agent-config 9.32.0 → 9.34.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 +85 -72
- package/dist/agent-src/commands/agent-handoff.md +15 -3
- package/dist/agent-src/commands/review/routing.md +3 -4
- package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
- package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
- package/dist/agent-src/skills/docker/SKILL.md +11 -0
- package/dist/agent-src/skills/laravel/SKILL.md +11 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/dist/cli/registry.js +4 -4
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
- package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-REMEZSVP.js} +1660 -146
- package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
- package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
- package/dist/cli-delegate/cmd_conformance.js +256 -226
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +21 -28
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +20 -5
- package/dist/cli-delegate/cmd_settings_get.js +4 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +17 -17
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +17 -0
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +114 -7
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3755 -1168
- package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
- package/package.json +2 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/src/agent-src/scripts/roadmap_gates.ts +107 -3
- package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/src/config/cost-parity-budget.json +167 -0
- package/src/config/gate-coverage.yml +44 -5
- package/src/config/quorum-attendance-budget.json +39 -0
- package/src/scripts/_cli/cmd_doctor.ts +69 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +25 -4
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/env_kill_switch.ts +37 -0
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/_lib/security_lint.ts +18 -4
- package/src/scripts/_lib/self_repair.ts +60 -0
- package/src/scripts/_lib/self_repair_store.ts +65 -5
- package/src/scripts/_lib/subagent_capsule.ts +78 -1
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +130 -8
- package/src/scripts/ai_council/orchestrator.ts +50 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/prompts.ts +165 -3
- package/src/scripts/ai_council/quorum.ts +30 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ai_team/review_gate.ts +2 -2
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/build_discovery_manifest.ts +165 -0
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_claims.ts +51 -2
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/ci_time_ratio.ts +3 -1
- package/src/scripts/conformance_scan.ts +105 -0
- package/src/scripts/cost/track.mjs +39 -0
- package/src/scripts/cost_summary.ts +104 -1
- package/src/scripts/council_cli.ts +131 -13
- package/src/scripts/discovery_graph.ts +201 -27
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/dispatch_r2_reviewer.ts +102 -1
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_handoffs.ts +92 -2
- package/src/scripts/lint_hedge_words.ts +317 -0
- package/src/scripts/lint_mcp_config_security.ts +1 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +56 -2
- package/src/scripts/lint_workflow_paths.ts +384 -0
- package/src/scripts/preamble_byte_census.ts +9 -1
- package/src/scripts/report_carrier_divergence.ts +10 -0
- package/src/scripts/report_conformance_funnel.ts +40 -0
- package/src/scripts/schemas/subagent.schema.json +3 -2
- package/src/scripts/self_repair_cli.ts +26 -1
- package/src/scripts/skill_linter.ts +58 -2
|
@@ -18,6 +18,8 @@ import * as fs from 'node:fs';
|
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
|
|
21
|
+
import { CACHE_READ_MULTIPLIER, CACHE_WRITE_MULTIPLIER_5M } from './ai_council/pricing.js';
|
|
22
|
+
|
|
21
23
|
const _HERE = fileURLToPath(import.meta.url);
|
|
22
24
|
// src/scripts/cost_summary.ts → parents[2] is the repo root (mirrors
|
|
23
25
|
// `Path(__file__).resolve().parent.parent.parent` in the .py).
|
|
@@ -70,6 +72,13 @@ interface KvBucket {
|
|
|
70
72
|
cache_read_input_tokens: number;
|
|
71
73
|
cache_creation_input_tokens: number;
|
|
72
74
|
telegraph_delta_tokens: number;
|
|
75
|
+
// Source rows whose `total_cost_usd` is a FLOOR, not a figure: at least
|
|
76
|
+
// one message priced at $0 for want of a rate (`cost/track.mjs`
|
|
77
|
+
// `rate_missing`). Carried into every grouping because the understatement
|
|
78
|
+
// travels with the row — a summary that aggregates past it reports the
|
|
79
|
+
// silent zero one layer up, which is the defect the row-level flag exists
|
|
80
|
+
// to remove.
|
|
81
|
+
rate_missing_sessions: number;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
function _zero_kv(): KvBucket {
|
|
@@ -81,6 +90,7 @@ function _zero_kv(): KvBucket {
|
|
|
81
90
|
cache_read_input_tokens: 0,
|
|
82
91
|
cache_creation_input_tokens: 0,
|
|
83
92
|
telegraph_delta_tokens: 0,
|
|
93
|
+
rate_missing_sessions: 0,
|
|
84
94
|
};
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -94,6 +104,7 @@ interface ModelBucket {
|
|
|
94
104
|
// cache itself is model-scoped — see pricing.ts), so by_model carries it.
|
|
95
105
|
cache_read_input_tokens: number;
|
|
96
106
|
cache_creation_input_tokens: number;
|
|
107
|
+
rate_missing_sessions: number;
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
function _zero_model(): ModelBucket {
|
|
@@ -104,15 +115,58 @@ function _zero_model(): ModelBucket {
|
|
|
104
115
|
output_tokens: 0,
|
|
105
116
|
cache_read_input_tokens: 0,
|
|
106
117
|
cache_creation_input_tokens: 0,
|
|
118
|
+
rate_missing_sessions: 0,
|
|
107
119
|
};
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
/**
|
|
123
|
+
* What the prompt cache bought, in INPUT-TOKEN EQUIVALENTS — not dollars.
|
|
124
|
+
*
|
|
125
|
+
* A cached read bills at `CACHE_READ_MULTIPLIER` of the input rate, so each
|
|
126
|
+
* read token saved `1 - multiplier` of one. A cache write bills at a premium,
|
|
127
|
+
* so each written token cost an extra `multiplier - 1`. Netting the two is the
|
|
128
|
+
* only honest single number here.
|
|
129
|
+
*
|
|
130
|
+
* Deliberately NOT priced in USD: the summary aggregates across models with
|
|
131
|
+
* different input rates, and `totals` carries no per-model token split to
|
|
132
|
+
* apply them to — a dollar figure would have to pick one rate and would be
|
|
133
|
+
* wrong for every other model in the row.
|
|
134
|
+
*
|
|
135
|
+
* The write premium uses the 5-minute multiplier. Rows carry no TTL split, and
|
|
136
|
+
* 5m is both Anthropic's default and the assumption `cost/track.mjs` already
|
|
137
|
+
* makes for unaccounted writes — so the two cost paths stay consistent rather
|
|
138
|
+
* than disagreeing quietly. A 1h-heavy workload therefore reads as slightly
|
|
139
|
+
* OPTIMISTIC, which is stated in the contract.
|
|
140
|
+
*/
|
|
141
|
+
function _cacheSavings(readTokens: number, writeTokens: number): number {
|
|
142
|
+
const saved = readTokens * (1 - CACHE_READ_MULTIPLIER);
|
|
143
|
+
const premium = writeTokens * (CACHE_WRITE_MULTIPLIER_5M - 1);
|
|
144
|
+
return Math.round(saved - premium);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** UTC calendar date (`YYYY-MM-DD`) of a row, or `'unknown'`. */
|
|
148
|
+
function _rowDate(row: Row): string {
|
|
149
|
+
// `startedAt` is the row's own first-message timestamp (track.mjs); the
|
|
150
|
+
// snake_case spelling is accepted for the same reason `session_id` is.
|
|
151
|
+
const raw = row['startedAt'] ?? row['started_at'];
|
|
152
|
+
if (typeof raw !== 'string' || !raw) {
|
|
153
|
+
return 'unknown';
|
|
154
|
+
}
|
|
155
|
+
const parsed = new Date(raw);
|
|
156
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
157
|
+
return 'unknown';
|
|
158
|
+
}
|
|
159
|
+
return parsed.toISOString().slice(0, 10);
|
|
160
|
+
}
|
|
161
|
+
|
|
110
162
|
/** Aggregate rows into the cost-summary structure (PyFloat-aware output). */
|
|
111
163
|
export function aggregate(rows: Row[]): Json {
|
|
112
164
|
// Insertion-ordered maps to mirror Python's dict ordering + sorted().
|
|
113
165
|
const by_sess = new Map<string, KvBucket>();
|
|
114
166
|
const by_conv = new Map<string, KvBucket>();
|
|
115
167
|
const by_model = new Map<string, ModelBucket>();
|
|
168
|
+
const by_date = new Map<string, KvBucket>();
|
|
169
|
+
const rate_missing_models = new Set<string>();
|
|
116
170
|
const totals = _zero_kv();
|
|
117
171
|
|
|
118
172
|
for (const row of rows) {
|
|
@@ -125,10 +179,17 @@ export function aggregate(rows: Row[]): Json {
|
|
|
125
179
|
const cread = _int(row['cache_read_input_tokens']);
|
|
126
180
|
const cwrite = _int(row['cache_creation_input_tokens']);
|
|
127
181
|
const delta = _delta(row);
|
|
182
|
+
// A row is understated when its producer says so. `_pyTruthy` keeps the
|
|
183
|
+
// absent-reading identical to every other additive field: no flag → 0.
|
|
184
|
+
const rateMissing = _pyTruthy(row['rate_missing']) ? 1 : 0;
|
|
185
|
+
for (const id of _strList(row['rate_missing_models'])) {
|
|
186
|
+
rate_missing_models.add(id);
|
|
187
|
+
}
|
|
128
188
|
|
|
129
189
|
const sessBucket = _getOr(by_sess, sid, _zero_kv);
|
|
130
190
|
const convBucket = _getOr(by_conv, cid, _zero_kv);
|
|
131
|
-
|
|
191
|
+
const dateBucket = _getOr(by_date, _rowDate(row), _zero_kv);
|
|
192
|
+
for (const bucket of [sessBucket, convBucket, dateBucket, totals]) {
|
|
132
193
|
bucket.sessions += 1;
|
|
133
194
|
bucket.total_cost_usd += cost;
|
|
134
195
|
bucket.input_tokens += itok;
|
|
@@ -136,6 +197,7 @@ export function aggregate(rows: Row[]): Json {
|
|
|
136
197
|
bucket.cache_read_input_tokens += cread;
|
|
137
198
|
bucket.cache_creation_input_tokens += cwrite;
|
|
138
199
|
bucket.telegraph_delta_tokens += delta;
|
|
200
|
+
bucket.rate_missing_sessions += rateMissing;
|
|
139
201
|
}
|
|
140
202
|
const m = _getOr(by_model, model, _zero_model);
|
|
141
203
|
m.sessions += 1;
|
|
@@ -144,6 +206,7 @@ export function aggregate(rows: Row[]): Json {
|
|
|
144
206
|
m.output_tokens += otok;
|
|
145
207
|
m.cache_read_input_tokens += cread;
|
|
146
208
|
m.cache_creation_input_tokens += cwrite;
|
|
209
|
+
m.rate_missing_sessions += rateMissing;
|
|
147
210
|
}
|
|
148
211
|
|
|
149
212
|
return {
|
|
@@ -159,6 +222,15 @@ export function aggregate(rows: Row[]): Json {
|
|
|
159
222
|
telegraph_delta_tokens: totals.telegraph_delta_tokens,
|
|
160
223
|
telegraph_multiplier_version: MULTIPLIER_VERSION,
|
|
161
224
|
telegraph_multiplier_active: MULTIPLIER_ACTIVE,
|
|
225
|
+
// Additive per the schema's own rule; token-equivalents, not USD.
|
|
226
|
+
cache_savings_input_token_equivalents: _cacheSavings(
|
|
227
|
+
totals.cache_read_input_tokens,
|
|
228
|
+
totals.cache_creation_input_tokens,
|
|
229
|
+
),
|
|
230
|
+
// The understated-cost qualification, surfaced instead of
|
|
231
|
+
// aggregated past: a non-zero count means total_cost_usd is a floor.
|
|
232
|
+
rate_missing_sessions: totals.rate_missing_sessions,
|
|
233
|
+
rate_missing_models: [...rate_missing_models].sort(),
|
|
162
234
|
},
|
|
163
235
|
by_session: _sortedEntries(by_sess).map(([k, v]) => ({
|
|
164
236
|
key: k,
|
|
@@ -169,6 +241,7 @@ export function aggregate(rows: Row[]): Json {
|
|
|
169
241
|
cache_read_input_tokens: v.cache_read_input_tokens,
|
|
170
242
|
cache_creation_input_tokens: v.cache_creation_input_tokens,
|
|
171
243
|
telegraph_delta_tokens: v.telegraph_delta_tokens,
|
|
244
|
+
rate_missing_sessions: v.rate_missing_sessions,
|
|
172
245
|
})),
|
|
173
246
|
by_conversation: _sortedEntries(by_conv).map(([k, v]) => ({
|
|
174
247
|
key: k,
|
|
@@ -179,6 +252,22 @@ export function aggregate(rows: Row[]): Json {
|
|
|
179
252
|
cache_read_input_tokens: v.cache_read_input_tokens,
|
|
180
253
|
cache_creation_input_tokens: v.cache_creation_input_tokens,
|
|
181
254
|
telegraph_delta_tokens: v.telegraph_delta_tokens,
|
|
255
|
+
rate_missing_sessions: v.rate_missing_sessions,
|
|
256
|
+
})),
|
|
257
|
+
// Ordered by date ascending — `_sortedEntries` is codepoint order, and
|
|
258
|
+
// ISO `YYYY-MM-DD` sorts chronologically under it. `'unknown'` sorts
|
|
259
|
+
// last (`u` > any digit), so a row with no timestamp never displaces a
|
|
260
|
+
// real day.
|
|
261
|
+
by_date: _sortedEntries(by_date).map(([k, v]) => ({
|
|
262
|
+
key: k,
|
|
263
|
+
sessions: v.sessions,
|
|
264
|
+
total_cost_usd: new PyFloat(v.total_cost_usd),
|
|
265
|
+
input_tokens: v.input_tokens,
|
|
266
|
+
output_tokens: v.output_tokens,
|
|
267
|
+
cache_read_input_tokens: v.cache_read_input_tokens,
|
|
268
|
+
cache_creation_input_tokens: v.cache_creation_input_tokens,
|
|
269
|
+
telegraph_delta_tokens: v.telegraph_delta_tokens,
|
|
270
|
+
rate_missing_sessions: v.rate_missing_sessions,
|
|
182
271
|
})),
|
|
183
272
|
by_model: _sortedEntries(by_model).map(([k, v]) => ({
|
|
184
273
|
model: k,
|
|
@@ -188,6 +277,7 @@ export function aggregate(rows: Row[]): Json {
|
|
|
188
277
|
output_tokens: v.output_tokens,
|
|
189
278
|
cache_read_input_tokens: v.cache_read_input_tokens,
|
|
190
279
|
cache_creation_input_tokens: v.cache_creation_input_tokens,
|
|
280
|
+
rate_missing_sessions: v.rate_missing_sessions,
|
|
191
281
|
})),
|
|
192
282
|
};
|
|
193
283
|
}
|
|
@@ -219,6 +309,19 @@ function _str(value: unknown): string {
|
|
|
219
309
|
return String(value);
|
|
220
310
|
}
|
|
221
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Read a list-of-strings field defensively — absent, wrong-typed, or holding
|
|
314
|
+
* non-string members all read as "nothing to add" rather than throwing or
|
|
315
|
+
* coercing. A summary must never fail on a malformed source row; it skips it,
|
|
316
|
+
* exactly as `_load` skips an unparseable line.
|
|
317
|
+
*/
|
|
318
|
+
function _strList(value: unknown): string[] {
|
|
319
|
+
if (!Array.isArray(value)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
return value.filter((v): v is string => typeof v === 'string' && v.length > 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
222
325
|
/** Mirror `int(row.get(k) or 0)` — falsy → 0; numeric/string → truncated int. */
|
|
223
326
|
function _int(value: unknown): number {
|
|
224
327
|
if (!_pyTruthy(value)) {
|
|
@@ -77,7 +77,14 @@ import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
|
|
|
77
77
|
import { resolveMemberTransport } from './ai_council/transport_resolver.js';
|
|
78
78
|
import { absentReasonFromCliFailure, classifyCliFailure, type AbsentReason } from './ai_council/transport_resolver.js';
|
|
79
79
|
import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
appendEvent,
|
|
82
|
+
appendQuorumEvent,
|
|
83
|
+
type QuorumAbsence,
|
|
84
|
+
type QuorumCommand,
|
|
85
|
+
type QuorumDispatch,
|
|
86
|
+
type QuorumEventPhase,
|
|
87
|
+
} from './ai_council/events_log.js';
|
|
81
88
|
import {
|
|
82
89
|
type ClassificationResult,
|
|
83
90
|
type SizeFitVerdict,
|
|
@@ -576,6 +583,16 @@ interface BuildMembersOptions {
|
|
|
576
583
|
* "null = never evaluated" convention.
|
|
577
584
|
*/
|
|
578
585
|
quorum_out?: { result: QuorumResult | null } | null;
|
|
586
|
+
/**
|
|
587
|
+
* Which CLI path is constructing — recorded on the `pre_run` quorum
|
|
588
|
+
* event so a consumer can exclude the paths that never run a pass.
|
|
589
|
+
* `estimate` is a spend-free preview and `run` may still abort at the
|
|
590
|
+
* necessity or size-fit gate after this point, so a `pre_run` line is
|
|
591
|
+
* evidence that construction happened, never that a pass followed;
|
|
592
|
+
* attendance rates are computed over `post_run` lines for exactly that
|
|
593
|
+
* reason. Defaults to `run` for a caller that does not say.
|
|
594
|
+
*/
|
|
595
|
+
command?: QuorumCommand;
|
|
579
596
|
/**
|
|
580
597
|
* Test-only injection point, mirroring the `settings`/`members`/`table`
|
|
581
598
|
* DI pattern the CLI commands already use. Production callers never set
|
|
@@ -630,6 +647,52 @@ function _quorum_setting_from(ai: Dict): QuorumSetting {
|
|
|
630
647
|
return 'majority';
|
|
631
648
|
}
|
|
632
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Emit one `quorum_result` line for a resolved quorum.
|
|
652
|
+
*
|
|
653
|
+
* Both `evaluateQuorum` call sites route through here, tagged by `phase`, so
|
|
654
|
+
* a solo-concluded pass is distinguishable from a full-attendance one in the
|
|
655
|
+
* event log — the gap `road-to-always-on-orchestration`'s Risk 6 asserted was
|
|
656
|
+
* already closed and was not.
|
|
657
|
+
*
|
|
658
|
+
* The emit sits at the call sites rather than inside `_postRunQuorum` on
|
|
659
|
+
* purpose: that function is exported and unit-tested as a pure derivation,
|
|
660
|
+
* and a write buried in it would make every test of it a log writer.
|
|
661
|
+
*
|
|
662
|
+
* `absent` entries arrive as the CLI's own `{member, reason, detail}` dicts;
|
|
663
|
+
* `detail` is dropped by `appendQuorumEvent`'s input type, which cannot carry
|
|
664
|
+
* free-form text at all.
|
|
665
|
+
*/
|
|
666
|
+
function _emitQuorumEvent(
|
|
667
|
+
phase: QuorumEventPhase,
|
|
668
|
+
quorum: QuorumResult,
|
|
669
|
+
absent: readonly Dict[],
|
|
670
|
+
ctx: {
|
|
671
|
+
command: QuorumCommand;
|
|
672
|
+
dispatch?: QuorumDispatch;
|
|
673
|
+
/** Enabled members before `--single` filtering; defaults to the roster that ran. */
|
|
674
|
+
configuredTotal?: number;
|
|
675
|
+
lens?: string;
|
|
676
|
+
invocation?: string;
|
|
677
|
+
},
|
|
678
|
+
): void {
|
|
679
|
+
appendQuorumEvent({
|
|
680
|
+
lens: ctx.lens ?? '',
|
|
681
|
+
invocation: ctx.invocation ?? '',
|
|
682
|
+
phase,
|
|
683
|
+
command: ctx.command,
|
|
684
|
+
dispatch: ctx.dispatch ?? 'full',
|
|
685
|
+
configuredTotal: ctx.configuredTotal ?? quorum.total,
|
|
686
|
+
result: quorum,
|
|
687
|
+
absent: absent.map(
|
|
688
|
+
(a): QuorumAbsence => ({
|
|
689
|
+
member: String(a['member'] ?? ''),
|
|
690
|
+
reason: (a['reason'] as QuorumAbsence['reason']) ?? 'unavailable',
|
|
691
|
+
}),
|
|
692
|
+
),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
633
696
|
/**
|
|
634
697
|
* Re-derive quorum AFTER the provider calls, over what was actually USABLE —
|
|
635
698
|
* never what merely CONSTRUCTED (M3, independent-review finding). `members`
|
|
@@ -762,6 +825,18 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
762
825
|
// that caller shape.
|
|
763
826
|
let total_enabled = 0;
|
|
764
827
|
let absent_count = 0;
|
|
828
|
+
// Every absence, regardless of whether the caller asked for the
|
|
829
|
+
// human-readable `skipped` out-param — the quorum event needs the
|
|
830
|
+
// member/reason pairs even when `skipped` is `null`, and reconstructing
|
|
831
|
+
// them from `absent_count` alone is impossible.
|
|
832
|
+
const absent_entries: Dict[] = [];
|
|
833
|
+
const record_absent = (entry: Dict): void => {
|
|
834
|
+
absent_count += 1;
|
|
835
|
+
absent_entries.push(entry);
|
|
836
|
+
if (skipped !== null) {
|
|
837
|
+
skipped.push(entry);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
765
840
|
for (const name of Object.keys(members_cfg)) {
|
|
766
841
|
const cfg = ((members_cfg[name] as Dict) || {}) as Dict;
|
|
767
842
|
if (!_pyBool(cfg['enabled'])) {
|
|
@@ -834,10 +909,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
834
909
|
reason: resolved.absentReason ?? 'unavailable',
|
|
835
910
|
detail,
|
|
836
911
|
};
|
|
837
|
-
|
|
838
|
-
if (skipped !== null) {
|
|
839
|
-
skipped.push(entry);
|
|
840
|
-
}
|
|
912
|
+
record_absent(entry);
|
|
841
913
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
842
914
|
continue;
|
|
843
915
|
}
|
|
@@ -877,10 +949,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
877
949
|
}
|
|
878
950
|
const detail = exc.message;
|
|
879
951
|
const entry: Dict = { member: name, reason: 'no_auth', detail };
|
|
880
|
-
|
|
881
|
-
if (skipped !== null) {
|
|
882
|
-
skipped.push(entry);
|
|
883
|
-
}
|
|
952
|
+
record_absent(entry);
|
|
884
953
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
885
954
|
continue;
|
|
886
955
|
}
|
|
@@ -910,10 +979,7 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
910
979
|
reason: 'binary_missing',
|
|
911
980
|
detail,
|
|
912
981
|
};
|
|
913
|
-
|
|
914
|
-
if (skipped !== null) {
|
|
915
|
-
skipped.push(entry);
|
|
916
|
-
}
|
|
982
|
+
record_absent(entry);
|
|
917
983
|
_stderr(`[council] SKIP ${name}: ${detail}\n`);
|
|
918
984
|
continue;
|
|
919
985
|
}
|
|
@@ -935,6 +1001,15 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
935
1001
|
if (quorum_out !== null) {
|
|
936
1002
|
const present = total_enabled - absent_count;
|
|
937
1003
|
quorum_out.result = evaluateQuorum(total_enabled, present, _quorum_setting_from(ai));
|
|
1004
|
+
// Construction-time attendance. `lens` / `invocation` stay empty
|
|
1005
|
+
// here on purpose: neither is known at member-construction time,
|
|
1006
|
+
// and `invocation_mode` (api / cli / manual) is a different axis
|
|
1007
|
+
// that would read as the same field under a shared name.
|
|
1008
|
+
// `command` IS recorded, because a preview and an aborted run both
|
|
1009
|
+
// reach this line and neither is a pass.
|
|
1010
|
+
_emitQuorumEvent('pre_run', quorum_out.result, absent_entries, {
|
|
1011
|
+
command: opts.command ?? 'run',
|
|
1012
|
+
});
|
|
938
1013
|
}
|
|
939
1014
|
if (members.length === 0) {
|
|
940
1015
|
if (skipped && skipped.length > 0) {
|
|
@@ -1703,6 +1778,10 @@ function cmd_estimate(
|
|
|
1703
1778
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
1704
1779
|
skipped,
|
|
1705
1780
|
quorum_out,
|
|
1781
|
+
// A cost preview spends nothing and runs no pass. The line is
|
|
1782
|
+
// still written — tagged, so a consumer excludes it deliberately
|
|
1783
|
+
// instead of never learning it was there.
|
|
1784
|
+
command: 'estimate',
|
|
1706
1785
|
});
|
|
1707
1786
|
}
|
|
1708
1787
|
if (table === null) {
|
|
@@ -2365,6 +2444,13 @@ function cmd_run(
|
|
|
2365
2444
|
quorum_out,
|
|
2366
2445
|
});
|
|
2367
2446
|
}
|
|
2447
|
+
// Measured, never assumed: `_apply_solo_dispatch` escalates back to the
|
|
2448
|
+
// full roster on three paths (empty fallback chain, no chain member
|
|
2449
|
+
// runtime-present, the picked member vanished). `--single` is therefore a
|
|
2450
|
+
// request, not an outcome, and a `dispatch` field written from the flag
|
|
2451
|
+
// would claim a solo pass that did not happen — contaminating the one
|
|
2452
|
+
// split the solo-conclusion rate depends on.
|
|
2453
|
+
const roster_before_dispatch = members.length;
|
|
2368
2454
|
if (_getattr(args, 'single', false)) {
|
|
2369
2455
|
const [filtered, solo_banner] = _apply_solo_dispatch(members);
|
|
2370
2456
|
members = filtered;
|
|
@@ -2373,6 +2459,8 @@ function cmd_run(
|
|
|
2373
2459
|
}
|
|
2374
2460
|
_emit_shadow_slo_banner();
|
|
2375
2461
|
}
|
|
2462
|
+
const dispatch_shape: QuorumDispatch =
|
|
2463
|
+
members.length < roster_before_dispatch ? 'single' : 'full';
|
|
2376
2464
|
if (table === null) {
|
|
2377
2465
|
// Anchor to the PROJECT root — the module default writes into the
|
|
2378
2466
|
// installed package dir when run from a consumer (pollutes the npm
|
|
@@ -2538,9 +2626,30 @@ function cmd_run(
|
|
|
2538
2626
|
// (mid-flight failures appended to the same array construction-time
|
|
2539
2627
|
// skips already populate, so `payload['absent_members']` needs no
|
|
2540
2628
|
// separate wiring either).
|
|
2629
|
+
// The pre-run reading, before it is overwritten — `build_members` set it
|
|
2630
|
+
// from `total_enabled`, i.e. the enabled roster before `--single` filtering
|
|
2631
|
+
// and before any construction failure. Without carrying it forward, a pass
|
|
2632
|
+
// with 3 configured members where 2 fail to construct and 1 answers emits
|
|
2633
|
+
// `{total: 1, present: 1}` and reads as full attendance.
|
|
2634
|
+
const configured_total = quorum_out.result?.total ?? null;
|
|
2541
2635
|
const post_run = _postRunQuorum(members, responses, ai_cfg);
|
|
2542
2636
|
quorum_out.result = post_run.quorum;
|
|
2543
2637
|
skipped.push(...post_run.absent);
|
|
2638
|
+
// Attendance telemetry for the reading that actually decided the pass.
|
|
2639
|
+
// `post_run.absent` is the mid-flight set only; the construction-time
|
|
2640
|
+
// skips already went out under `phase: 'pre_run'` from `build_members`,
|
|
2641
|
+
// and merging them here would double-count a member absent in both.
|
|
2642
|
+
_emitQuorumEvent('post_run', post_run.quorum, post_run.absent, {
|
|
2643
|
+
command: 'run',
|
|
2644
|
+
// Whether the roster actually shrank — see `dispatch_shape`. Without
|
|
2645
|
+
// this the line is byte-identical to a configured one-member council,
|
|
2646
|
+
// and the solo-conclusion rate cannot make the one distinction it
|
|
2647
|
+
// exists for.
|
|
2648
|
+
dispatch: dispatch_shape,
|
|
2649
|
+
...(configured_total !== null ? { configuredTotal: configured_total } : {}),
|
|
2650
|
+
lens: question.mode,
|
|
2651
|
+
invocation: String(_getattr(args, 'invocation', 'agent')),
|
|
2652
|
+
});
|
|
2544
2653
|
// Phase 4.1 — verdict → handoff envelope. Same tally `render()` computes
|
|
2545
2654
|
// internally for the Vote Tally block (mirrored here, not imported from
|
|
2546
2655
|
// there, because `render()` re-derives it from the SAVED payload on a
|
|
@@ -2817,12 +2926,21 @@ function cmd_debate(
|
|
|
2817
2926
|
const advisor_plans = _build_advisor_plans(ai_cfg, REPO_ROOT);
|
|
2818
2927
|
const explicit_overrides = _parse_model_overrides(_getattr<string[] | null>(args, 'model', null));
|
|
2819
2928
|
const skipped: Dict[] = [];
|
|
2929
|
+
// Debate spends the same providers as `run`, so its attendance belongs in
|
|
2930
|
+
// the same log. It carries the construction-time half only: there is no
|
|
2931
|
+
// `_postRunQuorum` on this path, and adding one would need a usable-response
|
|
2932
|
+
// definition for a multi-round debate that this change does not have. The
|
|
2933
|
+
// gap is declared in quorum-attendance-budget.json rather than left for a
|
|
2934
|
+
// reader to discover from a missing row.
|
|
2935
|
+
const quorum_out: { result: QuorumResult | null } = { result: null };
|
|
2820
2936
|
if (members === null) {
|
|
2821
2937
|
members = build_members(settings, {
|
|
2822
2938
|
invocation_mode: args.mode_override,
|
|
2823
2939
|
model_overrides: _advisor_model_overrides(advisor_plans, explicit_overrides),
|
|
2824
2940
|
siblings_overrides: _parse_siblings_overrides(_getattr<string[] | null>(args, 'siblings', null)),
|
|
2825
2941
|
skipped,
|
|
2942
|
+
quorum_out,
|
|
2943
|
+
command: 'debate',
|
|
2826
2944
|
});
|
|
2827
2945
|
}
|
|
2828
2946
|
if (table === null) {
|