@event4u/agent-config 9.28.0 → 9.30.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +172 -5
  4. package/README.md +2 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/inbox.md +22 -1
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/explain-run.md +31 -1
  9. package/dist/agent-src/commands/team/adversarial.md +8 -7
  10. package/dist/agent-src/commands/team/delegate.md +14 -12
  11. package/dist/agent-src/commands/team/review.md +9 -8
  12. package/dist/agent-src/commands/team/status.md +9 -8
  13. package/dist/agent-src/commands/team.md +29 -19
  14. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  15. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  17. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +22 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
  19. package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
  20. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  21. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  22. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  23. package/dist/agent-src/rules/delegation-policy.md +11 -7
  24. package/dist/agent-src/rules/preservation-guard.md +9 -0
  25. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  26. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  27. package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
  28. package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
  29. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  30. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  31. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  32. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  33. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -5
  34. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  35. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  36. package/dist/agent-src/templates/AGENTS.md +4 -4
  37. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  38. package/dist/agent-src/templates/roadmaps.md +23 -0
  39. package/dist/cli/registry.js +2 -0
  40. package/dist/cli/registry.js.map +1 -1
  41. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  42. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
  43. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
  44. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
  45. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
  46. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  47. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  48. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  49. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  50. package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
  51. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  52. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  53. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  54. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  55. package/dist/cli-delegate/cmd_conformance.js +2803 -137
  56. package/dist/cli-delegate/cmd_doctor.js +10 -5
  57. package/dist/cli-delegate/cmd_explain.js +2 -2
  58. package/dist/cli-delegate/cmd_export.js +4 -4
  59. package/dist/cli-delegate/cmd_handoff.js +88 -8
  60. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  61. package/dist/cli-delegate/cmd_migrate.js +2 -2
  62. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  63. package/dist/cli-delegate/cmd_prune.js +4 -4
  64. package/dist/cli-delegate/cmd_route_audit.js +3 -2
  65. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  66. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  67. package/dist/cli-delegate/cmd_settings_get.js +2 -8
  68. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +4 -4
  71. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  72. package/dist/cli-delegate/cmd_update.js +1 -1
  73. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +1 -1
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +72 -23
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +3 -2
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +39 -2
  82. package/dist/discovery/trust-report.md +2 -2
  83. package/dist/discovery/workspaces.json +12 -4
  84. package/dist/hooks/dispatch.js +2278 -611
  85. package/dist/install/install.mjs +56 -17
  86. package/dist/mcp/registry-manifest.json +3 -3
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +4122 -699
  89. package/dist/router.json +1 -1
  90. package/dist/scripts/_lib/agent_settings.js +54 -0
  91. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  92. package/dist/scripts/condense.js +147 -2
  93. package/dist/scripts/condense.js.map +1 -1
  94. package/dist/server/schemas/settings.js +12 -7
  95. package/dist/server/schemas/settings.js.map +1 -1
  96. package/dist/shared/settingsCarveOut.js +0 -9
  97. package/dist/shared/settingsCarveOut.js.map +1 -1
  98. package/dist/shared/settingsClasses.js +11 -9
  99. package/dist/shared/settingsClasses.js.map +1 -1
  100. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  101. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  102. package/dist/ui/index.html +1 -1
  103. package/docs/contracts/settings-classes.md +58 -12
  104. package/package.json +1 -1
  105. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  107. package/src/agent-src/templates/AGENTS.md +4 -4
  108. package/src/config/agent-settings.template.yml +88 -66
  109. package/src/config/discovery/packs.yml +11 -0
  110. package/src/config/discovery/workspaces.yml +1 -1
  111. package/src/config/dispatch-economy-metrics.json +61 -0
  112. package/src/config/evaluator-budgets.json +4 -4
  113. package/src/config/gate-violation-baselines.json +3 -3
  114. package/src/config/hook-token-budget.json +91 -0
  115. package/src/config/recycle-threshold-budget.json +12 -0
  116. package/src/config/rule-enforcement-baseline.json +86 -0
  117. package/src/scripts/_cli/cmd_conformance.ts +31 -44
  118. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  119. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  120. package/src/scripts/_dispatch.bash +23 -0
  121. package/src/scripts/_lib/agent_settings.ts +58 -0
  122. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  123. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  124. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  125. package/src/scripts/_lib/host_capability.ts +63 -6
  126. package/src/scripts/_lib/judgment_ladder.ts +634 -0
  127. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  128. package/src/scripts/_lib/orchestration_record.ts +78 -10
  129. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  130. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  131. package/src/scripts/_lib/self_repair.ts +354 -13
  132. package/src/scripts/_lib/self_repair_store.ts +28 -0
  133. package/src/scripts/_lib/session_eol.ts +230 -0
  134. package/src/scripts/_lib/session_role.ts +42 -0
  135. package/src/scripts/_lib/subagent_capsule.ts +341 -0
  136. package/src/scripts/_lib/subagent_response.ts +94 -0
  137. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  138. package/src/scripts/_lib/subagent_steering.ts +9 -4
  139. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  140. package/src/scripts/_lib/worker_budget.ts +37 -2
  141. package/src/scripts/ai_council/clients.ts +126 -55
  142. package/src/scripts/ai_council/config.ts +94 -6
  143. package/src/scripts/ai_council/handoff.ts +113 -0
  144. package/src/scripts/ai_council/modes.ts +19 -4
  145. package/src/scripts/ai_council/orchestrator.ts +105 -0
  146. package/src/scripts/ai_council/quorum.ts +84 -0
  147. package/src/scripts/ai_council/session.ts +44 -0
  148. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  149. package/src/scripts/ai_team/availability.ts +110 -0
  150. package/src/scripts/ai_team/config.ts +34 -14
  151. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  152. package/src/scripts/ask_transport.ts +221 -0
  153. package/src/scripts/bench_critic_protocol.ts +396 -0
  154. package/src/scripts/bench_hook_injection.ts +234 -0
  155. package/src/scripts/check_adr_frontmatter.ts +84 -40
  156. package/src/scripts/check_branch_freshness.ts +1 -0
  157. package/src/scripts/check_corpus_staleness.ts +18 -4
  158. package/src/scripts/check_detector_corpus.ts +219 -0
  159. package/src/scripts/check_gitignore_freshness.ts +42 -8
  160. package/src/scripts/check_hook_bundle_freshness.ts +24 -1
  161. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  162. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  163. package/src/scripts/check_static_layer_stability.ts +154 -0
  164. package/src/scripts/check_suppression_hygiene.ts +48 -0
  165. package/src/scripts/condense.ts +162 -2
  166. package/src/scripts/council_cli.ts +401 -25
  167. package/src/scripts/dispatch_economy_report.ts +448 -0
  168. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  169. package/src/scripts/explain_run.ts +206 -4
  170. package/src/scripts/forensics_report.ts +333 -0
  171. package/src/scripts/generate_settings_reference.ts +1 -0
  172. package/src/scripts/handoff_context_hook.ts +112 -4
  173. package/src/scripts/hook_manifest.yaml +117 -10
  174. package/src/scripts/hooks/concern_registry.ts +8 -0
  175. package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
  176. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  177. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  178. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  179. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  180. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  181. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  182. package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
  183. package/src/scripts/install-hooks.sh +1 -1
  184. package/src/scripts/language_mirror_hook.ts +89 -0
  185. package/src/scripts/lint_decision_impact.ts +14 -1
  186. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  187. package/src/scripts/lint_hook_manifest.ts +82 -0
  188. package/src/scripts/lint_never_silent.ts +276 -0
  189. package/src/scripts/lint_no_activation_gates.ts +271 -0
  190. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  191. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  192. package/src/scripts/measure_turn_end_gate.ts +194 -0
  193. package/src/scripts/orchestration_record.ts +12 -1
  194. package/src/scripts/rebuild_hook_bundle.ts +178 -0
  195. package/src/scripts/release.ts +74 -2
  196. package/src/scripts/report_conformance_funnel.ts +284 -0
  197. package/src/scripts/report_imperative_density.ts +1 -0
  198. package/src/scripts/routing_doctor.ts +118 -36
  199. package/src/scripts/score_skill_selection.ts +19 -5
  200. package/src/scripts/self_repair_cli.ts +337 -59
  201. package/src/scripts/session_eol_report.ts +261 -0
  202. package/src/scripts/team_events_spike.ts +271 -0
  203. package/src/scripts/team_review_gate_hook.ts +31 -9
  204. package/src/server/schemas/settings.ts +24 -17
  205. package/src/shared/settingsCarveOut.ts +0 -10
  206. package/src/shared/settingsClasses.ts +11 -9
  207. package/src/templates/compact-instructions.md +40 -0
  208. package/dist/ui/assets/index-DmR0mbk4.js +0 -26
  209. package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
