@event4u/agent-config 9.27.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. 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 };
@@ -25,6 +25,7 @@ import * as fs from 'node:fs';
25
25
  import * as os from 'node:os';
26
26
  import * as path from 'node:path';
27
27
 
28
+ import { comparePair } from './_lib/carrier_divergence.js';
28
29
  import { censusRuleDir } from './preamble_byte_census.js';
29
30
 
30
31
  const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
@@ -91,17 +92,15 @@ function dedupableCount(userDir: string, sourceDir: string): { skipped: number;
91
92
  * perfect version alignment. Classifying the two apart is what turns "0/110"
92
93
  * from a puzzle into an answer: `provenanceOnly == total` means aligning
93
94
  * versions would buy exactly nothing.
95
+ *
96
+ * The three-way comparison now lives in `_lib/carrier_divergence.ts` because a
97
+ * second surface needs it (`report_carrier_divergence`, round-5 Phase 1.3). The
98
+ * DIRECTORY WALK stays here and stays anchored on `sourceDir`: this harness asks
99
+ * how many installed twins the dedup could skip, which is a question about the
100
+ * projection's own rules, not about the union of both carriers. Only the
101
+ * comparison is shared — that is the part that must not drift, and it is also
102
+ * the part `scope_dedup.test.ts` pins.
94
103
  */
95
- const OWNERSHIP_KEYS = ['package', 'source_path'] as const;
96
-
97
- function stripOwnershipKeys(buf: Buffer): string {
98
- return buf
99
- .toString('utf-8')
100
- .split('\n')
101
- .filter((line) => !OWNERSHIP_KEYS.some((k) => line.startsWith(`${k}:`)))
102
- .join('\n');
103
- }
104
-
105
104
  interface ReachabilitySplit {
106
105
  total: number;
107
106
  identical: number;
@@ -130,9 +129,16 @@ function classifyReachability(userDir: string, sourceDir: string): ReachabilityS
130
129
  continue;
131
130
  }
132
131
  const b = fs.readFileSync(path.join(sourceDir, entry));
133
- if (a.equals(b)) split.identical += 1;
134
- else if (stripOwnershipKeys(a) === stripOwnershipKeys(b)) split.provenanceOnly += 1;
135
- else split.bodyDiff += 1;
132
+ switch (comparePair(a, b)) {
133
+ case 'identical':
134
+ split.identical += 1;
135
+ break;
136
+ case 'provenance-only':
137
+ split.provenanceOnly += 1;
138
+ break;
139
+ default:
140
+ split.bodyDiff += 1;
141
+ }
136
142
  }
137
143
  return split;
138
144
  }
@@ -39,6 +39,45 @@ import { atomic_write_json } from "./hooks/state_io.js";
39
39
  import { readHookStdin } from "./hooks/hook_stdin.js";
40
40
 
41
41
  export const SETTINGS_FILE = ".agent-settings.yml";
42
+
43
+ /**
44
+ * Project-relative settings locations, canonical first.
45
+ *
46
+ * The canonical project settings file is `agents/settings/.agent-settings.yml`;
47
+ * the repo-root file is a documented legacy fallback
48
+ * (`agent_settings.ts::project_settings_path`). This hook read ONLY the legacy
49
+ * path, so on a canonically installed consumer the file was always "missing" and
50
+ * the gate resolved to `settings_file_missing` → do not block. An Iron-Law gate
51
+ * that cannot fire on a correct install is not a gate.
52
+ *
53
+ * Inlined rather than imported: this module is deliberately dependency-free so
54
+ * it can run as a hook on every platform. The list is two entries and its source
55
+ * of truth is named above.
56
+ */
57
+ export const SETTINGS_CANDIDATES: readonly string[] = [
58
+ path.join("agents", "settings", SETTINGS_FILE),
59
+ SETTINGS_FILE,
60
+ ];
61
+
62
+ /**
63
+ * First existing settings file under `consumer_root`, canonical before legacy.
64
+ *
65
+ * Falls back to the CANONICAL path when neither exists, so the "missing" branch
66
+ * reports the file a consumer should create rather than the one they should not.
67
+ */
68
+ export function resolve_settings_path(consumer_root: string): string {
69
+ for (const rel of SETTINGS_CANDIDATES) {
70
+ const candidate = path.join(consumer_root, rel);
71
+ try {
72
+ if (fs.statSync(candidate).isFile()) {
73
+ return candidate;
74
+ }
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+ return path.join(consumer_root, SETTINGS_CANDIDATES[0] as string);
80
+ }
42
81
  // NOTE: the Python docstring says `agents/runtime/state/`, but the code
43
82
  // constant is `agents/state/`. Replicated verbatim — this is a latent
44
83
  // docstring/code divergence in the retired Python implementation (ADR-200 § replicate
@@ -141,7 +180,7 @@ export function run(options: {
141
180
  }): number {
142
181
  const { consumer_root } = options;
143
182
  const verbose = options.verbose ?? false;
144
- const settings_path = path.join(consumer_root, SETTINGS_FILE);
183
+ const settings_path = resolve_settings_path(consumer_root);
145
184
  let settings_present = false;
146
185
  try {
147
186
  settings_present = fs.statSync(settings_path).isFile();
@@ -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,9 +41,13 @@ 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
- const DEFAULT_DIR = 'agents/runtime/state/audit';
47
+ // Exported so other writers of the same audit-log-v1 orchestration line
48
+ // (e.g. orchestration_record_hook.ts, the deterministic PostToolUse capture)
49
+ // target the exact same directory instead of duplicating the literal.
50
+ export const DEFAULT_DIR = 'agents/runtime/state/audit';
45
51
 
46
52
  type Flags = Record<string, string | boolean>;
47
53
 
@@ -138,6 +144,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
138
144
  origin: str(flags, 'origin'),
139
145
  rules_carried: int(flags, 'rules-carried'),
140
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,
141
152
  phase: str(flags, 'phase') as LinePhase | undefined,
142
153
  outcome: str(flags, 'outcome') as LineOutcome | undefined,
143
154
  confidence_band: str(flags, 'confidence-band') as Band | undefined,