@event4u/agent-config 9.6.0 → 9.8.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/CHANGELOG.md +207 -942
- package/README.md +21 -21
- package/dist/agent-src/README.md +2 -2
- package/dist/agent-src/commands/commit/in-chunks.md +8 -0
- package/dist/agent-src/commands/commit.md +17 -0
- package/dist/agent-src/commands/pr/create.md +4 -0
- package/dist/agent-src/contexts/override-system.md +75 -5
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/downstream-changes.md +31 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +2 -0
- package/dist/agent-src/rules/language-and-tone.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +3 -0
- package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +3 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/telegraph-speak.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/verify-before-complete.md +2 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
- package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
- package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
- package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
- package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
- package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
- package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
- package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
- package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
- package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
- package/dist/cli/configRoot.js +112 -0
- package/dist/cli/configRoot.js.map +1 -0
- package/dist/cli/configRoot.test.js +97 -0
- package/dist/cli/configRoot.test.js.map +1 -0
- package/dist/cli/help.js +4 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +19 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -47
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +11 -6
- package/dist/install/install.mjs +16 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/server/app.js +10 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/routes/ping.js +18 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/schemas/settings.js +8 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/server/serverInfo.js +6 -2
- package/dist/server/serverInfo.js.map +1 -1
- package/dist/server/token.js +11 -4
- package/dist/server/token.js.map +1 -1
- package/dist/server/writeRoot.js +15 -1
- package/dist/server/writeRoot.js.map +1 -1
- package/dist/shared/capabilities.js +33 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/capabilities.test.js +39 -0
- package/dist/shared/capabilities.test.js.map +1 -0
- package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
- package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
- package/dist/ui/index.html +14 -5
- package/docs/CLAIMS.md +64 -38
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
- package/docs/benchmark.md +170 -9
- package/docs/catalog.md +8 -3
- package/docs/comparison.yaml +44 -2
- package/docs/contracts/local-server-ports.md +53 -0
- package/docs/contracts/no-runtime-boundary.md +1 -1
- package/docs/contracts/subagent-boundary.md +15 -0
- package/docs/customization.md +14 -3
- package/docs/decisions/ADR-016-installer-architecture.md +2 -0
- package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
- package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
- package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
- package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
- package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
- package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
- package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +31 -0
- package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
- package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
- package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
- package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
- package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
- package/docs/decisions/INDEX.md +21 -0
- package/docs/decisions/engine-reclassification-2026-07.md +104 -0
- package/docs/distribution/awesome-list-submission.md +1 -1
- package/docs/distribution/registries.md +36 -27
- package/docs/enforcement-by-host.md +7 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guides/gated-platform-reads.md +221 -0
- package/docs/positioning-evidence.md +1 -1
- package/docs/proof.md +92 -16
- package/docs/spawn-site-policy.md +72 -0
- package/docs/threat-model.md +2 -1
- package/package.json +6 -3
- package/src/agent-src/README.md +2 -2
- package/src/config/agent-settings.template.yml +43 -0
- package/src/config/reach-channels.yml +217 -0
- package/src/config/reach-prescriptions-intake.yml +200 -0
- package/src/scripts/_dispatch.bash +15 -0
- package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
- package/src/scripts/_lib/exec_evidence.ts +251 -0
- package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
- package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
- package/src/scripts/_lib/kernel_rules.ts +46 -0
- package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
- package/src/scripts/_lib/risk_paths.ts +160 -0
- package/src/scripts/_lib/secret_detector.test.ts +103 -0
- package/src/scripts/_lib/secret_detector.ts +319 -0
- package/src/scripts/_lib/spawn_env.ts +24 -4
- package/src/scripts/_lib/tool_probe.ts +359 -0
- package/src/scripts/adr/regenerate_index.ts +81 -6
- package/src/scripts/audit_adr_coverage.ts +33 -0
- package/src/scripts/bench_adversarial_council.ts +292 -0
- package/src/scripts/build_proof.ts +162 -0
- package/src/scripts/check_adr_frontmatter.ts +195 -0
- package/src/scripts/check_artefact_count_messaging.ts +4 -0
- package/src/scripts/check_backstop_debt.ts +200 -0
- package/src/scripts/check_claims.ts +188 -8
- package/src/scripts/check_comparison.ts +12 -0
- package/src/scripts/check_enforcement_coverage.ts +592 -0
- package/src/scripts/check_kernel_rule_bundle.ts +16 -12
- package/src/scripts/check_no_new_legacy_path.ts +2 -0
- package/src/scripts/check_reach_channels.ts +407 -0
- package/src/scripts/check_reach_staleness.ts +364 -0
- package/src/scripts/check_secret_leak.test.ts +47 -0
- package/src/scripts/check_secret_leak.ts +290 -0
- package/src/scripts/check_source_pointer_freshness.ts +165 -0
- package/src/scripts/code_graph/build.ts +0 -0
- package/src/scripts/code_graph/cli.ts +207 -0
- package/src/scripts/code_graph/detect.ts +134 -0
- package/src/scripts/code_graph/extract.ts +386 -0
- package/src/scripts/code_graph/loader.ts +123 -0
- package/src/scripts/code_graph/query.ts +176 -0
- package/src/scripts/code_graph/sanitize.ts +30 -0
- package/src/scripts/code_graph/types.ts +83 -0
- package/src/scripts/code_graph/validate.ts +63 -0
- package/src/scripts/external_sources_denylist.json +3 -1
- package/src/scripts/generate_subagent_floor.ts +194 -0
- package/src/scripts/hook_manifest.yaml +13 -3
- package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
- package/src/scripts/hooks/dispatch_hook.ts +5 -1
- package/src/scripts/iron_law_sha.ts +3 -12
- package/src/scripts/lint_override_kernel_guard.ts +219 -0
- package/src/scripts/lint_readme_serial_comma.ts +169 -0
- package/src/scripts/measure_rule_budget.ts +3 -12
- package/src/scripts/postinstall_gui.ts +129 -0
- package/src/scripts/reach_doctor.ts +1651 -0
- package/src/scripts/release.ts +24 -3
- package/src/scripts/risk_escalation_shadow.ts +160 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/reach-channels.schema.json +134 -0
- package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/skill_linter.ts +55 -0
- package/src/scripts/validate_reach_prescriptions.ts +751 -0
- package/src/server/schemas/settings.ts +16 -0
- package/src/shared/capabilities.test.ts +43 -0
- package/src/shared/capabilities.ts +75 -0
- package/src/templates/minimal/agents-overrides-readme.md +20 -0
- package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse code-graph nudge (ADR-124 Phase 4) — deterministic, warn-only.
|
|
4
|
+
*
|
|
5
|
+
* When a code-graph source is available and the agent is about to search or
|
|
6
|
+
* read source files, surface a one-line reminder to query the graph first.
|
|
7
|
+
* NEVER blocks (dispatcher contract: 0 allow · 2 warn) — Source G's strict
|
|
8
|
+
* block-the-first-read mode is deliberately NOT ported (blocking reads on a
|
|
9
|
+
* possibly-stale index violates the minimal-safe-diff posture). Once per
|
|
10
|
+
* session (a latch keyed by session id), so it cannot nag.
|
|
11
|
+
*
|
|
12
|
+
* Default-OFF. Fires only when `hooks.code_graph.enabled: true`. Disabled /
|
|
13
|
+
* missing / already-latched → no-op exit 0. fail_closed: false — any error
|
|
14
|
+
* returns allow; a token optimisation must never break a tool call.
|
|
15
|
+
*
|
|
16
|
+
* Branches (first eligible search/read this session):
|
|
17
|
+
* present + fresh → "query the graph first"
|
|
18
|
+
* present + stale → "index N commits behind — rebuild"
|
|
19
|
+
* absent → "no graph — build one?"
|
|
20
|
+
*/
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { detectSources, pickSource } from '../code_graph/detect.js';
|
|
26
|
+
|
|
27
|
+
const SETTINGS_FILE = '.agent-settings.yml';
|
|
28
|
+
const EXIT_ALLOW = 0;
|
|
29
|
+
const EXIT_WARN = 2;
|
|
30
|
+
const CODE_EXT = /\.(php|ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
|
|
31
|
+
|
|
32
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
33
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
34
|
+
|
|
35
|
+
function isObject(v: unknown): v is JsonObject {
|
|
36
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `hooks.code_graph.enabled: true` mini-parser (mirrors rtk_wrap_hook). */
|
|
40
|
+
export function enabled(root: string): boolean {
|
|
41
|
+
const f = path.join(root, SETTINGS_FILE);
|
|
42
|
+
let text: string;
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.statSync(f).isFile()) return false;
|
|
45
|
+
text = fs.readFileSync(f, 'utf-8');
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
let inHooks = false;
|
|
50
|
+
let inCg = false;
|
|
51
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
52
|
+
const line = raw.replace(/\s+$/, '');
|
|
53
|
+
if (!line || line.replace(/^\s+/, '').startsWith('#')) continue;
|
|
54
|
+
if (!(line.startsWith(' ') || line.startsWith('\t'))) {
|
|
55
|
+
inHooks = /^hooks\s*:\s*$/.test(line);
|
|
56
|
+
inCg = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inHooks) {
|
|
60
|
+
if (/^\s+code_graph\s*:\s*$/.test(line)) {
|
|
61
|
+
inCg = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (inCg && /^\s{0,3}\S/.test(line)) inCg = false;
|
|
65
|
+
}
|
|
66
|
+
if (inCg && /^\s+enabled\s*:\s*true\b/.test(line)) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ToolIntent {
|
|
72
|
+
isSearch: boolean;
|
|
73
|
+
isCodeRead: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Best-effort read of the intercepted tool + its target from the envelope. */
|
|
77
|
+
export function classifyTool(envelope: JsonObject): ToolIntent {
|
|
78
|
+
const payload = isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
79
|
+
const nameVal =
|
|
80
|
+
payload['tool_name'] ?? payload['toolName'] ?? payload['tool'] ?? envelope['tool_name'] ?? envelope['tool'];
|
|
81
|
+
const name = typeof nameVal === 'string' ? nameVal : '';
|
|
82
|
+
const ti = (isObject(payload['tool_input']) ? payload['tool_input'] : envelope['tool_input']) as
|
|
83
|
+
| JsonObject
|
|
84
|
+
| undefined;
|
|
85
|
+
const isSearch = name === 'Grep' || name === 'Glob';
|
|
86
|
+
let isCodeRead = false;
|
|
87
|
+
if (name === 'Read' && isObject(ti)) {
|
|
88
|
+
const fp = ti['file_path'] ?? ti['path'];
|
|
89
|
+
if (typeof fp === 'string') isCodeRead = CODE_EXT.test(fp);
|
|
90
|
+
}
|
|
91
|
+
return { isSearch, isCodeRead };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sessionId(envelope: JsonObject): string {
|
|
95
|
+
const s = envelope['session_id'] ?? envelope['sessionId'];
|
|
96
|
+
return typeof s === 'string' && s ? s : 'default';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function latchFile(root: string): string {
|
|
100
|
+
return path.join(root, 'agents', 'runtime', 'state', 'code-graph-nudge.json');
|
|
101
|
+
}
|
|
102
|
+
function alreadyNudged(root: string, session: string): boolean {
|
|
103
|
+
try {
|
|
104
|
+
const state = JSON.parse(fs.readFileSync(latchFile(root), 'utf-8')) as Record<string, boolean>;
|
|
105
|
+
return state[session] === true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function latch(root: string, session: string): void {
|
|
111
|
+
try {
|
|
112
|
+
const p = latchFile(root);
|
|
113
|
+
let state: Record<string, boolean> = {};
|
|
114
|
+
try {
|
|
115
|
+
state = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, boolean>;
|
|
116
|
+
} catch {
|
|
117
|
+
/* fresh */
|
|
118
|
+
}
|
|
119
|
+
state[session] = true;
|
|
120
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
121
|
+
fs.writeFileSync(p, JSON.stringify(state));
|
|
122
|
+
} catch {
|
|
123
|
+
/* fail-open: a persistence failure must not break the tool call */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const NATIVE_CACHE = path.join('agents', 'runtime', 'state', 'code-graph-v1.json');
|
|
128
|
+
|
|
129
|
+
/** The nudge line for the current source state (≤40 tokens, one line). */
|
|
130
|
+
export function nudgeReason(root: string): string {
|
|
131
|
+
const verdicts = detectSources(root, path.join(root, NATIVE_CACHE));
|
|
132
|
+
const picked = pickSource(verdicts);
|
|
133
|
+
if (!picked) {
|
|
134
|
+
return 'No code-graph found. For "who calls / where used / impact" run `code_graph build` once, then `code_graph query <symbol>` instead of grepping blind.';
|
|
135
|
+
}
|
|
136
|
+
if (picked.stale) {
|
|
137
|
+
return `code-graph is ${picked.commits_behind ?? 'N'} commit(s) behind — rebuild with \`code_graph build --update\` before trusting relationship answers.`;
|
|
138
|
+
}
|
|
139
|
+
return 'A fresh code-graph is present — for structure questions run `code_graph query|affected <symbol>` first; grep is the fallback (say which answered).';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function main(): number {
|
|
143
|
+
let envelope: JsonValue;
|
|
144
|
+
try {
|
|
145
|
+
const raw = process.stdin.isTTY ? '' : fs.readFileSync(0, 'utf-8');
|
|
146
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
147
|
+
} catch {
|
|
148
|
+
return EXIT_ALLOW;
|
|
149
|
+
}
|
|
150
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
151
|
+
|
|
152
|
+
const cwd = envelope['cwd'];
|
|
153
|
+
const pr = envelope['workspace_root'] ?? envelope['project_root'];
|
|
154
|
+
const root = typeof cwd === 'string' && cwd ? cwd : typeof pr === 'string' && pr ? pr : '.';
|
|
155
|
+
if (!enabled(root)) return EXIT_ALLOW;
|
|
156
|
+
|
|
157
|
+
const { isSearch, isCodeRead } = classifyTool(envelope);
|
|
158
|
+
if (!isSearch && !isCodeRead) return EXIT_ALLOW;
|
|
159
|
+
|
|
160
|
+
const session = sessionId(envelope);
|
|
161
|
+
if (alreadyNudged(root, session)) return EXIT_ALLOW; // once per session
|
|
162
|
+
|
|
163
|
+
let reason: string;
|
|
164
|
+
try {
|
|
165
|
+
reason = nudgeReason(root);
|
|
166
|
+
} catch {
|
|
167
|
+
return EXIT_ALLOW;
|
|
168
|
+
}
|
|
169
|
+
latch(root, session);
|
|
170
|
+
process.stdout.write(`${JSON.stringify({ decision: 'warn', reason })}\n`);
|
|
171
|
+
return EXIT_WARN;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isCliEntry(): boolean {
|
|
175
|
+
if (process.argv[1] === undefined) return false;
|
|
176
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
177
|
+
if (import.meta.url === argvUrl) return true;
|
|
178
|
+
try {
|
|
179
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (isCliEntry()) process.exit(main());
|
|
@@ -35,6 +35,7 @@ import path from "node:path";
|
|
|
35
35
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
36
36
|
import { parse as parseYaml } from "yaml";
|
|
37
37
|
|
|
38
|
+
import { hardenedSpawnEnv } from "../_lib/spawn_env.js";
|
|
38
39
|
import {
|
|
39
40
|
atomic_write_json,
|
|
40
41
|
feedback_dir,
|
|
@@ -500,7 +501,10 @@ function _run_concern(concern: ConcernDef, envelope: JsonObject): RunResult {
|
|
|
500
501
|
// Pass the package root so concerns can locate package-shipped
|
|
501
502
|
// distributed content (ADR-020 global-only consumers carry no
|
|
502
503
|
// project-local copy). REPO_ROOT is the dispatcher's own resolved root.
|
|
503
|
-
|
|
504
|
+
// Consumer-runtime concern dispatch: scrub code-execution-injection env
|
|
505
|
+
// vectors (loader / git-config / NODE_OPTIONS / …) before spawning each
|
|
506
|
+
// concern, then set the package-root marker as an explicit override.
|
|
507
|
+
const concern_env = hardenedSpawnEnv({ AGENT_CONFIG_PACKAGE_ROOT: REPO_ROOT });
|
|
504
508
|
|
|
505
509
|
const started = performance.now();
|
|
506
510
|
const { command, args: spawnArgs } = _resolve_tsx_invocation(script, cmd.slice(1));
|
|
@@ -26,6 +26,7 @@ import * as path from 'node:path';
|
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
27
|
|
|
28
28
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
29
|
+
import { KERNEL_RULE_IDS } from './_lib/kernel_rules.js';
|
|
29
30
|
|
|
30
31
|
const _HERE = fileURLToPath(import.meta.url);
|
|
31
32
|
|
|
@@ -60,18 +61,8 @@ function _isFile(p: string): boolean {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
// Locked kernel set —
|
|
64
|
-
const KERNEL_RULES: readonly string[] =
|
|
65
|
-
'agent-authority',
|
|
66
|
-
'ask-when-uncertain',
|
|
67
|
-
'commit-policy',
|
|
68
|
-
'direct-answers',
|
|
69
|
-
'language-and-tone',
|
|
70
|
-
'no-cheap-questions',
|
|
71
|
-
'non-destructive-by-default',
|
|
72
|
-
'scope-control',
|
|
73
|
-
'verify-before-complete',
|
|
74
|
-
];
|
|
64
|
+
// Locked kernel set — single-sourced in _lib/kernel_rules.ts.
|
|
65
|
+
const KERNEL_RULES: readonly string[] = KERNEL_RULE_IDS;
|
|
75
66
|
|
|
76
67
|
// Python: re.compile(r"```(?:[^\n]*\n)([\s\S]*?)```")
|
|
77
68
|
const _FENCE_RE = /```(?:[^\n]*\n)([\s\S]*?)```/g;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Override audit — kernel-rule overrides, reported rather than policed.
|
|
4
|
+
*
|
|
5
|
+
* The obvious design for this file was a build-failing lint on any kernel-named
|
|
6
|
+
* file under `agents/overrides/rules/`. The council talked us out of it, and the
|
|
7
|
+
* argument holds: the override layer is resolved by the *agent reading the
|
|
8
|
+
* instructions*, not by a loader, so a consumer who wants a kernel rule relaxed
|
|
9
|
+
* has other routes — a persona file, host-level agent config, a line in the
|
|
10
|
+
* prompt. Failing the build on route one does not shrink that surface; it moves
|
|
11
|
+
* the override to a route with no visibility at all, while the red X reads as
|
|
12
|
+
* coverage. That is the exact "claims enforcement it does not have" defect this
|
|
13
|
+
* whole roadmap exists to remove, pointed at ourselves.
|
|
14
|
+
*
|
|
15
|
+
* So the default output is a REPORT. A consumer reading "0 kernel overrides" or
|
|
16
|
+
* "2 kernel overrides, both registered" learns something true.
|
|
17
|
+
*
|
|
18
|
+
* Two things do hard-fail, because on our own authoring surface the coverage
|
|
19
|
+
* claim IS true:
|
|
20
|
+
* 1. a `Mode: replace` override on a kernel / safety-floor rule — the class is
|
|
21
|
+
* non-replaceable, and this is mechanically decidable from the header;
|
|
22
|
+
* 2. an `extend` on one with no entry in `agents/overrides/kernel-exceptions.yml`
|
|
23
|
+
* — tightening is allowed, going unrecorded is not.
|
|
24
|
+
*
|
|
25
|
+
* Honest limit, stated in-band: an `extend` block whose prose says "ignore
|
|
26
|
+
* everything above" passes this check. No linter reads intent. The registry
|
|
27
|
+
* entry is where a human records it, and the report is what makes it reviewable.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* ./scripts-run src/scripts/lint_override_kernel_guard # report
|
|
31
|
+
* ./scripts-run src/scripts/lint_override_kernel_guard --json
|
|
32
|
+
* ./scripts-run src/scripts/lint_override_kernel_guard --strict # + hard-fails
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 clean · 1 violation (--strict) · 2 usage/env error.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import { KERNEL_RULE_IDS, is_kernel_rule } from './_lib/kernel_rules.js';
|
|
42
|
+
|
|
43
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
45
|
+
const OVERRIDES_RULES = path.join(REPO_ROOT, 'agents', 'overrides', 'rules');
|
|
46
|
+
const REGISTRY = path.join(REPO_ROOT, 'agents', 'overrides', 'kernel-exceptions.yml');
|
|
47
|
+
const RULES_DIR = path.join(REPO_ROOT, 'src', 'rules');
|
|
48
|
+
|
|
49
|
+
export interface OverrideRow {
|
|
50
|
+
rule: string;
|
|
51
|
+
file: string;
|
|
52
|
+
mode: 'extend' | 'replace' | 'unknown';
|
|
53
|
+
kernel: boolean;
|
|
54
|
+
safety_floor: boolean;
|
|
55
|
+
registered: boolean;
|
|
56
|
+
cited: boolean;
|
|
57
|
+
violations: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `**Mode:** \`extend\`` — the header form the override contract specifies. */
|
|
61
|
+
export function parse_mode(text: string): 'extend' | 'replace' | 'unknown' {
|
|
62
|
+
const m = /^\s*\*\*Mode:\*\*\s*`?(extend|replace)`?\s*$/im.exec(text);
|
|
63
|
+
return m ? (m[1] as 'extend' | 'replace') : 'unknown';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `> Overrides: <rule> §<section> — <reason>` — the citation obligation. */
|
|
67
|
+
export function has_citation(text: string): boolean {
|
|
68
|
+
return /^>\s*Overrides:\s*\S+.*—.+$/im.test(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Rule ids carrying `tier: safety-floor`, read from the rule sources. */
|
|
72
|
+
export function safety_floor_ids(rules_dir: string): Set<string> {
|
|
73
|
+
const out = new Set<string>();
|
|
74
|
+
if (!fs.existsSync(rules_dir)) return out;
|
|
75
|
+
for (const name of fs.readdirSync(rules_dir)) {
|
|
76
|
+
if (!name.endsWith('.md')) continue;
|
|
77
|
+
const head = fs.readFileSync(path.join(rules_dir, name), 'utf-8').slice(0, 2000);
|
|
78
|
+
if (/^tier:\s*"?safety-floor"?\s*$/im.test(head)) out.add(name.replace(/\.md$/, ''));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Rule ids with a registered exception.
|
|
85
|
+
*
|
|
86
|
+
* Narrow reader, not a YAML dependency: the registry is a fixed two-level shape
|
|
87
|
+
* and the only fact needed is which rules appear under `exceptions:`. Anything
|
|
88
|
+
* more permissive would let a malformed registry read as coverage.
|
|
89
|
+
*/
|
|
90
|
+
export function registered_rules(text: string): Set<string> {
|
|
91
|
+
const out = new Set<string>();
|
|
92
|
+
let in_exceptions = false;
|
|
93
|
+
for (const raw of text.split('\n')) {
|
|
94
|
+
const line = raw.replace(/\s+$/, '');
|
|
95
|
+
if (/^exceptions:\s*$/.test(line)) { in_exceptions = true; continue; }
|
|
96
|
+
if (!in_exceptions) continue;
|
|
97
|
+
if (/^\S/.test(line)) break;
|
|
98
|
+
const rule_name = /^\s*-?\s*rule:\s*"?([a-z0-9-]+)"?\s*$/i.exec(line)?.[1];
|
|
99
|
+
if (rule_name !== undefined) out.add(rule_name);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function audit(): OverrideRow[] {
|
|
105
|
+
if (!fs.existsSync(OVERRIDES_RULES)) return [];
|
|
106
|
+
const floor = safety_floor_ids(RULES_DIR);
|
|
107
|
+
const registered = fs.existsSync(REGISTRY)
|
|
108
|
+
? registered_rules(fs.readFileSync(REGISTRY, 'utf-8'))
|
|
109
|
+
: new Set<string>();
|
|
110
|
+
|
|
111
|
+
const rows: OverrideRow[] = [];
|
|
112
|
+
for (const name of fs.readdirSync(OVERRIDES_RULES).sort()) {
|
|
113
|
+
if (!name.endsWith('.md')) continue;
|
|
114
|
+
const rule = name.replace(/\.md$/, '');
|
|
115
|
+
const kernel = is_kernel_rule(rule);
|
|
116
|
+
const is_floor = floor.has(rule);
|
|
117
|
+
if (!kernel && !is_floor) continue; // ordinary override — not this gate's business
|
|
118
|
+
|
|
119
|
+
const text = fs.readFileSync(path.join(OVERRIDES_RULES, name), 'utf-8');
|
|
120
|
+
const mode = parse_mode(text);
|
|
121
|
+
const cited = has_citation(text);
|
|
122
|
+
const is_registered = registered.has(rule);
|
|
123
|
+
|
|
124
|
+
const violations: string[] = [];
|
|
125
|
+
if (mode === 'replace') {
|
|
126
|
+
violations.push(
|
|
127
|
+
`\`replace\` on a ${kernel ? 'kernel' : 'safety-floor'} rule — this class may be tightened, never replaced`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (mode === 'unknown') {
|
|
131
|
+
violations.push(
|
|
132
|
+
'no readable `**Mode:**` header — an override on this class must declare its mode',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (mode === 'extend' && !is_registered) {
|
|
136
|
+
violations.push(
|
|
137
|
+
`\`extend\` on a ${kernel ? 'kernel' : 'safety-floor'} rule with no entry in agents/overrides/kernel-exceptions.yml`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!cited) {
|
|
141
|
+
violations.push('missing `> Overrides: <rule> §<section> — <reason>` citation');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rows.push({
|
|
145
|
+
rule,
|
|
146
|
+
file: path.relative(REPO_ROOT, path.join(OVERRIDES_RULES, name)),
|
|
147
|
+
mode,
|
|
148
|
+
kernel,
|
|
149
|
+
safety_floor: is_floor,
|
|
150
|
+
registered: is_registered,
|
|
151
|
+
cited,
|
|
152
|
+
violations,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function main(argv: string[]): number {
|
|
159
|
+
const as_json = argv.includes('--json');
|
|
160
|
+
const strict = argv.includes('--strict');
|
|
161
|
+
const rows = audit();
|
|
162
|
+
|
|
163
|
+
if (as_json) {
|
|
164
|
+
process.stdout.write(JSON.stringify({ kernel_overrides: rows }, null, 2) + '\n');
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const bad = rows.filter((r) => r.violations.length > 0);
|
|
169
|
+
|
|
170
|
+
if (rows.length === 0) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
`✅ override audit: 0 kernel / safety-floor overrides present ` +
|
|
173
|
+
`(${KERNEL_RULE_IDS.length} kernel rules, none overridden)\n`,
|
|
174
|
+
);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lines: string[] = [];
|
|
179
|
+
lines.push(
|
|
180
|
+
`override audit: ${rows.length} kernel / safety-floor override(s) · ` +
|
|
181
|
+
`${rows.filter((r) => r.registered).length} registered · ${bad.length} with findings`,
|
|
182
|
+
);
|
|
183
|
+
for (const r of rows) {
|
|
184
|
+
const tag = r.kernel ? 'kernel' : 'safety-floor';
|
|
185
|
+
lines.push(` · ${r.rule} [${tag}] mode=${r.mode} registered=${r.registered ? 'yes' : 'NO'}`);
|
|
186
|
+
for (const v of r.violations) lines.push(` ❌ ${v}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(
|
|
190
|
+
' Limit: this audits files under agents/overrides/. A rule can also be ' +
|
|
191
|
+
'relaxed through a persona, host config, or a direct instruction — ' +
|
|
192
|
+
'those routes are not visible here and are not claimed to be covered.',
|
|
193
|
+
);
|
|
194
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
195
|
+
|
|
196
|
+
if (strict && bad.length > 0) {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`❌ lint_override_kernel_guard: ${bad.length} override(s) violate the non-overridable class\n`,
|
|
199
|
+
);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Main-guard (realpath-compared, mirrors the repo convention).
|
|
206
|
+
if (process.argv[1] !== undefined) {
|
|
207
|
+
try {
|
|
208
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
209
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
210
|
+
if (here === argv1) {
|
|
211
|
+
process.exit(main(process.argv.slice(2)));
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
215
|
+
if (import.meta.url === argvUrl) {
|
|
216
|
+
process.exit(main(process.argv.slice(2)));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CI guard for the no-serial-comma house style in README.md prose.
|
|
4
|
+
*
|
|
5
|
+
* House style: prose never puts a comma directly before a final "and" / "or"
|
|
6
|
+
* (no Oxford / serial comma). `skills, commands and rules` — NOT
|
|
7
|
+
* `skills, commands, and rules`. README.md is the public claim surface for a
|
|
8
|
+
* package whose whole pitch is "every headline claim is machine-checked", so a
|
|
9
|
+
* drifting comma style in the copy is the same class of defect as a drifting
|
|
10
|
+
* number: this guard turns a one-off copy fix into a permanent build gate.
|
|
11
|
+
*
|
|
12
|
+
* Prose only. Fenced code, inline `backtick` spans, HTML comments (claim
|
|
13
|
+
* markers), URLs, and Markdown table rows are masked before matching so the
|
|
14
|
+
* guard never trips on code, data, or link syntax.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes: 0 clean · 1 violation found / README missing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
24
|
+
|
|
25
|
+
const README = 'README.md';
|
|
26
|
+
|
|
27
|
+
// A comma, whitespace, then a final coordinating conjunction as a whole word.
|
|
28
|
+
const SERIAL_COMMA = /,[ \t]+(and|or)\b/gi;
|
|
29
|
+
|
|
30
|
+
/** Python str.splitlines() — drops the trailing empty element. */
|
|
31
|
+
function _splitlines(text: string): string[] {
|
|
32
|
+
if (text === '') {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const normalised = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
36
|
+
const parts = normalised.split('\n');
|
|
37
|
+
if (parts[parts.length - 1] === '') {
|
|
38
|
+
parts.pop();
|
|
39
|
+
}
|
|
40
|
+
return parts;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return per-line content with everything that is not prose masked out.
|
|
45
|
+
*
|
|
46
|
+
* Order matters: HTML comments and fenced regions blank the whole line; on a
|
|
47
|
+
* surviving prose line we then blank inline backtick spans, URLs, and — for a
|
|
48
|
+
* Markdown table row — the whole line, so a `, and` inside code, a link, or a
|
|
49
|
+
* data cell never counts as a style violation.
|
|
50
|
+
*/
|
|
51
|
+
function _stripNoise(lines: string[]): string[] {
|
|
52
|
+
const urlRe = /https?:\/\/\S+|\(\.[\w./-]+\)/g;
|
|
53
|
+
const backtickRe = /`[^`]*`/g;
|
|
54
|
+
const cleaned: string[] = [];
|
|
55
|
+
let inFence = false;
|
|
56
|
+
let inHtml = false;
|
|
57
|
+
for (const raw of lines) {
|
|
58
|
+
const line = raw;
|
|
59
|
+
if (line.includes('<!--') && !line.includes('-->')) {
|
|
60
|
+
inHtml = true;
|
|
61
|
+
}
|
|
62
|
+
if (inHtml) {
|
|
63
|
+
cleaned.push('');
|
|
64
|
+
if (line.includes('-->')) {
|
|
65
|
+
inHtml = false;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const stripped = line.trim();
|
|
70
|
+
if (stripped.startsWith('```')) {
|
|
71
|
+
inFence = !inFence;
|
|
72
|
+
cleaned.push('');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (inFence) {
|
|
76
|
+
cleaned.push('');
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Single-line HTML comment (e.g. a claim marker) — blank it.
|
|
80
|
+
let content = line.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
81
|
+
// Markdown table row — data, not prose.
|
|
82
|
+
if (content.trim().startsWith('|')) {
|
|
83
|
+
cleaned.push('');
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Mask code spans / URLs with a word-char placeholder (not a space):
|
|
87
|
+
// a space would let a `code`-span sitting between a list comma and the
|
|
88
|
+
// final "and" / "or" collapse into a phantom ", or" (the first list
|
|
89
|
+
// comma is legitimate — only a comma *directly* before and/or counts).
|
|
90
|
+
content = content.replace(backtickRe, 'x').replace(urlRe, 'x');
|
|
91
|
+
cleaned.push(content);
|
|
92
|
+
}
|
|
93
|
+
return cleaned;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface Hit {
|
|
97
|
+
line: number;
|
|
98
|
+
match: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collect_hits(text: string): Hit[] {
|
|
102
|
+
const lines = _stripNoise(_splitlines(text));
|
|
103
|
+
const hits: Hit[] = [];
|
|
104
|
+
lines.forEach((content, idx) => {
|
|
105
|
+
SERIAL_COMMA.lastIndex = 0;
|
|
106
|
+
let m: RegExpExecArray | null;
|
|
107
|
+
while ((m = SERIAL_COMMA.exec(content)) !== null) {
|
|
108
|
+
hits.push({ line: idx + 1, match: m[0].trim() });
|
|
109
|
+
if (m.index === SERIAL_COMMA.lastIndex) {
|
|
110
|
+
SERIAL_COMMA.lastIndex += 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return hits;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function main(): number {
|
|
118
|
+
const quiet = process.argv.slice(2).includes('--quiet');
|
|
119
|
+
if (!fs.existsSync(README)) {
|
|
120
|
+
process.stderr.write(`error: ${README} not found\n`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hits = collect_hits(fs.readFileSync(README, 'utf-8'));
|
|
125
|
+
|
|
126
|
+
if (hits.length > 0) {
|
|
127
|
+
process.stdout.write(
|
|
128
|
+
`FAIL ${README}: ${hits.length} serial-comma violation(s) in prose ` +
|
|
129
|
+
`(house style: no comma before a final "and" / "or").\n`,
|
|
130
|
+
);
|
|
131
|
+
for (const { line, match } of hits) {
|
|
132
|
+
const lineStr = String(line).padStart(4, ' ');
|
|
133
|
+
process.stdout.write(` L${lineStr} -> "${match}"\n`);
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write('\nFix: drop the comma before the final "and" / "or".\n');
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!quiet) {
|
|
140
|
+
process.stdout.write(`OK ${README}: no serial-comma violations in prose.\n`);
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _isCliEntry(): boolean {
|
|
146
|
+
if (process.argv[1] === undefined) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
150
|
+
if (import.meta.url === argvUrl) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// A symlinked invocation (installed `.augment/` projection, or macOS
|
|
154
|
+
// /var → /private/var temp dirs) makes the raw URLs differ. Compare
|
|
155
|
+
// realpaths so the entry guard still fires under a symlink.
|
|
156
|
+
try {
|
|
157
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
158
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
159
|
+
return here === argv;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
166
|
+
process.exit(main());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { README, SERIAL_COMMA, collect_hits, main };
|
|
@@ -29,6 +29,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
29
29
|
|
|
30
30
|
import { artefact_roots } from './_lib/agent_src.js';
|
|
31
31
|
import * as token_count from './_lib/token_count.js';
|
|
32
|
+
import { KERNEL_RULE_ID_SET } from './_lib/kernel_rules.js';
|
|
32
33
|
|
|
33
34
|
const _HERE = fileURLToPath(import.meta.url);
|
|
34
35
|
// src/scripts/measure_rule_budget.ts → parents[2] is the repo root (mirrors
|
|
@@ -47,18 +48,8 @@ const PER_RULE_HARD = 2_500;
|
|
|
47
48
|
const PER_RULE_TARGET = 2_000;
|
|
48
49
|
const PER_RULE_OVERRIDE_CEILING = 4_000; // Iron-Law-override ADR ceiling.
|
|
49
50
|
|
|
50
|
-
// Locked kernel set — docs/contracts/kernel-membership.md § 4.
|
|
51
|
-
const KERNEL_RULES: ReadonlySet<string> =
|
|
52
|
-
'agent-authority',
|
|
53
|
-
'ask-when-uncertain',
|
|
54
|
-
'commit-policy',
|
|
55
|
-
'direct-answers',
|
|
56
|
-
'language-and-tone',
|
|
57
|
-
'no-cheap-questions',
|
|
58
|
-
'non-destructive-by-default',
|
|
59
|
-
'scope-control',
|
|
60
|
-
'verify-before-complete',
|
|
61
|
-
]);
|
|
51
|
+
// Locked kernel set — single-sourced in _lib/kernel_rules.ts (contract: docs/contracts/kernel-membership.md § 4).
|
|
52
|
+
const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_ID_SET;
|
|
62
53
|
|
|
63
54
|
export interface RuleMeasure {
|
|
64
55
|
id: string;
|