@@ -48,10 +48,22 @@ import {
48
48
  type BillingClass,
49
49
  type EnvironmentReport,
50
50
  } from '../_lib/environment_detector.js';
51
+ import { resolve_mode } from './modes.js';
51
52
 
52
53
  /** A concrete transport a member can actually run on. `auto` is not one. */
53
54
  export type Transport = 'api' | 'manual' | 'cli';
54
55
 
56
+ /**
57
+ * Machine-readable reason a member is `absent` from a pass
58
+ * (road-to-always-on-orchestration Phase 3.2). The static half — `no_binary`
59
+ * / `no_auth` — is decided here, at resolution time, before any provider
60
+ * call is attempted. The runtime half — `timeout` / `quota` — only exists
61
+ * once a call was actually attempted and failed; see
62
+ * `absentReasonFromCliFailure` below for the mapping FROM the mid-flight
63
+ * `CliFailureClass` a caller already classifies its errors into.
64
+ */
65
+ export type AbsentReason = 'no_binary' | 'no_auth' | 'timeout' | 'quota';
66
+
55
67
  /** The accepted values of the `mode` key, including the resolver-only `auto`. */
56
68
  export const VALID_TRANSPORT_MODES: ReadonlySet<string> = new Set([
57
69
  'api',
@@ -73,6 +85,16 @@ export interface ResolvedTransport {
73
85
  readonly makesProviderCall: boolean;
74
86
  /** One-line reason. Non-empty exactly when `available` is false. */
75
87
  readonly reason: string | null;
88
+ /**
89
+ * Machine-readable classification of `reason`, for a pass artifact that
90
+ * needs to bucket absent members without parsing prose (Phase 3.2).
91
+ * Non-null for the two STATIC auto-chain failures this function can
92
+ * classify (`no_binary` / `no_auth`); `null` when `available` is true,
93
+ * and also `null` for the "unknown mode" validation-error path below —
94
+ * that is a config bug, not a missing-capability finding, so it does not
95
+ * belong in this enum.
96
+ */
97
+ readonly absentReason: AbsentReason | null;
76
98
  }
77
99
 
78
100
  export interface ResolveTransportOptions {
@@ -149,6 +171,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
149
171
  authSource,
150
172
  makesProviderCall: false,
151
173
  reason: null,
174
+ absentReason: null,
152
175
  };
153
176
  }
154
177
 
@@ -160,6 +183,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
160
183
  authSource,
161
184
  makesProviderCall: true,
162
185
  reason: null,
186
+ absentReason: null,
163
187
  };
