@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +18 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/delegation-policy.md +11 -7
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +4 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-NZ5N2XQL.js} +3 -3
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-RCIA67XN.js} +111 -90
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +6 -43
- package/dist/cli-delegate/cmd_doctor.js +9 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -7
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +69 -20
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +373 -35
- package/dist/install/install.mjs +42 -17
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +651 -262
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +39 -9
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_lib/agent_settings.ts +58 -0
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/self_repair.ts +160 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +69 -35
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/orchestration_record.ts +8 -0
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/routing_doctor.ts +21 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +287 -57
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- 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,
|