@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
@@ -318,6 +318,87 @@ function _check_dead_concerns(
318
318
  }
319
319
  }
320
320
 
321
+ /**
322
+ * Role-axis validation (road-to-token-economy-dispatch Phase 2). Errors on:
323
+ * - a `roles` block that is not a mapping of role → { drop: [names] }
324
+ * - a drop entry naming an unknown concern
325
+ * - a drop entry naming a concern bound to ANY platform's `pre_tool_use`
326
+ * slot — the safety-guard slot is undroppable (Phase 2.3: "the manifest
327
+ * diff must show zero pre_tool_use guard removals, CI-checked"). The
328
+ * dispatcher also refuses pre_tool_use drops at runtime; this check
329
+ * makes the attempt a red build instead of a silent no-op.
330
+ */
331
+ export function _check_roles(
332
+ manifest: YamlObject,
333
+ concernNames: Set<string>,
334
+ errors: string[],
335
+ ): void {
336
+ const rolesRaw = manifest["roles"];
337
+ if (rolesRaw === undefined || rolesRaw === null) {
338
+ return; // no role axis — valid (every chain is the orchestrator default)
339
+ }
340
+ if (!isObject(rolesRaw)) {
341
+ errors.push(`roles: must be a mapping, got ${pyTypeName(rolesRaw)}`);
342
+ return;
343
+ }
344
+ // Concerns bound on any platform's pre_tool_use slot — the undroppable set.
345
+ const guardBound = new Set<string>();
346
+ const platformsRaw = manifest["platforms"];
347
+ if (isObject(platformsRaw)) {
348
+ for (const block of Object.values(platformsRaw)) {
349
+ if (!isObject(block)) continue;
350
+ const pre = block["pre_tool_use"];
351
+ if (Array.isArray(pre)) {
352
+ for (const n of pre) {
353
+ if (typeof n === "string") guardBound.add(n);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ for (const [role, spec] of Object.entries(rolesRaw)) {
359
+ if (!isObject(spec)) {
360
+ errors.push(`roles.${role}: must be a mapping, got ${pyTypeName(spec)}`);
361
+ continue;
362
+ }
363
+ const drop = spec["drop"];
364
+ if (drop === undefined || drop === null) {
365
+ continue; // a role without a drop list is a no-op entry — allowed
366
+ }
367
+ if (!Array.isArray(drop)) {
368
+ errors.push(`roles.${role}: 'drop' must be a list, got ${pyTypeName(drop)}`);
369
+ continue;
370
+ }
371
+ for (const name of drop) {
372
+ if (typeof name !== "string" || !concernNames.has(name)) {
373
+ errors.push(`roles.${role}: drop entry '${String(name)}' is not a known concern`);
374
+ continue;
375
+ }
376
+ if (guardBound.has(name)) {
377
+ errors.push(
378
+ `roles.${role}: drop entry '${name}' is bound to a pre_tool_use slot — ` +
379
+ `safety guards are undroppable on every role`,
380
+ );
381
+ }
382
+ // Severity-based twin of the slot check (review hardening 2026-08-10):
383
+ // a fail_closed or non-advisory concern is guard-shaped regardless of
384
+ // which slot it binds to — dropping one from a role chain would be a
385
+ // safety removal wearing an economy label.
386
+ const concernsRaw = manifest["concerns"];
387
+ const spec = isObject(concernsRaw) ? concernsRaw[name] : undefined;
388
+ if (isObject(spec)) {
389
+ const advisory = String(spec["severity"] ?? "").trim().toLowerCase() === "advisory";
390
+ const failClosed = spec["fail_closed"] === true;
391
+ if (failClosed || !advisory) {
392
+ errors.push(
393
+ `roles.${role}: drop entry '${name}' is ${failClosed ? "fail_closed" : "not severity: advisory"} — ` +
394
+ `only advisory, fail-open concerns may be role-dropped`,
395
+ );
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+
321
402
  export function lint(manifestPath: string, strict: boolean): number {
322
403
  if (!_isFile(manifestPath)) {
323
404
  process.stderr.write(
@@ -342,6 +423,7 @@ export function lint(manifestPath: string, strict: boolean): number {
342
423
  const warnings: string[] = [];
343
424
  const concernNames = _check_concerns(manifest, errors);
344
425
  const bound = _check_platforms(manifest, concernNames, errors, warnings);
426
+ _check_roles(manifest, concernNames, errors);
345
427
  _check_aliases(manifest, errors);
346
428
  _check_orphan_trampolines(manifest, errors);
347
429
  _check_dead_concerns(concernNames, bound, warnings);
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * lint_never_silent — no shipped guidance may direct a silent re-run or the
4
+ * concealment of a detected miss.
5
+ *
6
+ * The hidden `attempt → critic → re-attempt` mechanism — silently re-running a
7
+ * turn so the user never notices the miss — was built, benchmarked and
8
+ * falsified (capability Δ = 0; council verdict TERMINAL, recorded in
9
+ * `src/skills/recursive-verification/SKILL.md`). The self-repair loop pins
10
+ * "correction is always visible" as a non-goal boundary. This gate keeps that
11
+ * boundary mechanical: a rule, skill, or command that *directs* an agent to
12
+ * silently re-run, or to hide a detected failure, is a CI failure — while
13
+ * prose that *describes* or *forbids* the falsified mechanism stays legal,
14
+ * because describing the failure mode is how the corpus teaches against it.
15
+ *
16
+ * The describe-vs-direct split is decided per matching line:
17
+ * - a line carrying a negation / prohibition / falsification marker
18
+ * ("never", "forbidden", "must not", "falsified", "violation", …) is a
19
+ * DESCRIPTION and passes;
20
+ * - a line carrying an explicit `<!-- never-silent-allow: <reason> -->`
21
+ * marker (same line or the line above) passes;
22
+ * - anything else that matches is a DIRECTIVE and fails.
23
+ *
24
+ * Exit codes: 0 = clean, 1 = violations, 2 = usage.
25
+ */
26
+ import * as fs from 'node:fs';
27
+ import * as os from 'node:os';
28
+ import * as path from 'node:path';
29
+ import { fileURLToPath, pathToFileURL } from 'node:url';
30
+
31
+ import { GateLedger } from './_lib/gate_ledger.js';
32
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
33
+ import { reportScanned } from './_lib/scan_scope.js';
34
+
35
+ const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
36
+ const REAL_REPO_ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
37
+
38
+ /** The shipped-guidance surfaces the ban covers, relative to the repo root. */
39
+ export const SCAN_ROOTS = ['src/rules', 'src/skills', 'src/agent-src/commands'] as const;
40
+
41
+ export const ALLOW_MARKER = 'never-silent-allow:';
42
+
43
+ /**
44
+ * The two directive families the falsified mechanism decomposes into:
45
+ * re-running without telling, and concealing what was detected.
46
+ */
47
+ const SILENT_RERUN_RES: readonly RegExp[] = [
48
+ /\bsilent(?:ly)?\s+(?:re-?run|re-?runs|re-?attempt|retry|retries|re-?execute)\b/i,
49
+ /\b(?:re-?run|re-?attempt|retry|re-?execute)\b[^.\n]{0,40}\bsilently\b/i,
50
+ ];
51
+ const CONCEAL_RES: readonly RegExp[] = [
52
+ /\b(?:hide|conceal|paper over|cover up|suppress)\b[^.\n]{0,50}\b(?:miss(?:es)?|failure|mistake|error|defect|violation)\b/i,
53
+ ];
54
+
55
+ /**
56
+ * A matching line that also carries one of these is prose ABOUT the mechanism —
57
+ * a prohibition, a description of the falsified variant, or a named failure
58
+ * mode — never a directive to perform it. Checked on the matching line AND the
59
+ * line above it: markdown wraps sentences, so the negation of a bullet like
60
+ * "Do NOT promise zero flake; … disguises silent retries" can sit one physical
61
+ * line before the phrase that matches.
62
+ */
63
+ const DESCRIPTION_MARKERS =
64
+ /\b(?:never|niemals|no\b|kein\w*|forbidden|must not|do(?:es)? not|don'?t|won'?t|cannot|can not|falsified|benchmarked|anti-?pattern|violation|banned|prohibited|refus\w*|stays out|is the failure|non-goal|disguis\w*|mask\w*|not\b)/i;
65
+
66
+ export interface Violation {
67
+ readonly file: string;
68
+ readonly line: number;
69
+ readonly text: string;
70
+ readonly family: 'silent-rerun' | 'conceal-miss';
71
+ }
72
+
73
+ function matchFamily(line: string): Violation['family'] | null {
74
+ if (SILENT_RERUN_RES.some((re) => re.test(line))) {
75
+ return 'silent-rerun';
76
+ }
77
+ if (CONCEAL_RES.some((re) => re.test(line))) {
78
+ return 'conceal-miss';
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /** Pure per-file check — exported so the corpus behaviour is unit-testable. */
84
+ export function checkText(file: string, text: string): Violation[] {
85
+ const out: Violation[] = [];
86
+ const lines = text.split('\n');
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const line = lines[i]!;
89
+ const family = matchFamily(line);
90
+ if (family === null) {
91
+ continue;
92
+ }
93
+ const prev = i > 0 ? lines[i - 1]! : '';
94
+ if (DESCRIPTION_MARKERS.test(line) || DESCRIPTION_MARKERS.test(prev)) {
95
+ continue;
96
+ }
97
+ if (line.includes(ALLOW_MARKER) || prev.includes(ALLOW_MARKER)) {
98
+ continue;
99
+ }
100
+ out.push({ file, line: i + 1, text: line.trim(), family });
101
+ }
102
+ return out;
103
+ }
104
+
105
+ function collectMarkdown(root: string): string[] {
106
+ const out: string[] = [];
107
+ const walk = (dir: string): void => {
108
+ let entries: fs.Dirent[];
109
+ try {
110
+ entries = fs.readdirSync(dir, { withFileTypes: true });
111
+ } catch {
112
+ return;
113
+ }
114
+ for (const e of entries) {
115
+ const p = path.join(dir, e.name);
116
+ if (e.isDirectory()) {
117
+ walk(p);
118
+ } else if (e.isFile() && e.name.endsWith('.md')) {
119
+ out.push(p);
120
+ }
121
+ }
122
+ };
123
+ walk(root);
124
+ return out.sort();
125
+ }
126
+
127
+ function parseArgs(argv: readonly string[]): { quiet: boolean } {
128
+ let quiet = false;
129
+ for (const a of argv) {
130
+ if (a === '--quiet') {
131
+ quiet = true;
132
+ } else if (a === '-h' || a === '--help') {
133
+ process.stdout.write('usage: lint_never_silent [--quiet]\n');
134
+ process.exit(0);
135
+ } else {
136
+ process.stderr.write(`lint_never_silent: unrecognized argument: ${a}\n`);
137
+ process.exit(2);
138
+ }
139
+ }
140
+ return { quiet };
141
+ }
142
+
143
+ export function selfTest(): number {
144
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lns-'));
145
+ const mk = (body: string): string => {
146
+ const root = fs.mkdtempSync(path.join(tmp, 'repo-'));
147
+ const p = path.join(root, 'src', 'rules', 'r.md');
148
+ fs.mkdirSync(path.dirname(p), { recursive: true });
149
+ fs.writeFileSync(p, `# R\n\n${body}\n`, 'utf-8');
150
+ return root;
151
+ };
152
+ const run = (root: string): number => {
153
+ process.env['LINT_NEVER_SILENT_ROOT'] = root;
154
+ try {
155
+ return runGateCli(REAL_REPO_ROOT, 'src/scripts/lint_never_silent.ts', ['--quiet'], root);
156
+ } finally {
157
+ delete process.env['LINT_NEVER_SILENT_ROOT'];
158
+ }
159
+ };
160
+ try {
161
+ return runSelfTest({
162
+ gate: 'lint_never_silent',
163
+ minCases: 3,
164
+ minRejectCases: 2,
165
+ cases: [
166
+ {
167
+ name: 'a directive to silently re-run the turn is rejected',
168
+ expect: 'reject',
169
+ run: () =>
170
+ run(mk('On a detected miss, silently re-run the turn and present the corrected answer.')),
171
+ },
172
+ {
173
+ name: 'a directive to conceal a detected failure is rejected',
174
+ expect: 'reject',
175
+ run: () => run(mk('If the gate fails, suppress the error and continue as if it passed.')),
176
+ },
177
+ {
178
+ name: 'a no-prefixed word (Note:, now) does not exonerate a directive — R2 finding #1',
179
+ expect: 'reject',
180
+ run: () => run(mk('Note: silently re-run the suite after fixing.')),
181
+ },
182
+ {
183
+ name: 'prose that FORBIDS the mechanism passes — describing is how the corpus teaches',
184
+ expect: 'accept',
185
+ run: () =>
186
+ run(mk('NEVER silently re-run the turn — that mechanism was built and falsified.')),
187
+ },
188
+ {
189
+ name: 'an explicit allow-marker passes',
190
+ expect: 'accept',
191
+ run: () =>
192
+ run(mk('<!-- never-silent-allow: negative example in documentation -->\nsilently re-run the turn')),
193
+ },
194
+ {
195
+ name: 'a clean rule passes',
196
+ expect: 'accept',
197
+ run: () => run(mk('Verify with the real tool and surface the result to the user.')),
198
+ },
199
+ ],
200
+ });
201
+ } finally {
202
+ fs.rmSync(tmp, { recursive: true, force: true });
203
+ }
204
+ }
205
+
206
+ export function main(argv?: readonly string[]): number {
207
+ const raw = argv ?? process.argv.slice(2);
208
+ if (raw.includes('--self-test')) {
209
+ return selfTest();
210
+ }
211
+ const args = parseArgs(raw);
212
+ const root = process.env['LINT_NEVER_SILENT_ROOT'] ?? REAL_REPO_ROOT;
213
+
214
+ const ledger = new GateLedger('lint_never_silent');
215
+ const violations: Violation[] = [];
216
+ let scanned = 0;
217
+ for (const rel of SCAN_ROOTS) {
218
+ for (const file of collectMarkdown(path.join(root, rel))) {
219
+ ledger.plan(file);
220
+ const found = checkText(path.relative(root, file), fs.readFileSync(file, 'utf-8'));
221
+ scanned += 1;
222
+ if (found.length > 0) {
223
+ violations.push(...found);
224
+ ledger.fail(file, `${String(found.length)} never-silent directive(s)`);
225
+ } else {
226
+ ledger.complete(file);
227
+ }
228
+ }
229
+ }
230
+
231
+ if (violations.length > 0) {
232
+ process.stderr.write(`❌ lint_never_silent: ${String(violations.length)} violation(s):\n`);
233
+ for (const v of violations) {
234
+ process.stderr.write(
235
+ ` • ${v.file}:${String(v.line)} [${v.family}] ${v.text}\n` +
236
+ ' Shipped guidance must not direct a silent re-run or the concealment of a\n' +
237
+ ' detected miss (the mechanism is falsified — correction is always visible).\n' +
238
+ ` Describing/forbidding it is fine; mark a deliberate negative example with\n` +
239
+ ` <!-- ${ALLOW_MARKER} <reason> --> on or above the line.\n`,
240
+ );
241
+ }
242
+ } else if (!args.quiet) {
243
+ process.stdout.write(`✅ never-silent clean — ${String(scanned)} shipped artefact(s).\n`);
244
+ }
245
+
246
+ ledger.report();
247
+ reportScanned({
248
+ gate: 'lint_never_silent',
249
+ scanned,
250
+ units: 'markdown artefact(s)',
251
+ roots: [...SCAN_ROOTS],
252
+ });
253
+ return violations.length > 0 ? 1 : 0;
254
+ }
255
+
256
+ const _HERE = fileURLToPath(import.meta.url);
257
+ function _isCliEntry(): boolean {
258
+ if (process.argv[1] === undefined) {
259
+ return false;
260
+ }
261
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
262
+ return true;
263
+ }
264
+ try {
265
+ return (
266
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
267
+ fs.realpathSync(path.resolve(process.argv[1]))
268
+ );
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ if (_isCliEntry() || process.argv[1] === _HERE) {
275
+ process.exit(main());
276
+ }
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `no-activation-gates` — CI backstop for the always-on-orchestration
4
+ * doctrine (road-to-always-on-orchestration Phase 1).
5
+ *
6
+ * The doctrine deletes every per-layer on/off setting for subagents,
7
+ * council, and team: `subagents.enabled`, `subagents.auto`,
8
+ * `subagents.host_capabilities`, and (Step 1.3) `ai_team.enabled` are gone
9
+ * from the shipped template, and there is exactly ONE surviving switch —
10
+ * `emergency.orchestration_halt`, the audited incident-only kill switch (see
11
+ * `docs/contracts/settings-classes.md` § "The one exception").
12
+ *
13
+ * This gate fails the build if the template ever reintroduces a settings
14
+ * boolean whose only semantics is "is this orchestration layer on": any leaf
15
+ * matching `(subagents|ai_team|council)\.(enabled|auto)$`, except the one
16
+ * allowlisted exception.
17
+ *
18
+ * `ai_team.enabled` WAS allowlisted here in an earlier phase (a distinct,
19
+ * still-opt-in feature) and has since joined the deletion in Step 1.3:
20
+ * `/team`'s availability is now resolved from a codex-CLI/auth probe
21
+ * (`src/scripts/ai_team/availability.ts`), matching the doctrine already
22
+ * applied to `subagents.*`. The allowlist below now carries only the one
23
+ * emergency switch; the pattern stays live so a reintroduced
24
+ * `ai_team.enabled`, a future `ai_team.auto`-shaped key, or any
25
+ * `subagents.*` / `council.*` activation flag still fails the build.
26
+ *
27
+ * CLI contract: exit 0 = clean, 1 = a forbidden key exists (or the scan root
28
+ * is dead). `--quiet` suppresses the per-key listing on the clean path.
29
+ */
30
+ import * as fs from 'node:fs';
31
+ import * as os from 'node:os';
32
+ import * as path from 'node:path';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
+
35
+ import { load as parseYaml } from 'js-yaml';
36
+
37
+ import { settingsLeafPaths } from '../shared/settingsClasses.js';
38
+ import { GateLedger } from './_lib/gate_ledger.js';
39
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
40
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
41
+
42
+ const GATE = 'lint_no_activation_gates';
43
+
44
+ const QUIET = process.argv.slice(2).includes('--quiet');
45
+
46
+ const _HERE = path.resolve(fileURLToPath(import.meta.url));
47
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
48
+
49
+ const TEMPLATE_RELATIVE = 'src/config/agent-settings.template.yml';
50
+
51
+ /**
52
+ * The one settings boolean the always-on doctrine keeps — never activation.
53
+ *
54
+ * `emergency.orchestration_halt` is listed here as DOCUMENTATION OF INTENT,
55
+ * not as a live exemption: `ACTIVATION_GATE_RE` below is scoped to
56
+ * `(subagents|ai_team|council).(enabled|auto)`, so this key — wrong prefix
57
+ * (`emergency`, not one of those three) AND wrong suffix
58
+ * (`orchestration_halt`, not `enabled`/`auto`) — never matches it and is
59
+ * filtered out at the `not_applicable_kind` branch in the scan loop below,
60
+ * before the `ALLOWLIST.has(...)` check is ever reached for it (independent
61
+ * review, road-to-always-on-orchestration). Removing this line would change
62
+ * nothing observable; it stays so a reader asking "where is the one
63
+ * surviving switch accounted for in this gate" finds an answer, honestly
64
+ * labelled as inert rather than implied to be doing enforcement work.
65
+ */
66
+ const ALLOWLIST: ReadonlySet<string> = new Set([
67
+ 'emergency.orchestration_halt',
68
+ // `ai_team.enabled` was allowlisted here in an earlier phase (it gated
69
+ // a DIFFERENT, then still-opt-in feature — the cross-model `/team`
70
+ // review family — and its replacement was deferred pending a decision
71
+ // on whether to conflate it with Claude Code's experimental native
72
+ // Agent Teams flag). Step 1.3 resolved that: `/team`'s availability is
73
+ // now a codex-CLI/auth probe (`src/scripts/ai_team/availability.ts`),
74
+ // NOT the native Agent Teams flag and NOT a settings flag at all — so
75
+ // the key is deleted, not allowlisted. Do not re-add it here.
76
+ ]);
77
+
78
+ /** `(subagents|ai_team|council).(enabled|auto)` — the activation-gate shape. */
79
+ const ACTIVATION_GATE_RE = /^(subagents|ai_team|council)\.(enabled|auto)$/;
80
+
81
+ type Yaml = string | number | boolean | null | Yaml[] | { [k: string]: Yaml };
82
+
83
+ function _rootOverride(argv: readonly string[]): string | null {
84
+ const i = argv.indexOf('--root');
85
+ const value = i === -1 ? undefined : argv[i + 1];
86
+ return value === undefined ? null : path.resolve(value);
87
+ }
88
+
89
+ function _readFile(p: string): string | null {
90
+ try {
91
+ return fs.readFileSync(p, 'utf-8');
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Prove, against the real CLI, that this gate still rejects what it must.
99
+ *
100
+ * The accept case is a two-key clean template; each reject case adds exactly
101
+ * one forbidden key, so a pass proves the specific detection and not merely
102
+ * that the fixture is broken.
103
+ */
104
+ export function selfTest(): number {
105
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'lnag-selftest-'));
106
+ const seed = (name: string, template: string): string => {
107
+ const root = path.join(tmp, name);
108
+ fs.mkdirSync(path.join(root, 'src', 'config'), { recursive: true });
109
+ fs.writeFileSync(path.join(root, TEMPLATE_RELATIVE), template, 'utf-8');
110
+ return root;
111
+ };
112
+ const run = (cwd: string): number =>
113
+ runGateCli(REPO_ROOT, `src/scripts/${GATE}.ts`, ['--quiet', '--root', cwd], REPO_ROOT);
114
+
115
+ const clean = 'subagents:\n downshift: true\nemergency:\n orchestration_halt: false\n';
116
+
117
+ try {
118
+ return runSelfTest({
119
+ gate: GATE,
120
+ minCases: 6,
121
+ minRejectCases: 4,
122
+ cases: [
123
+ {
124
+ // "allowlisted" here would overstate it — the emergency
125
+ // switch passes because its shape falls outside
126
+ // `ACTIVATION_GATE_RE` entirely (`not_applicable_kind`),
127
+ // never because `ALLOWLIST.has(...)` fires for it. See
128
+ // the `ALLOWLIST` comment above for why the entry is
129
+ // documentation of intent rather than a live exemption.
130
+ name: 'a clean template with only the (structurally out-of-scope) emergency switch passes',
131
+ expect: 'accept',
132
+ run: () => run(seed('clean', clean)),
133
+ },
134
+ {
135
+ name: 'a leftover ai_team.enabled key is rejected — the allowlist no longer covers it (Step 1.3)',
136
+ expect: 'reject',
137
+ run: () => run(seed('ai-team-enabled', `${clean}ai_team:\n enabled: false\n`)),
138
+ },
139
+ {
140
+ name: 'subagents.enabled is rejected',
141
+ expect: 'reject',
142
+ run: () => run(seed('subagents-enabled', `${clean}subagents:\n enabled: true\n`)),
143
+ },
144
+ {
145
+ name: 'subagents.auto is rejected',
146
+ expect: 'reject',
147
+ run: () => run(seed('subagents-auto', `${clean}subagents:\n auto: "on"\n`)),
148
+ },
149
+ {
150
+ name: 'council.enabled is rejected (future-key protection)',
151
+ expect: 'reject',
152
+ run: () => run(seed('council-enabled', `${clean}council:\n enabled: true\n`)),
153
+ },
154
+ {
155
+ name: 'a NEW ai_team.auto key is rejected — no ai_team.* key is allowlisted anymore',
156
+ expect: 'reject',
157
+ run: () => run(seed('ai-team-auto', `${clean}ai_team:\n auto: "on"\n`)),
158
+ },
159
+ {
160
+ name: 'a missing template — a dead scan root — is rejected, not passed as "nothing to check"',
161
+ expect: 'reject',
162
+ run: () => run(path.join(tmp, 'dead-root-does-not-exist')),
163
+ },
164
+ ],
165
+ });
166
+ } finally {
167
+ fs.rmSync(tmp, { recursive: true, force: true });
168
+ }
169
+ }
170
+
171
+ function main(): number {
172
+ if (process.argv.slice(2).includes('--self-test')) {
173
+ return selfTest();
174
+ }
175
+ const root = _rootOverride(process.argv.slice(2)) ?? REPO_ROOT;
176
+ const templatePath = path.join(root, TEMPLATE_RELATIVE);
177
+
178
+ const templateText = _readFile(templatePath);
179
+ if (templateText === null) {
180
+ process.stderr.write(`❌ ${GATE}: settings template not found at ${TEMPLATE_RELATIVE} — the scan root is dead.\n`);
181
+ return 1;
182
+ }
183
+
184
+ let parsed: Yaml;
185
+ try {
186
+ parsed = parseYaml(templateText) as Yaml;
187
+ } catch (e) {
188
+ process.stderr.write(`❌ ${GATE}: ${TEMPLATE_RELATIVE} did not parse: ${String(e)}\n`);
189
+ return 1;
190
+ }
191
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
192
+ process.stderr.write(`❌ ${GATE}: ${TEMPLATE_RELATIVE} did not parse to a map.\n`);
193
+ return 1;
194
+ }
195
+
196
+ const leaves = settingsLeafPaths(parsed);
197
+ try {
198
+ reportScanned({ gate: GATE, scanned: leaves.length, units: 'settings leaf key(s)', roots: [TEMPLATE_RELATIVE] });
199
+ } catch (e) {
200
+ if (e instanceof DeadScopeError) {
201
+ process.stderr.write(`❌ ${e.message}\n`);
202
+ return 1;
203
+ }
204
+ throw e;
205
+ }
206
+
207
+ const ledger = new GateLedger(GATE);
208
+ ledger.plan(leaves);
209
+
210
+ const findings: string[] = [];
211
+ for (const leaf of leaves) {
212
+ if (!ACTIVATION_GATE_RE.test(leaf)) {
213
+ ledger.outOfScope(leaf, 'not_applicable_kind');
214
+ continue;
215
+ }
216
+ if (ALLOWLIST.has(leaf)) {
217
+ ledger.outOfScope(leaf, 'declared_exemption');
218
+ continue;
219
+ }
220
+ const finding =
221
+ `${TEMPLATE_RELATIVE} \`${leaf}\` is an activation-gate-shaped setting — the ` +
222
+ 'always-on-orchestration doctrine forbids a per-layer on/off switch. Delete it ' +
223
+ 'and resolve the layer from a host-capability probe/registry, or add it to the ' +
224
+ `${GATE} allowlist with a stated reason if it is genuinely not one.`;
225
+ findings.push(finding);
226
+ ledger.fail(leaf, finding);
227
+ }
228
+
229
+ // Unconditional, per `GateLedger.report`'s own contract — `--quiet` mutes
230
+ // the verbose per-key lines this gate prints below, never the ledger's own
231
+ // scanned/planned/skipped summary.
232
+ ledger.report();
233
+
234
+ if (findings.length > 0) {
235
+ for (const f of findings) {
236
+ process.stderr.write(`❌ ${f}\n`);
237
+ }
238
+ return 1;
239
+ }
240
+ if (!QUIET) {
241
+ process.stdout.write(`✅ ${GATE}: no activation-gate-shaped settings key found.\n`);
242
+ }
243
+ return 0;
244
+ }
245
+
246
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
247
+ function _isCliEntry(): boolean {
248
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
249
+ return false;
250
+ }
251
+ if (process.argv[1] === undefined) {
252
+ return false;
253
+ }
254
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
255
+ if (import.meta.url === argvUrl) {
256
+ return true;
257
+ }
258
+ try {
259
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
260
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
261
+ return here === argv;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+
267
+ if (_isCliEntry()) {
268
+ process.exit(main());
269
+ }
270
+
271
+ export { GATE, REPO_ROOT, TEMPLATE_RELATIVE, main };
@@ -22,6 +22,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
23
  import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
24
24
 
25
+ // ledger-exempt: single-artefact prose scan — the whole corpus is the one file README.md, and every hit is a line offset inside it that the failure output already prints in full; there is no enumerated target collection whose members could be silently continued past, and the file-absent / wrong-cwd case is a hard failure via assertWatchlistResolves rather than a green run over zero targets.
25
26
  const _HERE = fileURLToPath(import.meta.url);
26
27
 
27
28
  const README = 'README.md';