164
188
  }
165
189
 
@@ -185,6 +209,7 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
185
209
  authSource,
186
210
  makesProviderCall: true,
187
211
  reason: null,
212
+ absentReason: null,
188
213
  };
189
214
  }
190
215
 
@@ -197,16 +222,80 @@ export function resolveTransport(opts: ResolveTransportOptions): ResolvedTranspo
197
222
  authSource,
198
223
  makesProviderCall: true,
199
224
  reason: null,
225
+ absentReason: null,
200
226
  };
201
227
  }
202
228
 
203
- return unavailable(provider, billing, autoUnavailableReason(provider, binary, cliAuthed));
229
+ return unavailable(
230
+ provider,
231
+ billing,
232
+ autoUnavailableReason(provider, binary, cliAuthed),
233
+ classifyStaticAbsentReason(binary),
234
+ );
235
+ }
236
+
237
+ export interface ResolveMemberTransportOptions {
238
+ readonly provider: string;
239
+ /** The single read-only environment report `resolveTransport` needs. */
240
+ readonly report: EnvironmentReport;
241
+ /** e.g. `/council mode:manual`. Highest precedence layer. */
242
+ readonly invocationMode?: string | null;
243
+ /** The member's own config sub-dict (`ai_council.members.<name>`). */
244
+ readonly memberSettings?: ReadonlyMap<string, unknown> | Record<string, unknown> | null;
245
+ /** `ai_council.mode` / `defaults.mode`, already extracted by the caller. */
246
+ readonly globalMode?: string | null;
247
+ readonly binaryOverride?: string | null;
248
+ readonly apiKeyPresent?: boolean;
249
+ }
250
+
251
+ export interface ResolvedMemberTransport extends ResolvedTransport {
252
+ /** The mode `resolve_mode` chose BEFORE any `auto` expansion. */
253
+ readonly configuredMode: string;
254
+ }
255
+
256
+ /**
257
+ * The single reconciled entry point for "given this member's config, what
258
+ * transport does it actually run on" (road-to-always-on-orchestration
259
+ * Phase 3.1). Composes `modes.ts::resolve_mode` (which layer decided the
260
+ * mode) with `resolveTransport` (what `auto` expands to on THIS machine) so
261
+ * a caller never has to replicate either step by hand.
262
+ *
263
+ * This exists because the two functions previously lived at arm's length:
264
+ * `resolve_mode` can legitimately return the literal string `'auto'`, and
265
+ * nothing forced a caller to then route that value through
266
+ * `resolveTransport` — a hand-rolled `mode === 'api' | 'cli' | 'manual'`
267
+ * switch (the shape `council_cli.ts::build_members` still uses today) has
268
+ * no `'auto'` case and treats it as an unrecognised transport instead of
269
+ * expanding it. `resolveMemberTransport` is the fix: there is no longer a
270
+ * gap between "which mode was configured" and "which transport that becomes"
271
+ * for any caller that uses it instead of the two functions separately.
272
+ */
273
+ export function resolveMemberTransport(
274
+ opts: ResolveMemberTransportOptions,
275
+ ): ResolvedMemberTransport {
276
+ const configuredMode = resolve_mode(opts.provider, {
277
+ invocationMode: opts.invocationMode ?? null,
278
+ memberSettings: opts.memberSettings ?? null,
279
+ globalMode: opts.globalMode ?? null,
280
+ });
281
+ const resolved = resolveTransport({
282
+ provider: opts.provider,
283
+ mode: configuredMode,
284
+ report: opts.report,
285
+ binaryOverride: opts.binaryOverride ?? null,
286
+ // `exactOptionalPropertyTypes` rejects an explicit `undefined` for an
287
+ // optional-boolean field, so the key is omitted entirely rather than
288
+ // forwarded as `apiKeyPresent: undefined`.
289
+ ...(opts.apiKeyPresent !== undefined ? { apiKeyPresent: opts.apiKeyPresent } : {}),
290
+ });
291
+ return { ...resolved, configuredMode };
204
292
  }
