@event4u/agent-config 9.28.0 → 9.29.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 (147) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +6 -6
  3. package/CHANGELOG.md +66 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/cost/report.md +2 -2
  7. package/dist/agent-src/commands/team/adversarial.md +8 -7
  8. package/dist/agent-src/commands/team/delegate.md +13 -12
  9. package/dist/agent-src/commands/team/review.md +9 -8
  10. package/dist/agent-src/commands/team/status.md +9 -8
  11. package/dist/agent-src/commands/team.md +29 -19
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  13. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  14. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  15. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
  16. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  17. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  18. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  19. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  20. package/dist/agent-src/rules/delegation-policy.md +11 -7
  21. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  22. package/dist/agent-src/rules/token-budget-discipline.md +1 -1
  23. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  24. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  25. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  26. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  27. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  28. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  29. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  30. package/dist/agent-src/templates/AGENTS.md +4 -4
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/roadmaps.md +23 -0
  33. package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
  34. package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
  35. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  36. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  37. package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
  38. package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
  39. package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
  40. package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
  41. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  42. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  43. package/dist/cli-delegate/cmd_conformance.js +6 -43
  44. package/dist/cli-delegate/cmd_doctor.js +9 -5
  45. package/dist/cli-delegate/cmd_explain.js +2 -2
  46. package/dist/cli-delegate/cmd_export.js +4 -4
  47. package/dist/cli-delegate/cmd_handoff.js +3 -3
  48. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  49. package/dist/cli-delegate/cmd_migrate.js +2 -2
  50. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  51. package/dist/cli-delegate/cmd_prune.js +4 -4
  52. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  53. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_get.js +1 -7
  55. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  56. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  57. package/dist/cli-delegate/cmd_sync.js +4 -4
  58. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  59. package/dist/cli-delegate/cmd_update.js +1 -1
  60. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  61. package/dist/cli-delegate/cmd_validate.js +4 -4
  62. package/dist/cli-delegate/cmd_versions.js +1 -1
  63. package/dist/discovery/deprecation-report.md +1 -1
  64. package/dist/discovery/discovery-manifest.json +69 -20
  65. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  66. package/dist/discovery/discovery-manifest.summary.md +3 -2
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +39 -2
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +12 -4
  71. package/dist/hooks/dispatch.js +373 -35
  72. package/dist/install/install.mjs +42 -17
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +651 -262
  76. package/dist/router.json +1 -1
  77. package/dist/scripts/_lib/agent_settings.js +54 -0
  78. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  79. package/dist/scripts/condense.js +147 -2
  80. package/dist/scripts/condense.js.map +1 -1
  81. package/dist/server/schemas/settings.js +6 -7
  82. package/dist/server/schemas/settings.js.map +1 -1
  83. package/dist/shared/settingsCarveOut.js +0 -9
  84. package/dist/shared/settingsCarveOut.js.map +1 -1
  85. package/dist/shared/settingsClasses.js +11 -9
  86. package/dist/shared/settingsClasses.js.map +1 -1
  87. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  88. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  89. package/dist/ui/index.html +1 -1
  90. package/docs/contracts/settings-classes.md +39 -9
  91. package/package.json +1 -1
  92. package/src/agent-src/templates/AGENTS.md +4 -4
  93. package/src/config/agent-settings.template.yml +52 -66
  94. package/src/config/discovery/packs.yml +11 -0
  95. package/src/config/discovery/workspaces.yml +1 -1
  96. package/src/config/gate-violation-baselines.json +3 -3
  97. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  98. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  99. package/src/scripts/_lib/agent_settings.ts +58 -0
  100. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  101. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  102. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  103. package/src/scripts/_lib/host_capability.ts +63 -6
  104. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  105. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  106. package/src/scripts/_lib/orchestration_record.ts +37 -0
  107. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  108. package/src/scripts/_lib/self_repair.ts +160 -13
  109. package/src/scripts/_lib/self_repair_store.ts +28 -0
  110. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  111. package/src/scripts/_lib/subagent_response.ts +19 -0
  112. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  113. package/src/scripts/_lib/subagent_steering.ts +9 -4
  114. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  115. package/src/scripts/_lib/worker_budget.ts +37 -2
  116. package/src/scripts/ai_council/clients.ts +76 -54
  117. package/src/scripts/ai_council/config.ts +94 -6
  118. package/src/scripts/ai_council/handoff.ts +113 -0
  119. package/src/scripts/ai_council/modes.ts +19 -4
  120. package/src/scripts/ai_council/orchestrator.ts +105 -0
  121. package/src/scripts/ai_council/quorum.ts +84 -0
  122. package/src/scripts/ai_council/session.ts +44 -0
  123. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  124. package/src/scripts/ai_team/availability.ts +110 -0
  125. package/src/scripts/ai_team/config.ts +34 -14
  126. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  127. package/src/scripts/bench_critic_protocol.ts +396 -0
  128. package/src/scripts/check_detector_corpus.ts +202 -0
  129. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  130. package/src/scripts/condense.ts +162 -2
  131. package/src/scripts/council_cli.ts +401 -25
  132. package/src/scripts/forensics_report.ts +333 -0
  133. package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
  134. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  135. package/src/scripts/lint_never_silent.ts +276 -0
  136. package/src/scripts/lint_no_activation_gates.ts +271 -0
  137. package/src/scripts/orchestration_record.ts +8 -0
  138. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  139. package/src/scripts/release.ts +74 -2
  140. package/src/scripts/routing_doctor.ts +21 -36
  141. package/src/scripts/score_skill_selection.ts +19 -5
  142. package/src/scripts/self_repair_cli.ts +287 -57
  143. package/src/scripts/team_events_spike.ts +271 -0
  144. package/src/scripts/team_review_gate_hook.ts +31 -9
  145. package/src/server/schemas/settings.ts +10 -17
  146. package/src/shared/settingsCarveOut.ts +0 -10
  147. package/src/shared/settingsClasses.ts +11 -9
