@event4u/agent-config 14.13.0 → 14.14.1
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Agent-artifact location guard — a roadmap-shaped file outside the roadmap root.
|
|
4
|
+
*
|
|
5
|
+
* WHAT IT PROTECTS, AND WHAT IT DOES NOT.
|
|
6
|
+
* ---------------------------------------
|
|
7
|
+
* This gate protects THIS repository, and only this one. A consumer repository
|
|
8
|
+
* never runs this repository's CI, so a gate scoped here protects the tree
|
|
9
|
+
* where the error is LEAST likely to occur — the observed failure was in a
|
|
10
|
+
* consumer project. That is not a reason to skip it, since the same mistake is
|
|
11
|
+
* possible here, but it IS a reason to write the limit down: a gate whose reach
|
|
12
|
+
* is not stated gets cited as broader than it is.
|
|
13
|
+
*
|
|
14
|
+
* The outward-facing half is `agent-config doctor --strict`, which already
|
|
15
|
+
* knows the consumer shape and can now return a non-zero exit.
|
|
16
|
+
*
|
|
17
|
+
* The constraint the detection rests on: every other mechanism that governs a
|
|
18
|
+
* roadmap is keyed on the roadmap root, so a file placed outside it is governed
|
|
19
|
+
* by nothing. This gate is the one that is keyed on the complement.
|
|
20
|
+
*
|
|
21
|
+
* WHY THE NAME TEST ALONE IS NOT THE DETECTOR.
|
|
22
|
+
* -------------------------------------------
|
|
23
|
+
* `is_roadmap_candidate` is IMPORTED from `update_roadmap_progress` rather than
|
|
24
|
+
* mirrored locally, the way `check_estate_count.ts` does it — one definition of
|
|
25
|
+
* what counts, so the roadmap corpus and this guard cannot drift apart.
|
|
26
|
+
*
|
|
27
|
+
* But that function answers "is this filename eligible to be a roadmap inside
|
|
28
|
+
* the roadmap root", and its answer over an arbitrary tree is YES for nearly
|
|
29
|
+
* every `.md` file: it excludes four names and one prefix and otherwise accepts
|
|
30
|
+
* anything. Run bare over this repository it would report thousands of
|
|
31
|
+
* legitimate documents. So the name test is a PRE-FILTER, and the decision is
|
|
32
|
+
* made by {@link roadmapShape}, which reads the file: roadmap frontmatter plus a
|
|
33
|
+
* `## Phase` heading plus at least one checkbox step. All three, because each
|
|
34
|
+
* alone is common — plenty of documents carry frontmatter, and a design doc can
|
|
35
|
+
* carry checkboxes.
|
|
36
|
+
*
|
|
37
|
+
* The finding NAMES the signals that fired, so a false positive is arguable
|
|
38
|
+
* rather than mysterious. A document that trips this gate and should not is a
|
|
39
|
+
* conversation about the signals, which is the conversation worth having.
|
|
40
|
+
*
|
|
41
|
+
* Exit codes: 0 = clean · 1 = a roadmap-shaped file sits outside the root · 3 = internal error.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as fs from 'node:fs';
|
|
45
|
+
import * as os from 'node:os';
|
|
46
|
+
import * as path from 'node:path';
|
|
47
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
48
|
+
|
|
49
|
+
import { is_roadmap_candidate as isRoadmapCandidate } from '../agent-src/scripts/update_roadmap_progress.js';
|
|
50
|
+
import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
51
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
52
|
+
|
|
53
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
54
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
55
|
+
|
|
56
|
+
/** The one legitimate home. Everything else is a finding. */
|
|
57
|
+
export const ROADMAP_ROOT_REL = 'agents/roadmaps';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Directories the walk never enters.
|
|
61
|
+
*
|
|
62
|
+
* `dist/` and the per-tool projection trees are generated copies — a finding
|
|
63
|
+
* there is a finding about `src/`, reported twice. `node_modules` and `.git`
|
|
64
|
+
* are not this repository's prose.
|
|
65
|
+
*/
|
|
66
|
+
const SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
67
|
+
'.git',
|
|
68
|
+
'node_modules',
|
|
69
|
+
'dist',
|
|
70
|
+
'.augment',
|
|
71
|
+
'.claude',
|
|
72
|
+
'.cursor',
|
|
73
|
+
'.clinerules',
|
|
74
|
+
'.agents',
|
|
75
|
+
'coverage',
|
|
76
|
+
'.venv',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/** Frontmatter keys that only a roadmap carries in this tree. */
|
|
80
|
+
const FM_ROADMAP_RE = /^complexity:[ \t]*(lightweight|structural)[ \t]*$/m;
|
|
81
|
+
const FM_RE = /^---\n([\s\S]*?)\n---/;
|
|
82
|
+
const PHASE_RE = /^##[ \t]+Phase[ \t]/m;
|
|
83
|
+
const CHECKBOX_RE = /^[ \t]*- \[[ x~-]\][ \t]/m;
|
|
84
|
+
|
|
85
|
+
export interface Shape {
|
|
86
|
+
/** Which signals fired. Empty means not roadmap-shaped. */
|
|
87
|
+
readonly signals: readonly string[];
|
|
88
|
+
readonly isRoadmap: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether a file's CONTENT is a roadmap.
|
|
93
|
+
*
|
|
94
|
+
* All three signals are required. Each is individually common; together they are
|
|
95
|
+
* this repository's roadmap format and nothing else's.
|
|
96
|
+
*/
|
|
97
|
+
export function roadmapShape(text: string): Shape {
|
|
98
|
+
const signals: string[] = [];
|
|
99
|
+
const fm = FM_RE.exec(text);
|
|
100
|
+
if (fm !== null && FM_ROADMAP_RE.test(fm[1] as string)) {
|
|
101
|
+
signals.push('roadmap frontmatter (`complexity:` lightweight|structural)');
|
|
102
|
+
}
|
|
103
|
+
if (PHASE_RE.test(text)) {
|
|
104
|
+
signals.push('a `## Phase` heading');
|
|
105
|
+
}
|
|
106
|
+
if (CHECKBOX_RE.test(text)) {
|
|
107
|
+
signals.push('at least one checkbox step');
|
|
108
|
+
}
|
|
109
|
+
return { signals, isRoadmap: signals.length === 3 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Finding {
|
|
113
|
+
readonly rel: string;
|
|
114
|
+
readonly signals: readonly string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ScanResult {
|
|
118
|
+
readonly findings: readonly Finding[];
|
|
119
|
+
/** Every `.md` file the walk visited — the scope, not the matches. */
|
|
120
|
+
readonly scanned: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function walkMarkdown(root: string): string[] {
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
const stack = [root];
|
|
126
|
+
while (stack.length > 0) {
|
|
127
|
+
const cur = stack.pop() as string;
|
|
128
|
+
let entries: fs.Dirent[];
|
|
129
|
+
try {
|
|
130
|
+
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const e of entries) {
|
|
135
|
+
if (e.isSymbolicLink()) continue;
|
|
136
|
+
const full = path.join(cur, e.name);
|
|
137
|
+
if (e.isDirectory()) {
|
|
138
|
+
if (!SKIP_DIRS.has(e.name)) stack.push(full);
|
|
139
|
+
} else if (e.name.toLowerCase().endsWith('.md')) {
|
|
140
|
+
out.push(full);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out.sort();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `true` when `child` is at or below `root`. */
|
|
148
|
+
function isUnder(child: string, root: string): boolean {
|
|
149
|
+
const rel = path.relative(root, child);
|
|
150
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function scan(root: string): ScanResult {
|
|
154
|
+
const roadmapRoot = path.join(root, ...ROADMAP_ROOT_REL.split('/'));
|
|
155
|
+
const files = walkMarkdown(root);
|
|
156
|
+
const findings: Finding[] = [];
|
|
157
|
+
for (const f of files) {
|
|
158
|
+
if (isUnder(f, roadmapRoot)) continue;
|
|
159
|
+
// Cheap name pre-filter first — the SHARED definition, imported, so the
|
|
160
|
+
// roadmap corpus and this guard cannot drift apart.
|
|
161
|
+
if (!isRoadmapCandidate(f)) continue;
|
|
162
|
+
let text: string;
|
|
163
|
+
try {
|
|
164
|
+
text = fs.readFileSync(f, 'utf-8');
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const shape = roadmapShape(text);
|
|
169
|
+
if (!shape.isRoadmap) continue;
|
|
170
|
+
findings.push({
|
|
171
|
+
rel: path.relative(root, f).split(path.sep).join('/'),
|
|
172
|
+
signals: shape.signals,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { findings, scanned: files.length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface Args {
|
|
179
|
+
quiet: boolean;
|
|
180
|
+
selfTest: boolean;
|
|
181
|
+
root: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseArgs(argv: readonly string[]): Args {
|
|
185
|
+
const args: Args = { quiet: false, selfTest: false, root: REPO_ROOT };
|
|
186
|
+
for (let i = 0; i < argv.length; i++) {
|
|
187
|
+
const a = argv[i] as string;
|
|
188
|
+
if (a === '--quiet') args.quiet = true;
|
|
189
|
+
else if (a === '--self-test') args.selfTest = true;
|
|
190
|
+
else if (a === '--root') args.root = path.resolve(argv[++i] ?? '.');
|
|
191
|
+
else if (a === '-h' || a === '--help') {
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
'usage: check_agent_artifact_location [-h] [--quiet] [--root DIR] [--self-test]\n',
|
|
194
|
+
);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
} else {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`check_agent_artifact_location: error: unrecognized arguments: ${a}\n`,
|
|
199
|
+
);
|
|
200
|
+
process.exit(2);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return args;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A minimal file with all three shape signals. */
|
|
207
|
+
const ROADMAP_FIXTURE =
|
|
208
|
+
'---\ncomplexity: lightweight\nstatus: ready\n---\n' +
|
|
209
|
+
'# Road to a fixture\n\n## Phase 1 - a phase\n\n- [ ] **1.1 A step.**\n';
|
|
210
|
+
|
|
211
|
+
/** Same content, but not roadmap-shaped — frontmatter only. */
|
|
212
|
+
const PROSE_FIXTURE = '---\ntitle: a design note\n---\n\n# A design note\n\n- [ ] a todo\n';
|
|
213
|
+
|
|
214
|
+
function selfTestCases(): SelfTestCase[] {
|
|
215
|
+
const mk = (rel: string, body: string): string => {
|
|
216
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'artloc-'));
|
|
217
|
+
const full = path.join(dir, ...rel.split('/'));
|
|
218
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
219
|
+
fs.writeFileSync(full, body);
|
|
220
|
+
return dir;
|
|
221
|
+
};
|
|
222
|
+
const run = (dir: string): number =>
|
|
223
|
+
runGateCli(
|
|
224
|
+
REPO_ROOT,
|
|
225
|
+
'src/scripts/check_agent_artifact_location.ts',
|
|
226
|
+
['--root', dir, '--quiet'],
|
|
227
|
+
REPO_ROOT,
|
|
228
|
+
);
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
name: 'a roadmap-shaped file under docs/roadmaps/ is reported',
|
|
232
|
+
expect: 'reject',
|
|
233
|
+
run: () => run(mk('docs/roadmaps/road-to-a-fixture.md', ROADMAP_FIXTURE)),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'a roadmap-shaped file at the tree root is reported',
|
|
237
|
+
expect: 'reject',
|
|
238
|
+
run: () => run(mk('road-to-a-fixture.md', ROADMAP_FIXTURE)),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'the SAME file under agents/roadmaps/ is not reported',
|
|
242
|
+
expect: 'accept',
|
|
243
|
+
run: () => run(mk('agents/roadmaps/road-to-a-fixture.md', ROADMAP_FIXTURE)),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'a file under agents/roadmaps/later/ is not reported',
|
|
247
|
+
expect: 'accept',
|
|
248
|
+
run: () => run(mk('agents/roadmaps/later/road-to-a-fixture.md', ROADMAP_FIXTURE)),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'prose carrying frontmatter and a checkbox but no phase is not reported',
|
|
252
|
+
expect: 'accept',
|
|
253
|
+
run: () => run(mk('docs/a-design-note.md', PROSE_FIXTURE)),
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function main(argv?: readonly string[]): number {
|
|
259
|
+
const args = parseArgs(argv ?? process.argv.slice(2));
|
|
260
|
+
|
|
261
|
+
if (args.selfTest) {
|
|
262
|
+
return runSelfTest({
|
|
263
|
+
gate: 'check_agent_artifact_location',
|
|
264
|
+
cases: selfTestCases(),
|
|
265
|
+
minCases: 5,
|
|
266
|
+
minRejectCases: 2,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let result: ScanResult;
|
|
271
|
+
try {
|
|
272
|
+
result = scan(args.root);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
275
|
+
process.stderr.write(`❌ internal error: ${msg}\n`);
|
|
276
|
+
return 3;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The unit is every `.md` the walk VISITED, not the matches: zero matches is
|
|
280
|
+
// the normal clean state, so only the unfiltered walk can tell "nothing
|
|
281
|
+
// misplaced" from "nothing read".
|
|
282
|
+
try {
|
|
283
|
+
reportScanned({
|
|
284
|
+
gate: 'check_agent_artifact_location',
|
|
285
|
+
scanned: result.scanned,
|
|
286
|
+
units: 'markdown file(s)',
|
|
287
|
+
roots: [path.relative(REPO_ROOT, args.root) || '.'],
|
|
288
|
+
});
|
|
289
|
+
} catch (exc) {
|
|
290
|
+
if (exc instanceof DeadScopeError) {
|
|
291
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
throw exc;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (result.findings.length === 0) {
|
|
298
|
+
if (!args.quiet) {
|
|
299
|
+
process.stdout.write(
|
|
300
|
+
`✅ check_agent_artifact_location: no roadmap-shaped file outside ` +
|
|
301
|
+
`${ROADMAP_ROOT_REL}/ (${String(result.scanned)} markdown file(s) read).\n`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
process.stderr.write(
|
|
308
|
+
`❌ check_agent_artifact_location: ${String(result.findings.length)} ` +
|
|
309
|
+
`roadmap-shaped file(s) outside ${ROADMAP_ROOT_REL}/:\n`,
|
|
310
|
+
);
|
|
311
|
+
for (const f of result.findings) {
|
|
312
|
+
process.stderr.write(` ${f.rel}\n`);
|
|
313
|
+
process.stderr.write(` reads as a roadmap because it carries ${f.signals.join(', ')}\n`);
|
|
314
|
+
}
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
` Move it to ${ROADMAP_ROOT_REL}/, or — if it is genuinely not a roadmap — ` +
|
|
317
|
+
'say which of the three signals it should not be carrying.\n' +
|
|
318
|
+
' NOTE: this gate scans THIS repository only. A consumer project never runs\n' +
|
|
319
|
+
' it; `agent-config doctor` is the surface that carries a finding outward.\n',
|
|
320
|
+
);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _isCliEntry(): boolean {
|
|
325
|
+
if (process.argv[1] === undefined) return false;
|
|
326
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
327
|
+
if (import.meta.url === argvUrl) return true;
|
|
328
|
+
try {
|
|
329
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (_isCliEntry()) {
|
|
336
|
+
process.exit(main());
|
|
337
|
+
}
|
|
@@ -20,10 +20,14 @@
|
|
|
20
20
|
* - `check_command_count_messaging.ts` keeps the command-specific
|
|
21
21
|
* active-vs-shim split checks.
|
|
22
22
|
*
|
|
23
|
-
* Scope: flagship surfaces
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Scope: flagship surfaces (SURFACES below) PLUS every governed rule under
|
|
24
|
+
* `src/rules/` (RULE_SURFACE_DIR). Rules were the largest hole this gate had:
|
|
25
|
+
* they are the most-delivered surface this package ships — a wrong self-count
|
|
26
|
+
* there reaches every consumer session — and the list below named sixteen doc
|
|
27
|
+
* paths and no rule path. Dated census / analysis snapshots (SKILL_CENSUS,
|
|
28
|
+
* skills-taxonomy, positioning-evidence, …) carry point-in-time counts by
|
|
29
|
+
* design and are deliberately out of scope — the gate's charter is the surfaces
|
|
30
|
+
* a fresh reader treats as current.
|
|
27
31
|
*
|
|
28
32
|
* Exit codes: 0 clean · 1 drift or internal inconsistency.
|
|
29
33
|
*/
|
|
@@ -66,6 +70,41 @@ export const SURFACES: readonly string[] = [
|
|
|
66
70
|
// absent there. A gate must judge repo content, not untracked local state.
|
|
67
71
|
];
|
|
68
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Governed rules, enumerated rather than listed.
|
|
75
|
+
*
|
|
76
|
+
* A hand-maintained list of 120 rule paths would go stale on the first rule
|
|
77
|
+
* added, which is the drift class this gate exists to catch — so the rule
|
|
78
|
+
* surface is a directory, walked at run time.
|
|
79
|
+
*/
|
|
80
|
+
export const RULE_SURFACE_DIR = 'src/rules';
|
|
81
|
+
|
|
82
|
+
export function rule_surfaces(root: string = ROOT): string[] {
|
|
83
|
+
const dir = path.join(root, RULE_SURFACE_DIR);
|
|
84
|
+
if (!fs.existsSync(dir)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return fs
|
|
88
|
+
.readdirSync(dir)
|
|
89
|
+
.filter((f) => f.endsWith('.md'))
|
|
90
|
+
.sort()
|
|
91
|
+
.map((f) => `${RULE_SURFACE_DIR}/${f}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A count that belongs to a dated measurement, not to this package's live
|
|
96
|
+
* totals.
|
|
97
|
+
*
|
|
98
|
+
* The gate's charter already excludes point-in-time snapshots; until now it
|
|
99
|
+
* could only express that by leaving a whole FILE out. A rule may carry a dated
|
|
100
|
+
* host measurement inside a paragraph of live prose, and neither excluding the
|
|
101
|
+
* rule nor rewriting the measurement to today's total is correct — the second
|
|
102
|
+
* falsifies recorded evidence. The marker is per line, and it is a claim a
|
|
103
|
+
* reviewer can falsify: the line must name the date or the artefact the figure
|
|
104
|
+
* was measured against.
|
|
105
|
+
*/
|
|
106
|
+
export const DATED_MEASUREMENT_MARKER = '<!-- count: dated -->';
|
|
107
|
+
|
|
69
108
|
/** kind → canonical-count resolver. "governed rules" is the canonical total
|
|
70
109
|
* phrasing for rules; bare "N rules" is NOT matched (too many legitimate
|
|
71
110
|
* subset scopes: kernel rules, router rules, maintainer-only rules …). */
|
|
@@ -108,6 +147,9 @@ export function scan_text(
|
|
|
108
147
|
for (const [kind, pattern] of KIND_PATTERNS) {
|
|
109
148
|
for (let i = 0; i < lines.length; i++) {
|
|
110
149
|
const re = new RegExp(pattern.source, pattern.flags);
|
|
150
|
+
if (lines[i]!.includes(DATED_MEASUREMENT_MARKER)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
111
153
|
for (const m of lines[i]!.matchAll(re)) {
|
|
112
154
|
const found = Number.parseInt(m[2]!, 10);
|
|
113
155
|
(seen[kind] ??= new Set()).add(found);
|
|
@@ -219,6 +261,15 @@ export interface CoverageGap {
|
|
|
219
261
|
*
|
|
220
262
|
* A position counts as anchored when some `TARGETS` pattern for the same
|
|
221
263
|
* file, bound to a compatible kind, matches the same line.
|
|
264
|
+
*
|
|
265
|
+
* `src/rules/**` is scanned for VALUE drift but is deliberately excluded from
|
|
266
|
+
* this pass, and the reason is mechanical rather than stylistic: satisfying it
|
|
267
|
+
* would require `update_counts` to become a writer into `src/rules/`, where the
|
|
268
|
+
* kernel rules live behind `block_kernel_rule_writes`. A generator that must
|
|
269
|
+
* never touch part of its own target directory is a generator waiting to be
|
|
270
|
+
* disarmed by a path move. So a stale count in a rule fails on value and a human
|
|
271
|
+
* fixes it — the coverage claim this pass makes stays true of the surfaces it
|
|
272
|
+
* actually covers, instead of being widened into a claim it cannot keep.
|
|
222
273
|
*/
|
|
223
274
|
export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
|
|
224
275
|
const anchorsByFile = new Map<string, ReadonlyArray<[string, string]>>();
|
|
@@ -254,6 +305,16 @@ export function anchor_coverage_gaps(root: string = ROOT): CoverageGap[] {
|
|
|
254
305
|
|
|
255
306
|
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
256
307
|
const QUIET = argv.includes('--quiet');
|
|
308
|
+
// Enumerated, not listed. An empty walk means the rule tree moved and this
|
|
309
|
+
// gate is watching nothing — a dead scope, never a clean run.
|
|
310
|
+
const rules = rule_surfaces();
|
|
311
|
+
if (rules.length === 0) {
|
|
312
|
+
process.stderr.write(
|
|
313
|
+
`❌ check_artefact_count_messaging: ${RULE_SURFACE_DIR}/ resolved to no .md file — ` +
|
|
314
|
+
'the rule surface moved and this gate would pass by blindness.\n',
|
|
315
|
+
);
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
257
318
|
// This gate walks no tree — it guards a hand-maintained surface list, and
|
|
258
319
|
// every missing entry is skipped with a warning so nothing hard-breaks on a
|
|
259
320
|
// doc rename. That tolerance is also the failure mode: a repo-wide docs
|
|
@@ -262,7 +323,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
262
323
|
try {
|
|
263
324
|
assertWatchlistResolves({
|
|
264
325
|
gate: 'check_artefact_count_messaging',
|
|
265
|
-
candidates: [...SURFACES, ...STRUCTURED_SURFACES.map((s) => s.file)],
|
|
326
|
+
candidates: [...SURFACES, ...STRUCTURED_SURFACES.map((s) => s.file), ...rules],
|
|
266
327
|
repoRoot: ROOT,
|
|
267
328
|
});
|
|
268
329
|
} catch (exc) {
|
|
@@ -285,7 +346,7 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
285
346
|
|
|
286
347
|
const allFindings: Finding[] = [];
|
|
287
348
|
const global_seen: Record<string, Set<number>> = {};
|
|
288
|
-
for (const rel of SURFACES) {
|
|
349
|
+
for (const rel of [...SURFACES, ...rules]) {
|
|
289
350
|
const p = path.join(ROOT, rel);
|
|
290
351
|
if (!fs.existsSync(p)) {
|
|
291
352
|
process.stderr.write(` ⚠️ surface missing (skipped): ${rel}\n`);
|
|
@@ -347,8 +408,13 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
347
408
|
process.stdout.write(` internal inconsistency — ${line}\n`);
|
|
348
409
|
}
|
|
349
410
|
process.stdout.write(
|
|
350
|
-
'\nFix: run `./scripts-run src/scripts/update_counts`
|
|
351
|
-
'
|
|
411
|
+
'\nFix: on a flagship surface, run `./scripts-run src/scripts/update_counts` —\n' +
|
|
412
|
+
'every anchored position is generator-written, so never hand-type one there.\n' +
|
|
413
|
+
`Under ${RULE_SURFACE_DIR}/ the generator does not write (the kernel rules live\n` +
|
|
414
|
+
'there behind block_kernel_rule_writes), so a rule count is a hand edit: correct\n' +
|
|
415
|
+
'the number, or — if the figure is a dated measurement rather than a live\n' +
|
|
416
|
+
`self-count — mark that line \`${DATED_MEASUREMENT_MARKER}\` and say what it was\n` +
|
|
417
|
+
'measured against.\n',
|
|
352
418
|
);
|
|
353
419
|
}
|
|
354
420
|
|
|
@@ -628,6 +628,26 @@ export interface Summary {
|
|
|
628
628
|
frequency_gap: number;
|
|
629
629
|
/** Rules with no `obligation_frequency` to join — the nine kernel rules. */
|
|
630
630
|
frequency_unclassified: number;
|
|
631
|
+
/**
|
|
632
|
+
* The three classes that make `undeclared` readable.
|
|
633
|
+
*
|
|
634
|
+
* WHY THEY SHIP IN THE ARTEFACT. `undeclared` is the number reviewers quote,
|
|
635
|
+
* and quoted bare it reads as a backlog of rules somebody forgot to wire.
|
|
636
|
+
* Most of it is nothing of the kind. `kernel_denied` rules cannot carry an
|
|
637
|
+
* `enforced_by` field at all — `block_kernel_rule_writes` refuses the write
|
|
638
|
+
* with no agent-accessible override, so their absence is structural.
|
|
639
|
+
* `observer` rules have a carrier that fires and cannot block by design.
|
|
640
|
+
* `carrier_less` rules have no machine carrier of any kind.
|
|
641
|
+
*
|
|
642
|
+
* A reader subtracting these from `undeclared` gets the population where
|
|
643
|
+
* wiring is actually the missing thing — and, more usefully, sees at a glance
|
|
644
|
+
* that the observer set (10) is the reachable one: a floor set against the
|
|
645
|
+
* full `undeclared` would stall in its second release and be ignored, which
|
|
646
|
+
* is the failure `check_release_highlights.ts` already documents for its own
|
|
647
|
+
* warning.
|
|
648
|
+
*/
|
|
649
|
+
kernel_denied: number;
|
|
650
|
+
carrier_less: number;
|
|
631
651
|
/**
|
|
632
652
|
* The denominator every figure above is taken over, WITH its frame. See
|
|
633
653
|
* {@link denominator_frames} for why the frame ships even when the two
|
|
@@ -652,6 +672,14 @@ export function summarise(rows: RuleCoverage[]): Summary {
|
|
|
652
672
|
blocking_pct: rows.length === 0 ? 0 : Math.round((blocking / rows.length) * 1000) / 10,
|
|
653
673
|
frequency_gap: count((r) => r.frequency_verdict === 'gap'),
|
|
654
674
|
frequency_unclassified: count((r) => r.frequency_verdict === 'unclassified'),
|
|
675
|
+
kernel_denied: count(
|
|
676
|
+
(r) => KERNEL_RULE_ID_SET.has(r.id) && r.frequency_verdict === 'unclassified',
|
|
677
|
+
),
|
|
678
|
+
carrier_less: count(
|
|
679
|
+
(r) =>
|
|
680
|
+
r.declared.length > 0 &&
|
|
681
|
+
r.declared.every((d) => d.startsWith('observer:maintainer-review')),
|
|
682
|
+
),
|
|
655
683
|
frames: denominator_frames(rows.length),
|
|
656
684
|
};
|
|
657
685
|
}
|
|
@@ -779,7 +807,13 @@ function main(argv: string[]): number {
|
|
|
779
807
|
'blocking and in that bucket at once. Coverage is a ratchet: --check fails ' +
|
|
780
808
|
'when blocking falls, or unwired / local_only / missing / frequency_gap ' +
|
|
781
809
|
'rises. Regenerate intentionally with --write-baseline when the change is ' +
|
|
782
|
-
'the point.'
|
|
810
|
+
'the point. READ `undeclared` WITH ITS THREE CLASSES, never bare: ' +
|
|
811
|
+
'`kernel_denied` cannot carry an enforced_by field at all ' +
|
|
812
|
+
'(block_kernel_rule_writes refuses the write, no agent-accessible ' +
|
|
813
|
+
'override), `observer` has a carrier that fires and cannot block by ' +
|
|
814
|
+
'design, and `carrier_less` has no machine carrier of any kind. The ' +
|
|
815
|
+
'reachable population is the observer set, not `undeclared` — a floor ' +
|
|
816
|
+
'set against the full number would stall in its second release.',
|
|
783
817
|
summary,
|
|
784
818
|
},
|
|
785
819
|
null,
|
|
@@ -811,6 +845,11 @@ function main(argv: string[]): number {
|
|
|
811
845
|
` frequency: ${summary.frequency_gap} gap · ${summary.frequency_unclassified} unclassified ` +
|
|
812
846
|
`(kernel — block_kernel_rule_writes denies the field)`,
|
|
813
847
|
);
|
|
848
|
+
lines.push(
|
|
849
|
+
` undeclared ${summary.undeclared} splits: ${summary.kernel_denied} kernel-denied · ` +
|
|
850
|
+
`${summary.observer} observer · ${summary.carrier_less} carrier-less — ` +
|
|
851
|
+
`the observer set is the reachable population`,
|
|
852
|
+
);
|
|
814
853
|
// The denominator is published WITH its frame, and it is the only sanctioned
|
|
815
854
|
// source for that number: `check_enforcement_denominator` reds when a
|
|
816
855
|
// tracked doc carries an enforcement count this resolver did not produce.
|