205
293
 
206
294
  function unavailable(
207
295
  _provider: string,
208
296
  billing: BillingClass,
209
297
  reason: string,
298
+ absentReason: AbsentReason | null = null,
210
299
  ): ResolvedTransport {
211
300
  return {
212
301
  available: false,
@@ -217,9 +306,21 @@ function unavailable(
217
306
  authSource: null,
218
307
  makesProviderCall: false,
219
308
  reason,
309
+ absentReason,
220
310
  };
221
311
  }
222
312
 
313
+ /**
314
+ * `no_binary` when the CLI binary itself never resolved (whether or not a
315
+ * credential was found — a credential with nothing to run it against is
316
+ * still "the binary is missing" from the caller's point of view); `no_auth`
317
+ * when the binary IS present but neither the cli-auth nor the key rung
318
+ * resolved. Mirrors `autoUnavailableReason`'s three-way branch below.
319
+ */
320
+ function classifyStaticAbsentReason(binary: string | null): AbsentReason {
321
+ return binary === null ? 'no_binary' : 'no_auth';
322
+ }
323
+
223
324
  /** The one-line reason `auto` prints when no rung resolved. */
224
325
  function autoUnavailableReason(
225
326
  provider: string,
@@ -298,6 +399,31 @@ export function isFallbackEligible(failure: CliFailureClass): boolean {
298
399
  return FALLBACK_ELIGIBLE.has(failure);
299
400
  }
300
401
 
402
+ /**
403
+ * Maps a mid-flight `CliFailureClass` (a call was actually attempted and
404
+ * failed) onto the same `AbsentReason` enum a static resolution failure
405
+ * uses (road-to-always-on-orchestration Phase 3.2/3.4), so a pass artifact
406
+ * can bucket every absent member — whether it never got a transport at all
407
+ * or lost one partway through — under one vocabulary. Returns `null` for a
408
+ * failure class outside the four-value enum (`cli_unsupported`,
409
+ * `server_error`, `other`); a caller falls back to the raw failure detail
410
+ * for those rather than mis-classifying them.
411
+ */
412
+ export function absentReasonFromCliFailure(failure: CliFailureClass): AbsentReason | null {
413
+ switch (failure) {
414
+ case 'binary_missing':
415
+ return 'no_binary';
416
+ case 'auth_rejected':
417
+ return 'no_auth';
418
+ case 'timeout':
419
+ return 'timeout';
420
+ case 'quota_exhausted':
421
+ return 'quota';
422
+ default:
423
+ return null;
424
+ }
425
+ }
426
+
301
427
  /**
302
428
  * One-shot, per-provider fallback ledger for a single invocation.
303
429
  *
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `/team` availability — CLI-first, not settings-gated
3
+ * (road-to-always-on-orchestration Phase 1, Step 1.3).
4
+ *
5
+ * `ai_team.enabled` — the former master switch — was DELETED. `/team`'s
6
+ * on/off state is now a FACT resolved from the machine, the same doctrine
7
+ * already applied to the subagent and council layers (Phase 1): the codex
8
+ * CLI binary must resolve on `$PATH`, AND some auth for the `openai`
9
+ * provider must be detectable (subscription login, API key, key file, or
10
+ * env key — the same transport-preference order `auto` walks via
11
+ * `strongestAuth`). Reuses the existing, read-only, zero-spend,
12
+ * per-process-cached detector (`_lib/environment_detector.ts`); this module
13
+ * adds NO new probing and NO new cache of its own.
14
+ *
15
+ * `ai_team.allow_delegate` is UNCHANGED by this module: a distinct,
16
+ * write-access authorization gate that stacks ON TOP of availability, never
17
+ * a replacement for it (`assert_delegate_allowed` in `team_dispatch.ts`
18
+ * still checks it separately, after availability).
19
+ */
20
+ import { detectEnvironment, strongestAuth } from '../_lib/environment_detector.js';
21
+ import { load_agent_settings, type SettingsDict } from '../_lib/agent_settings.js';
22
+ import { _team_codex_binary_name, shutilWhich } from '../_cli/cmd_doctor.js';
23
+
24
+ /** Tool id `environment_detector` uses for the codex CLI (`install/toolDetection.ts`). */
25
+ const CODEX_HOST_ID = 'codex';
26
+
27
+ /** Provider id `environment_detector` / `PROVIDER_CLI_META` uses for the codex CLI. */
28
+ const CODEX_PROVIDER = 'openai';
29
+
30
+ /** The provider default `_team_codex_binary_name` falls back to with no override configured. */
31
+ const DEFAULT_CODEX_BINARY = 'codex';
32
+
33
+ export interface TeamAvailability {
34
+ readonly available: boolean;
35
+ /** One clear line for the user — populated only when `available` is false. */
36
+ readonly reason: string | null;
37
+ }
38
+
39
+ /**
40
+ * codex CLI binary + auth presence — the availability half of the `/team`
41
+ * gate. Cached per process via `detectEnvironment()`'s own no-argument memo
42
+ * (`resetEnvironmentCache()` in tests); this function adds no caching of
43
+ * its own.
44
+ *
45
+ * m5 fix (independent-review finding) — `detectEnvironment()`'s host probe
46
+ * is hardcoded to the literal binary name `codex`; it has no way to be told
47
+ * "check this other name/path instead". So an operator who set
48
+ * `members.openai.binary:` in `.ai-council.yml` (honoured under an
49
+ * effective mode of `cli` OR `auto` — `cmd_doctor.ts::_mayRunOverCli`) was
50
+ * invisible to this check: `/team` could report "codex CLI not available"
51
+ * even with a working, custom-named/pathed binary configured and
52
+ * functional. `_team_codex_binary_name` (shared with `cmd_doctor.ts`'s own
53
+ * `council-cli` / `team` checks, so the override-resolution logic has one
54
+ * source) resolves the SAME name `cmd_doctor` would use; when it differs
55
+ * from the provider default, this probes that name directly instead of
56
+ * trusting the fixed-name host detector.
57
+ */
58
+ export function checkCodexAvailability(project_root: string = process.cwd()): TeamAvailability {
59
+ const report = detectEnvironment();
60
+ const configuredBinary = _team_codex_binary_name(project_root);
61
+ const installed =
62
+ configuredBinary !== DEFAULT_CODEX_BINARY
63
+ ? shutilWhich(configuredBinary) !== null
64
+ : (report.hosts.find((h) => h.id === CODEX_HOST_ID)?.installed ?? false);
65
+ if (!installed) {
66
+ return {
67
+ available: false,
68
+ reason:
69
+ 'codex CLI not available — /team needs the codex CLI installed and ' +
70
+ 'authenticated. Install: `npm install -g @openai/codex`, then run ' +
71
+ '`codex login`. Verify with `agent-config doctor --check team`.',
72
+ };
73
+ }
74
+ if (strongestAuth(report, CODEX_PROVIDER) === null) {
75
+ return {
76
+ available: false,
77
+ reason:
78
+ 'codex CLI not available — no auth detected for it. Run `codex login` ' +
79
+ '(or set `OPENAI_API_KEY`). Verify with `agent-config doctor --check team`.',
80
+ };
81
+ }
82
+ return { available: true, reason: null };
83
+ }
84
+
85
+ /**
86
+ * `emergency.orchestration_halt` — the one audited incident switch over the
87
+ * always-on orchestration stack (subagents, council, TEAM). Reads the
88
+ * project settings cascade fresh (or an injected dict for tests) on every
89
+ * call — no cache of its own, so an armed halt takes effect on the very
90
+ * next read, same as every other `orchestration_halt` reader in the tree
91
+ * (`routing_doctor.ts`, `delegation_nudge_hook.ts`).
92
+ */
93
+ export function isOrchestrationHalted(
94
+ settings?: SettingsDict | null,
95
+ cwd?: string | null,
96
+ ): boolean {
97
+ const merged = settings ?? load_agent_settings({ cwd: cwd ?? process.cwd() });
98
+ const emergency = (merged as Record<string, unknown>)['emergency'];
99
+ if (typeof emergency !== 'object' || emergency === null || Array.isArray(emergency)) {
100
+ return false;
101
+ }
102
+ return (emergency as Record<string, unknown>)['orchestration_halt'] === true;
103
+ }
104
+
105
+ /** The one-line halt notice — `/team` prints this verbatim when halted. */
106
+ export const ORCHESTRATION_HALT_MESSAGE =
107
+ '/team is halted (`emergency.orchestration_halt: true`) — the always-on ' +
108
+ 'orchestration stack (subagents, council, team) is paused for incident ' +
109
+ 'response. Resume: set `orchestration_halt: false` with a non-empty ' +
110
+ '`orchestration_halt_justification` in `.agent-settings.yml`.';
@@ -4,23 +4,36 @@
4
4
  *
5
5
  * Mirrors the fail-closed validation posture of the council loader
6
6
  * (`src/scripts/ai_council/config.ts`) at a fraction of its surface: the
7
- * `ai_team` block has five flat keys and no nested sub-schemas, so this
8
- * loader is deliberately lean — defaults, per-key type checks, and hard
9
- * unknown-key rejection. Contract: `docs/contracts/ai-team-config.md`.
7
+ * `ai_team` block has a handful of flat keys and no nested sub-schemas
8
+ * beyond `review_gate`, so this loader is deliberately lean — defaults,
9
+ * per-key type checks, and hard unknown-key rejection. Contract:
10
+ * `docs/contracts/ai-team-config.md`.
10
11
  *
11
12
  * Unlike the council (ALWAYS user-global `.ai-council.yml`, ADR-104),
12
13
  * `ai_team` lives in the PROJECT settings cascade (`.agent-settings.yml`
13
- * via `load_agent_settings`) — team mode is a per-project posture
14
- * (default-off), not a per-developer credential store; the codex CLI
15
- * carries the subscription auth itself.
14
+ * via `load_agent_settings`) — team mode is a per-project posture, not a
15
+ * per-developer credential store; the codex CLI carries the subscription
16
+ * auth itself.
17
+ *
18
+ * `enabled` — the former master on/off switch — was DELETED
19
+ * (road-to-always-on-orchestration Phase 1, Step 1.3): `/team`'s
20
+ * availability is now a FACT resolved from the machine (codex CLI on
21
+ * `$PATH` + detectable auth — `src/scripts/ai_team/availability.ts`), not
22
+ * a settings flag, matching the doctrine already applied to the
23
+ * subagent/council layers. A leftover `enabled` key from an older install
24
+ * is accepted-and-ignored here (never rejected as unknown, never applied
25
+ * by any reader) so an existing project's `.agent-settings.yml` does not
26
+ * start failing `/team` outright; `load_agent_settings` still surfaces the
27
+ * one-line deprecation warning for it (`_lib/agent_settings.ts`).
16
28
  *
17
29
  * Validation contract (all enforced at load time, fail-closed):
18
30
  *
19
- * 1. `ai_team` absent → defaults (feature off). Non-mapping → error.
31
+ * 1. `ai_team` absent → defaults. Non-mapping → error.
20
32
  * 2. Unknown keys under `ai_team` are rejected — a typo must never
21
33
  * silently disable a gate (`allow_delegate` misspelled = delegation
22
- * stays off AND the load fails loudly).
23
- * 3. `enabled`, `allow_delegate`, `suppress_setup_hint` are booleans.
34
+ * stays off AND the load fails loudly). `enabled` is the one
35
+ * deliberate exception: accepted, untyped, ignored (see above).
36
+ * 3. `allow_delegate`, `suppress_setup_hint` are booleans.
24
37
  * 4. `model` is a non-empty string. `'auto'` = pass no `--model` flag