@@ -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 };
@@ -16,6 +16,8 @@
16
16
  * [--route-taken primitive|subagent] [--budget-hit true|false] [--correctness-match true|false] \
17
17
  * [--cache-hit true|false] [--origin lean-init-2026] \
18
18
  * [--rules-carried 32] [--rules-used 5] \
19
+ * [--capsule-emitted true|false] [--capsule-entries 9] \
20
+ * [--watermark-step 8] [--saturation-step 4] [--trigger-arm-earlier watermark|saturation|tie] \
19
21
  * [--dir <audit-dir>] [--dry-run]
20
22
  *
21
23
  * Read by `src/scripts/orchestration_savings_report.ts`.
@@ -39,6 +41,7 @@ import {
39
41
  type LineOutcome,
40
42
  type RecordInput,
41
43
  type RouteTaken,
44
+ type TriggerArm,
42
45
  } from './_lib/orchestration_record.js';
43
46
 
44
47
  // Exported so other writers of the same audit-log-v1 orchestration line
@@ -141,6 +144,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
141
144
  origin: str(flags, 'origin'),
142
145
  rules_carried: int(flags, 'rules-carried'),
143
146
  rules_used: int(flags, 'rules-used'),
147
+ capsule_emitted: bool(flags, 'capsule-emitted'),
148
+ capsule_entries: int(flags, 'capsule-entries'),
149
+ watermark_step: int(flags, 'watermark-step'),
150
+ saturation_step: int(flags, 'saturation-step'),
151
+ trigger_arm_earlier: str(flags, 'trigger-arm-earlier') as TriggerArm | undefined,
144
152
  phase: str(flags, 'phase') as LinePhase | undefined,
145
153
  outcome: str(flags, 'outcome') as LineOutcome | undefined,
146
154
  confidence_band: str(flags, 'confidence-band') as Band | undefined,