@event4u/agent-config 14.19.0 → 14.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Jury aggregation — road-to-admissible-council-seats 3.2.
3
+ *
4
+ * BESIDE `consensus.ts`, not inside it, and the separation is the design.
5
+ * `consensus.ts` aggregates a COUNCIL, whose members carry verdicts.
6
+ * A jury does not: per ADR-257 a route this package did not pay for may propose
7
+ * and may score, never decide. Folding jury scoring into the consensus path
8
+ * would put a scorer on the code path that produces verdicts, which is the
9
+ * exact transition this roadmap's Risk 5 names — "scoring quietly becomes
10
+ * deciding".
11
+ *
12
+ * Two rules, and both are refusals.
13
+ *
14
+ * 1. **At least two model families, or the panel is ABSENT.** Not a warning, a
15
+ * refusal: `docs/CLAIMS.md` records that a same-posture second vendor's
16
+ * catches were a strict SUBSET of the first's. A same-family panel therefore
17
+ * measures redundancy, and reporting redundancy as agreement is the failure
18
+ * a diversity requirement exists to prevent.
19
+ * 2. **The aggregate is a trimmed statistic, never a vote count.** For the same
20
+ * reason: counting agreeing voices rewards a panel for being correlated.
21
+ *
22
+ * What "trimmed" means here, exactly: with n >= 3 the single lowest and single highest score are dropped and the
23
+ * rest averaged — which at n = 3 IS the median, and is why one corrupted score
24
+ * in a three-judge panel cannot move the result the way it moves an arithmetic
25
+ * mean. With n = 2 there is nothing to trim; the mean is returned with
26
+ * `trimmed: 0` so a reader can see that no robustness was available rather than
27
+ * inferring it from a method name.
28
+ */
29
+
30
+ import {
31
+ judgeBothOrders,
32
+ positionConsistency,
33
+ type JudgeConsistency,
34
+ type PairResolution,
35
+ type PairwiseJudge,
36
+ type SwapObservation,
37
+ } from './judge_position_bias.js';
38
+
39
+ /** One judge's score for one item. `family` is the MODEL family, not the seat. */
40
+ export interface JuryScore {
41
+ readonly judge: string;
42
+ readonly family: string;
43
+ /** Any finite number. The aggregator does not impose a scale. */
44
+ readonly score: number;
45
+ }
46
+
47
+ /** Why a panel produced no aggregate. Each value is a REFUSAL, not an error. */
48
+ export type JuryAbsentReason = 'no_scores' | 'single_family' | 'non_finite_score';
49
+
50
+ export type JuryResult =
51
+ | {
52
+ readonly present: false;
53
+ readonly absentReason: JuryAbsentReason;
54
+ readonly reason: string;
55
+ }
56
+ | {
57
+ readonly present: true;
58
+ readonly aggregate: number;
59
+ readonly method: 'trimmed-mean';
60
+ /** How many scores the trim discarded. 0 when n < 3. */
61
+ readonly trimmed: number;
62
+ readonly judges: number;
63
+ readonly families: readonly string[];
64
+ };
65
+
66
+ /**
67
+ * Aggregate one item's jury scores.
68
+ *
69
+ * Never throws on panel composition: a refused panel is a VERDICT the caller
70
+ * reads, the same shape the transport resolver uses for a refused seat. An
71
+ * exception would make "this panel does not qualify" indistinguishable from a
72
+ * bug at every call site.
73
+ */
74
+ export function juryAggregate(scores: readonly JuryScore[]): JuryResult {
75
+ if (scores.length === 0) {
76
+ return { present: false, absentReason: 'no_scores', reason: 'no jury scores supplied' };
77
+ }
78
+ for (const s of scores) {
79
+ if (!Number.isFinite(s.score)) {
80
+ return {
81
+ present: false,
82
+ absentReason: 'non_finite_score',
83
+ reason: `judge ${s.judge} returned a non-finite score; the panel is refused rather than silently dropping it`,
84
+ };
85
+ }
86
+ }
87
+ const families = [...new Set(scores.map((s) => s.family))].sort();
88
+ if (families.length < 2) {
89
+ return {
90
+ present: false,
91
+ absentReason: 'single_family',
92
+ reason:
93
+ `panel spans ${families.length} model family (${families.join(', ') || 'none'}); ` +
94
+ 'at least two are required — a same-family panel measures redundancy, not agreement',
95
+ };
96
+ }
97
+ const sorted = [...scores.map((s) => s.score)].sort((a, b) => a - b);
98
+ const trimmed = sorted.length >= 3 ? 2 : 0;
99
+ const kept = trimmed === 0 ? sorted : sorted.slice(1, sorted.length - 1);
100
+ const aggregate = kept.reduce((acc, v) => acc + v, 0) / kept.length;
101
+ return {
102
+ present: true,
103
+ aggregate,
104
+ method: 'trimmed-mean',
105
+ trimmed,
106
+ judges: scores.length,
107
+ families,
108
+ };
109
+ }
110
+
111
+ /** One judge's swapped observation plus the family it belongs to. */
112
+ export interface JuryPairwiseJudge {
113
+ readonly judge: string;
114
+ readonly family: string;
115
+ readonly fn: PairwiseJudge;
116
+ }
117
+
118
+ export interface JuryPairwiseResult {
119
+ readonly present: boolean;
120
+ readonly absentReason: JuryAbsentReason | null;
121
+ readonly reason: string | null;
122
+ /** Panel resolution, or `null` when the panel is absent. */
123
+ readonly resolution: PairResolution | null;
124
+ readonly observations: readonly SwapObservation[];
125
+ readonly consistency: readonly JudgeConsistency[];
126
+ }
127
+
128
+ /**
129
+ * Run a pairwise jury with the position swap applied PER JUDGE.
130
+ *
131
+ * Reuses `judge_position_bias.ts` rather than re-implementing the swap: a
132
+ * second implementation of "show both orders and reconcile" is a second place
133
+ * for the reconciliation to be subtly wrong, and that module already resolves a
134
+ * verdict that does not survive the swap to `inconsistent` rather than to a
135
+ * winner.
136
+ *
137
+ * The panel resolution is the strict majority of the judges' own resolutions,
138
+ * and `inconsistent` judges are counted in the DENOMINATOR — a judge that
139
+ * flipped has an opinion about nothing, and dropping it from the denominator
140
+ * would let two stable judges out of five look unanimous.
141
+ */
142
+ export function juryPairwise(
143
+ id: string,
144
+ a: string,
145
+ b: string,
146
+ judges: readonly JuryPairwiseJudge[],
147
+ ): JuryPairwiseResult {
148
+ const families = [...new Set(judges.map((j) => j.family))].sort();
149
+ if (judges.length === 0 || families.length < 2) {
150
+ return {
151
+ present: false,
152
+ absentReason: judges.length === 0 ? 'no_scores' : 'single_family',
153
+ reason:
154
+ judges.length === 0
155
+ ? 'no judges supplied'
156
+ : `panel spans ${families.length} model family; at least two are required`,
157
+ resolution: null,
158
+ observations: [],
159
+ consistency: [],
160
+ };
161
+ }
162
+ const observations: SwapObservation[] = [];
163
+ const consistency: JudgeConsistency[] = [];
164
+ const tally = new Map<PairResolution, number>();
165
+ for (const j of judges) {
166
+ const obs = judgeBothOrders(id, a, b, j.fn);
167
+ observations.push(obs);
168
+ consistency.push(positionConsistency(j.judge, [obs]));
169
+ tally.set(obs.resolution, (tally.get(obs.resolution) ?? 0) + 1);
170
+ }
171
+ let resolution: PairResolution = 'inconsistent';
172
+ for (const [k, v] of tally) {
173
+ if (v * 2 > judges.length) {
174
+ resolution = k;
175
+ }
176
+ }
177
+ return { present: true, absentReason: null, reason: null, resolution, observations, consistency };
178
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ classifyContentClass as _classifyContentClass,
3
+ DEFAULT_SEAT_CEILING as _DEFAULT_SEAT_CEILING,
4
+ type ContentClass,
5
+ } from './content_ceiling.js';
6
+
7
+ /**
8
+ * Seat policy — the vocabulary for refusing a route that IS reachable.
9
+ *
10
+ * road-to-admissible-council-seats 2.1. `AbsentReason` was exactly four values
11
+ * — `no_binary | no_auth | timeout | quota` — and every one of them means "the
12
+ * route is not reachable". A route that is reachable and **must not be used**
13
+ * had no name at all, so the resolver could not fail closed on terms, privacy,
14
+ * unproven cost, or a served model that is not the requested one. The absence
15
+ * of a word was the absence of the refusal: `resolveTransport`'s `api`/`cli`
16
+ * branches return `available: true` unconditionally, so anything the four
17
+ * reachability reasons could not express resolved OPEN.
18
+ *
19
+ * Fail closed on the unknown, which is the whole point.
20
+ * `classifyPolicyExclusion` maps anything it does not recognise to
21
+ * `policy_unknown`, and `policy_unknown` is an ABSENT reason like every other
22
+ * member of this union. An unrecognised policy token is the case where the
23
+ * config is trying to say something the code cannot understand; resolving it
24
+ * to "available" would make a typo an authorisation. This is the roadmap's own
25
+ * Risk 3 — "a new refusal vocabulary defaults open instead of closed".
26
+ *
27
+ * What this is NOT. It is not a secrets floor. Redaction (`skills/ai-council/references/
28
+ * cost-and-redaction.md`) still runs underneath and is not replaced. It is not
29
+ * an authorisation either: nothing here ever makes a seat available that was
30
+ * not already available — every value in the union only ever subtracts.
31
+ */
32
+
33
+ /** Reachable-but-refused reasons. Every value means ABSENT. */
34
+ export const POLICY_EXCLUSIONS = [
35
+ /** The provider's terms forbid this use of the route. */
36
+ 'policy_terms',
37
+ /** A privacy constraint forbids sending to this route. */
38
+ 'policy_privacy',
39
+ /** The route's cost property is asserted rather than proven. */
40
+ 'policy_unproven_cost',
41
+ /** The route serves a model other than the one requested. */
42
+ 'policy_model_mismatch',
43
+ /** 2.2 — the payload's content class exceeds the seat's ceiling. */
44
+ 'policy_content_ceiling',
45
+ /** The config named an exclusion this build does not understand. */
46
+ 'policy_unknown',
47
+ ] as const;
48
+
49
+ export type PolicyExclusion = (typeof POLICY_EXCLUSIONS)[number];
50
+
51
+ const _SET: ReadonlySet<string> = new Set<string>(POLICY_EXCLUSIONS);
52
+
53
+ /** True for a value this build can act on. `policy_unknown` included: it is a
54
+ * real, actionable verdict (refuse), not a parse failure. */
55
+ export function isPolicyExclusion(v: unknown): v is PolicyExclusion {
56
+ return typeof v === 'string' && _SET.has(v);
57
+ }
58
+
59
+ /**
60
+ * Any configured value → a verdict. Unrecognised input, including `null` used
61
+ * where a string was meant, becomes `policy_unknown` — never `null`, because a
62
+ * `null` return would put the caller back in the position of deciding what an
63
+ * unreadable policy means, which is where the open default came from.
64
+ *
65
+ * `undefined` and `null` are the two ways of saying "no exclusion configured"
66
+ * and are the ONLY inputs that return `null`. Everything else is a refusal.
67
+ */
68
+ export function classifyPolicyExclusion(raw: unknown): PolicyExclusion | null {
69
+ if (raw === undefined || raw === null) {
70
+ return null;
71
+ }
72
+ return isPolicyExclusion(raw) ? raw : 'policy_unknown';
73
+ }
74
+
75
+ /** One-line human reason for a refusal. `council:status` prints this. */
76
+ export function policyExclusionReason(p: PolicyExclusion, provider?: string): string {
77
+ const who = provider === undefined ? 'this seat' : provider;
78
+ switch (p) {
79
+ case 'policy_terms':
80
+ return `${who} is excluded by policy: the route's terms forbid this use`;
81
+ case 'policy_privacy':
82
+ return `${who} is excluded by policy: a privacy constraint forbids this route`;
83
+ case 'policy_unproven_cost':
84
+ return `${who} is excluded by policy: the route's cost property is asserted, not proven`;
85
+ case 'policy_model_mismatch':
86
+ return `${who} is excluded by policy: the route serves a model other than the one requested`;
87
+ case 'policy_content_ceiling':
88
+ return `${who} is excluded by policy: the payload exceeds the seat's content ceiling`;
89
+ case 'policy_unknown':
90
+ return `${who} is excluded by policy: an exclusion this build does not understand (failing closed)`;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Parsed together because they are one question — "what may this seat do, and
96
+ * if it may do nothing, why" — and because `config.ts` sits ~670 lines past
97
+ * the 1,500-line source ceiling, where every line costs one excess line.
98
+ *
99
+ * Returns an error MESSAGE rather than throwing so `config.ts` keeps raising
100
+ * its own `CouncilConfigError` (which its callers catch by type) without this
101
+ * module importing it and creating a cycle back into the loader.
102
+ */
103
+ export interface SeatFields {
104
+ readonly disabled_reason: string | null;
105
+ readonly policy_exclusion: PolicyExclusion | null;
106
+ readonly content_ceiling: ContentClass;
107
+ }
108
+
109
+ export function parseSeatFields(
110
+ name: string,
111
+ raw: Record<string, unknown>,
112
+ ): { fields: SeatFields } | { error: string } {
113
+ const dr = raw['disabled_reason'];
114
+ let disabled_reason: string | null = null;
115
+ if (dr !== undefined && dr !== null) {
116
+ if (typeof dr !== 'string' || dr.trim() === '') {
117
+ return {
118
+ error:
119
+ `members.${name}.disabled_reason must be a non-empty string when set ` +
120
+ `(got ${JSON.stringify(dr)}).`,
121
+ };
122
+ }
123
+ disabled_reason = dr.trim();
124
+ }
125
+ // Both of the next two fail CLOSED on an unreadable value: an unknown
126
+ // exclusion token becomes `policy_unknown` (which resolves ABSENT) and an
127
+ // unknown ceiling falls back to the RESTRICTIVE one. Throwing instead would
128
+ // be defensible, but a config typo would then take the whole council down
129
+ // rather than the one seat it names.
130
+ return {
131
+ fields: {
132
+ disabled_reason,
133
+ policy_exclusion: classifyPolicyExclusion(raw['policy_exclusion']),
134
+ content_ceiling: _classifyContentClass(raw['content_ceiling'], _DEFAULT_SEAT_CEILING),
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The seat-status helpers `council:status` renders from.
3
+ *
4
+ * Extracted from `council_cli.ts` (road-to-admissible-council-seats 1.2/2.1):
5
+ * that file sits ~2,460 lines past the 1,500-line ceiling
6
+ * `check_source_size_budget` ratchets, so every line added there costs one
7
+ * excess line. These three surfaces are what 1.2 and 2.1 grew, and they are a
8
+ * coherent unit — "what does this package know about a seat before any call is
9
+ * made" — rather than an arbitrary slice taken to satisfy a gate.
10
+ *
11
+ * Nothing here authorizes anything. `disabledSeats` reports a note the config
12
+ * carries; `resolvedTransportFor` only ever forwards a REFUSAL input to the
13
+ * resolver.
14
+ */
15
+
16
+ import type { EnvironmentReport } from '../_lib/environment_detector.js';
17
+ import { detectEnvironment } from '../_lib/environment_detector.js';
18
+ import { DEFAULT_SEAT_CEILING, type ContentClass } from './content_ceiling.js';
19
+ import type { PolicyExclusion } from './seat_policy.js';
20
+ import { resolveMemberTransport } from './transport_resolver.js';
21
+
22
+ interface SeatLike {
23
+ readonly binary: string | null;
24
+ readonly api_key_ref: string | null;
25
+ readonly policy_exclusion?: PolicyExclusion | null;
26
+ readonly content_ceiling?: ContentClass;
27
+ }
28
+
29
+ interface ConfigLike {
30
+ readonly members: ReadonlyMap<string, { readonly enabled: boolean; readonly disabled_reason: string | null }>;
31
+ }
32
+
33
+ /**
34
+ * Resolve the concrete transport a member would use on THIS machine right now.
35
+ *
36
+ * `cmd_status` exists to answer "is the council reachable, and will it bill me".
37
+ * Since the transport-mode setting was removed, the second half is not a value
38
+ * anyone can look up in the config — it is a per-machine resolution. Printing a
39
+ * configured mode would be the same class of stale answer the removed setting
40
+ * produced. `mode` is pinned to `auto` because the loader no longer emits
41
+ * anything else.
42
+ */
43
+ export function resolvedTransportFor(
44
+ name: string,
45
+ member: SeatLike,
46
+ report?: EnvironmentReport,
47
+ contentClass?: ContentClass | null,
48
+ ): ReturnType<typeof resolveMemberTransport> {
49
+ return resolveMemberTransport({
50
+ provider: name,
51
+ report: report ?? detectEnvironment(),
52
+ invocationMode: null,
53
+ memberSettings: null,
54
+ globalMode: 'auto',
55
+ binaryOverride: member.binary,
56
+ policyExclusion: member.policy_exclusion ?? null,
57
+ seatCeiling: member.content_ceiling ?? DEFAULT_SEAT_CEILING,
58
+ contentClass: contentClass ?? null,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Disabled seats and the reason each records. `NO_DISABLED_REASON` is the
64
+ * honest rendering of an unset field: an `enabled: false` with nothing beside
65
+ * it is a fact about the config, and hiding it is what made the seat silent.
66
+ */
67
+ export const NO_DISABLED_REASON = 'disabled, no reason recorded';
68
+
69
+ export function disabledSeats(cfg: ConfigLike | null): Record<string, string> {
70
+ if (cfg === null) {
71
+ return {};
72
+ }
73
+ return Object.fromEntries(
74
+ [...cfg.members.entries()]
75
+ .filter(([, m]) => !m.enabled)
76
+ .map(([n, m]) => [n, m.disabled_reason ?? NO_DISABLED_REASON]),
77
+ );
78
+ }
79
+
@@ -49,6 +49,12 @@ import {
49
49
  type EnvironmentReport,
50
50
  } from '../_lib/environment_detector.js';
51
51
  import { resolve_mode } from './modes.js';
52
+ import {
53
+ admitsContent,
54
+ DEFAULT_SEAT_CEILING,
55
+ type ContentClass,
56
+ } from './content_ceiling.js';
57
+ import { policyExclusionReason, type PolicyExclusion } from './seat_policy.js';
52
58
 
53
59
  /** A concrete transport a member can actually run on. `auto` is not one. */
54
60
  export type Transport = 'api' | 'manual' | 'cli';
@@ -62,7 +68,17 @@ export type Transport = 'api' | 'manual' | 'cli';
62
68
  * `absentReasonFromCliFailure` below for the mapping FROM the mid-flight
63
69
  * `CliFailureClass` a caller already classifies its errors into.
64
70
  */
65
- export type AbsentReason = 'no_binary' | 'no_auth' | 'timeout' | 'quota';
71
+ /** The four REACHABILITY reasons. Each means "the route cannot be run". */
72
+ export type UnreachableReason = 'no_binary' | 'no_auth' | 'timeout' | 'quota';
73
+
74
+ /**
75
+ * Reachability OR policy. The policy half (2.1) names the case the four
76
+ * original values could not: a route that IS reachable and must not be used.
77
+ * Both halves mean the same thing to a caller — the member is absent — which
78
+ * is why they share one union rather than sitting in two fields a caller could
79
+ * forget to check.
80
+ */
81
+ export type AbsentReason = UnreachableReason | PolicyExclusion;
66
82
 
67
83
  /** The accepted values of the `mode` key, including the resolver-only `auto`. */
68
84
  export const VALID_TRANSPORT_MODES: ReadonlySet<string> = new Set([
@@ -110,6 +126,19 @@ export interface ResolveTransportOptions {
110
126
  * env-key auth record exists for this provider" from the report.
111
127
  */
112
128
  readonly apiKeyPresent?: boolean;
129
+ /**
130
+ * A configured policy exclusion for this seat (2.1). Non-null resolves
131
+ * ABSENT whatever the mode says — a reachable route that must not be used
132
+ * is not made usable by being reachable.
133
+ */
134
+ readonly policyExclusion?: PolicyExclusion | null;
135
+ /** The seat's content ceiling (2.2). Defaults to `project-content`. */
136
+ readonly seatCeiling?: ContentClass | null;
137
+ /**
138
+ * The class of what this pass would send. Omitted → no content check runs,
139
+ * which is what keeps every existing caller's behaviour identical.
140
+ */
141
+ readonly contentClass?: ContentClass | null;
113
142
  }
114
143
 
115
144
  /** Auth sources that mean "the provider CLI can authenticate". */
@@ -161,6 +190,28 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
161
190
  const authSource = auth?.source ?? null;
162
191
  const billing = classifyBilling(provider, authSource);
163
192
 
193
+ // Policy first (2.1). Before the mode dispatch, because `api` and `cli`
194
+ // below return `available: true` UNCONDITIONALLY — a refusal placed after
195
+ // them would be unreachable for exactly the seats it exists to stop.
196
+ const exclusion = opts.policyExclusion ?? null;
197
+ if (exclusion !== null) {
198
+ return unavailable(provider, billing, policyExclusionReason(exclusion, provider), exclusion);
199
+ }
200
+ // Content ceiling (2.2). Only runs when the caller declared what it would
201
+ // send; an undeclared pass is unchanged, not silently refused.
202
+ const contentClass = opts.contentClass ?? null;
203
+ if (contentClass !== null) {
204
+ const verdict = admitsContent(opts.seatCeiling ?? DEFAULT_SEAT_CEILING, contentClass);
205
+ if (!verdict.admitted) {
206
+ return unavailable(
207
+ provider,
208
+ billing,
209
+ `${provider}: ${verdict.reason ?? 'content ceiling refused the payload'}`,
210
+ 'policy_content_ceiling',
211
+ );
212
+ }
213
+ }
214
+
164
215
  if (mode === 'manual') {
165
216
  return {
166
217
  available: true,
@@ -246,6 +297,10 @@ export interface ResolveMemberTransportOptions {
246
297
  readonly globalMode?: string | null;
247
298
  readonly binaryOverride?: string | null;
248
299
  readonly apiKeyPresent?: boolean;
300
+ /** Forwarded to `resolveTransport` (2.1 / 2.2). */
301
+ readonly policyExclusion?: PolicyExclusion | null;
302
+ readonly seatCeiling?: ContentClass | null;
303
+ readonly contentClass?: ContentClass | null;
249
304
  }
250
305
 
251
306
  export interface ResolvedMemberTransport extends ResolvedTransport {
@@ -287,6 +342,9 @@ export function resolveMemberTransport(
287
342
  // optional-boolean field, so the key is omitted entirely rather than
288
343
  // forwarded as `apiKeyPresent: undefined`.
289
344
  ...(opts.apiKeyPresent !== undefined ? { apiKeyPresent: opts.apiKeyPresent } : {}),
345
+ policyExclusion: opts.policyExclusion ?? null,
346
+ seatCeiling: opts.seatCeiling ?? null,
347
+ contentClass: opts.contentClass ?? null,
290
348
  });
291
349
  return { ...resolved, configuredMode };
292
350
  }