25
38
  * (the codex CLI default applies); any other value passes through
26
39
  * verbatim.
@@ -47,7 +60,6 @@ export interface AiTeamReviewGateConfig {
47
60
  }
48
61
 
49
62
  export interface AiTeamConfig {
50
- readonly enabled: boolean;
51
63
  readonly model: string;
52
64
  readonly allow_delegate: boolean;
53
65
  readonly max_calls_per_day: number;
@@ -63,7 +75,6 @@ export const AI_TEAM_REVIEW_GATE_DEFAULTS: AiTeamReviewGateConfig = Object.freez
63
75
 
64
76
  /** Shipped defaults — byte-parity with `src/config/agent-settings.template.yml`. */
65
77
  export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
66
- enabled: false,
67
78
  model: 'auto',
68
79
  allow_delegate: false,
69
80
  max_calls_per_day: 50,
@@ -79,7 +90,6 @@ export const AI_TEAM_DEFAULTS: AiTeamConfig = Object.freeze({
79
90
  export const AI_TEAM_MODEL_AUTO = 'auto';
80
91
 
81
92
  const _KNOWN_KEYS: ReadonlySet<string> = new Set([
82
- 'enabled',
83
93
  'model',
84
94
  'allow_delegate',
85
95
  'max_calls_per_day',
@@ -87,6 +97,14 @@ const _KNOWN_KEYS: ReadonlySet<string> = new Set([
87
97
  'review_gate',
88
98
  ]);
89
99
 
100
+ /**
101
+ * Deleted key accepted-and-ignored for migration grace (see module docstring).
102
+ * Never added to `_KNOWN_KEYS` — it carries no validated type and is never
103
+ * read into `AiTeamConfig`; it exists in this set only so the unknown-key
104
+ * loop below does not fail-closed an existing project's leftover value.
105
+ */
106
+ const _DELETED_KEYS: ReadonlySet<string> = new Set(['enabled']);
107
+
90
108
  const _KNOWN_REVIEW_GATE_KEYS: ReadonlySet<string> = new Set([
91
109
  'managed',
92
110
  'max_consecutive_blocks',
@@ -175,6 +193,9 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
175
193
  }
176
194
 
177
195
  for (const key of Object.keys(raw)) {
196
+ if (_DELETED_KEYS.has(key)) {
197
+ continue; // deleted, accepted-and-ignored — see module docstring.
198
+ }
178
199
  if (!_KNOWN_KEYS.has(key)) {
179
200
  throw new TeamConfigError(
180
201
  `ai_team.${key}: unknown key; valid: ` +
@@ -183,7 +204,6 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
183
204
  }
184
205
  }
185
206
 
186
- const enabled = _requireBool(raw, 'enabled', AI_TEAM_DEFAULTS.enabled);
187
207
  const allow_delegate = _requireBool(raw, 'allow_delegate', AI_TEAM_DEFAULTS.allow_delegate);
188
208
  const suppress_setup_hint = _requireBool(
189
209
  raw,
@@ -216,7 +236,7 @@ export function build_ai_team_config(raw: unknown): AiTeamConfig {
216
236
 
217
237
  const review_gate = _build_review_gate('review_gate' in raw ? raw['review_gate'] : undefined);
218
238
 
219
- return { enabled, model, allow_delegate, max_calls_per_day, suppress_setup_hint, review_gate };
239
+ return { model, allow_delegate, max_calls_per_day, suppress_setup_hint, review_gate };
220
240
  }
221
241
 
222
242
  /**
@@ -18,11 +18,20 @@
18
18
  * instead of live repo access, no background jobs, explicitly worse than the
19
19
  * plugin — pointing Claude-Code users back to the Phase 2 plugin path.
20
20
  *
21
- * Governance reuse (Phase 3 Step 3): same `ai_team` config (fail-closed when
22
- * `enabled: false`), same `cli_call_budget` openai quota bucket (the gate and
23
- * the recording both live in `CliClient.ask` — no new counter), same
24
- * `_AUTH_FAILURE_PATTERNS` classification. `--manual` renders the bundle
25
- * between `═` rules for paste-into-web usage — no call, no quota.
21
+ * Governance reuse (Phase 3 Step 3): same `ai_team` config, same
22
+ * `cli_call_budget` openai quota bucket (the gate and the recording both
23
+ * live in `CliClient.ask` — no new counter), same `_AUTH_FAILURE_PATTERNS`
24
+ * classification. `--manual` renders the bundle between `═` rules for
25
+ * paste-into-web usage — no call, no quota.
26
+ *
27
+ * Availability (road-to-always-on-orchestration Phase 1, Step 1.3):
28
+ * `ai_team.enabled` was DELETED. Every dispatch entry point below is
29
+ * fail-closed on TWO facts instead — `emergency.orchestration_halt` (the
30
+ * one audited incident switch over the always-on stack) and
31
+ * `checkCodexAvailability()` (codex CLI + auth presence,
32
+ * `./availability.js`) — never a settings flag. `ai_team.allow_delegate`
33
+ * is unchanged: a second, write-access opt-in that stacks ON TOP of
34
+ * availability in `assert_delegate_allowed`.
26
35
  *
27
36
  * The review system prompt is derived from the adversarial-review findings
28
37
  * shape of an Apache-2.0 upstream — attribution in the repo-root NOTICE file.
@@ -38,6 +47,12 @@ import {
38
47
  load_cli_call_counts,
39
48
  OpenAICliClient,
40
49
  } from '../ai_council/clients.js';
50
+ import {
51
+ checkCodexAvailability,
52
+ isOrchestrationHalted,
53
+ ORCHESTRATION_HALT_MESSAGE,
54
+ type TeamAvailability,
55
+ } from './availability.js';
41
56
  import { AI_TEAM_MODEL_AUTO, type AiTeamConfig, load_ai_team_config } from './config.js';
42
57
 
43
58
  // ── errors ──────────────────────────────────────────────────────────────
@@ -51,50 +66,67 @@ export class TeamDispatchError extends Error {
51
66
  }
52
67
 
53
68
  /**
54
- * Raised when `ai_team.enabled` is false — the fail-closed gate.
55
- * The message carries the enable pointer; callers print it verbatim.
69
+ * Raised when `/team` is unavailable — either `emergency.orchestration_halt`
70
+ * is set, or the codex CLI / auth availability check
71
+ * (`checkCodexAvailability`) failed. `reason` is the one-line message the
72
+ * relevant check produced; callers print it verbatim. There is no more
73
+ * settings flag behind this — see the module docstring.
56
74
  */
57
75
  export class TeamDisabledError extends TeamDispatchError {
58
- constructor() {
59
- super(
60
- 'team mode is disabled (`ai_team.enabled: false`, the shipped default). ' +
61
- 'Set `ai_team.enabled: true` in `.agent-settings.yml` to opt in — ' +
62
- 'see docs/contracts/ai-team-config.md.',
63
- );
76
+ constructor(reason: string) {
77
+ super(reason);
64
78
  this.name = 'TeamDisabledError';
65
79
  }
66
80
  }
67
81
 
68
82
  /**
69
- * Thrown by `assert_delegate_allowed` when `ai_team.enabled` is true but the
83
+ * Thrown by `assert_delegate_allowed` when `/team` is available but the
70
84
  * second opt-in (`ai_team.allow_delegate`) is not. `/team delegate` is the
71
85
  * only wrapper that hands WRITE access to the second model, so it is
72
- * double-gated; `enabled: true` alone is never delegate authorization.
86
+ * double-gated; availability alone is never delegate authorization.
73
87
  */
74
88
  export class TeamDelegateDisabledError extends TeamDispatchError {
75
89
  constructor() {
76
90
  super(
77
91
  '`/team delegate` is disabled (`ai_team.allow_delegate: false`, the shipped ' +
78
92
  'default). It is the only team-mode wrapper that hands write access to ' +
79
- 'the second model, so it needs its own opt-in: set both ' +
80
- '`ai_team.enabled: true` and `ai_team.allow_delegate: true` in ' +
81
- '`.agent-settings.yml` — see docs/contracts/ai-team-config.md.',
93
+ 'the second model, so it needs its own opt-in: set ' +
94
+ '`ai_team.allow_delegate: true` in `.agent-settings.yml` (once /team ' +
95
+ 'itself is available — codex CLI installed and authenticated) — see ' +
96
+ 'docs/contracts/ai-team-config.md.',
82
97
  );
83
98
  this.name = 'TeamDelegateDisabledError';
84
99
  }
85
100
  }
86
101
 
102
+ /** Test seams for the two availability facts (`assert_delegate_allowed`, `run_team_review`). */
103
+ export interface TeamAvailabilityOverrides {
104
+ /** Default: `checkCodexAvailability()` — the real, cached, zero-spend probe. */
105
+ availability?: TeamAvailability;
106
+ /** Default: `isOrchestrationHalted(undefined, cwd)` — reads the real settings cascade. */
107
+ halted?: boolean;
108
+ }
109
+
87
110
  /**
88
111
  * Deterministic mirror of the `/team delegate` double gate: throws
89
- * `TeamDisabledError` unless `ai_team.enabled`, then
112
+ * `TeamDisabledError` when halted or `/team` is unavailable, then
90
113
  * `TeamDelegateDisabledError` unless `ai_team.allow_delegate`. The command
91
114
  * doc's prose gates instruct the agent; this guard is the machine-checkable
92
115
  * contract (`--delegate-gate` CLI mode) tests pin against.
93
116
  */
94
- export function assert_delegate_allowed(config?: AiTeamConfig, cwd?: string | null): void {
117
+ export function assert_delegate_allowed(
118
+ config?: AiTeamConfig,
119
+ cwd?: string | null,
120
+ overrides: TeamAvailabilityOverrides = {},
121
+ ): void {
95
122
  const cfg = config ?? load_ai_team_config({ cwd: cwd ?? null });
96
- if (!cfg.enabled) {
97
- throw new TeamDisabledError();
123
+ const halted = overrides.halted ?? isOrchestrationHalted(undefined, cwd ?? null);
124
+ if (halted) {
125
+ throw new TeamDisabledError(ORCHESTRATION_HALT_MESSAGE);
126
+ }
127
+ const availability = overrides.availability ?? checkCodexAvailability(cwd ?? undefined);
128
+ if (!availability.available) {
129
+ throw new TeamDisabledError(availability.reason ?? 'codex CLI not available.');
98
130
  }
99
131
  if (!cfg.allow_delegate) {
100
132
  throw new TeamDelegateDisabledError();
@@ -511,6 +543,10 @@ export interface RunTeamReviewOptions {
511
543
  cli_calls_path?: string | null;
512
544
  /** Sink for user-facing output; default `process.stdout`. */
513
545
  out?: (line: string) => void;
546
+ /** Test seam: override the availability check (default: the real, cached probe). */
547
+ availability?: TeamAvailability;
548
+ /** Test seam: override the halted flag (default: read from the settings cascade). */
549
+ halted?: boolean;
514
550
  }
515
551
 
516
552
  function _quota_snapshot(config: AiTeamConfig, cli_calls_path: string | null): TeamReviewQuota {
@@ -564,16 +600,22 @@ function _envelope_from_response(
564
600
  /**
565
601
  * Run the multi-host team-review fallback.
566
602
  *
567
- * Order of operations is the contract: (1) fail-closed config gate,
568
- * (2) capability-delta header at the top, (3) read-only bundle,
569
- * (4) manual render OR one synchronous `codex exec` call through the shared
570
- * quota/auth machinery, (5) envelope emission. Throws `TeamDisabledError`
571
- * when `ai_team.enabled` is false — never a silent no-op.
603
+ * Order of operations is the contract: (1) fail-closed availability gate —
604
+ * halted, then codex CLI/auth — (2) capability-delta header at the top,
605
+ * (3) read-only bundle, (4) manual render OR one synchronous `codex exec`
606
+ * call through the shared quota/auth machinery, (5) envelope emission.
607
+ * Throws `TeamDisabledError` when halted or unavailable — never a silent
608
+ * no-op.
572
609
  */
573
610
  export function run_team_review(opts: RunTeamReviewOptions = {}): TeamReviewRunResult {
574
611
  const config = opts.config ?? load_ai_team_config({ cwd: opts.cwd ?? null });
575
- if (!config.enabled) {
576
- throw new TeamDisabledError();
612
+ const halted = opts.halted ?? isOrchestrationHalted(undefined, opts.cwd ?? null);
613
+ if (halted) {
614
+ throw new TeamDisabledError(ORCHESTRATION_HALT_MESSAGE);
615
+ }
616
+ const availability = opts.availability ?? checkCodexAvailability(opts.cwd ?? undefined);
617
+ if (!availability.available) {
618
+ throw new TeamDisabledError(availability.reason ?? 'codex CLI not available.');
577
619
  }
578
620
  const out = opts.out ?? ((line: string): void => void process.stdout.write(`${line}\n`));
579
621