@event4u/agent-config 14.19.0 → 14.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +108 -156
  5. package/dist/agent-src/commands/agent-handoff.md +42 -41
  6. package/dist/agent-src/contexts/execution/host-capability-manifest.md +7 -1
  7. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  8. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  9. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  10. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  11. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/agent-src/templates/roadmaps.md +14 -1
  14. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  15. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  16. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  17. package/dist/cli/mcp/dispatch.js +10 -0
  18. package/dist/cli/mcp/stdio.js +33 -2
  19. package/dist/cli/mcp/telemetry.js +227 -0
  20. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-C6IPZRDA.js} +79 -12
  21. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  22. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-NUMQHLS5.js} +21 -4
  23. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  24. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  25. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  26. package/dist/cli-delegate/cmd_conformance.js +73 -109
  27. package/dist/cli-delegate/cmd_doctor.js +2 -2
  28. package/dist/cli-delegate/cmd_export.js +2 -2
  29. package/dist/cli-delegate/cmd_handoff.js +41 -11
  30. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  31. package/dist/cli-delegate/cmd_prune.js +2 -2
  32. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  33. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  34. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  35. package/dist/cli-delegate/cmd_sync.js +2 -2
  36. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  37. package/dist/cli-delegate/cmd_validate.js +2 -2
  38. package/dist/discovery/deprecation-report.md +1 -1
  39. package/dist/discovery/discovery-manifest.json +9 -9
  40. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  41. package/dist/discovery/discovery-manifest.summary.md +1 -1
  42. package/dist/discovery/dormancy-report.md +1 -1
  43. package/dist/discovery/orphan-report.md +1 -1
  44. package/dist/discovery/packs.json +2 -2
  45. package/dist/discovery/trust-report.md +1 -1
  46. package/dist/discovery/workspaces.json +2 -2
  47. package/dist/hooks/dispatch.js +7257 -6497
  48. package/dist/install/install.mjs +393 -168
  49. package/dist/mcp/registry-manifest.json +2 -2
  50. package/dist/mcp/server.json +2 -2
  51. package/dist/mcp/server.mjs +497 -118
  52. package/dist/scripts/mcp_server/telemetry.js +148 -0
  53. package/docs/guidelines/agent-infra/naming.md +1 -1
  54. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  55. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  56. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  57. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  58. package/package.json +1 -1
  59. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  60. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  61. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  62. package/src/config/assurance-capability-registry.json +9 -0
  63. package/src/config/gate-coverage.yml +55 -0
  64. package/src/config/gate-violation-baselines.json +15 -3
  65. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  66. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  67. package/src/scripts/_lib/host_capability.ts +38 -0
  68. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  69. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  70. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  71. package/src/scripts/_lib/orchestration_record.ts +23 -1
  72. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  73. package/src/scripts/_lib/release_position.ts +96 -0
  74. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  75. package/src/scripts/_lib/security_lint.ts +179 -7
  76. package/src/scripts/_lib/self_repair.ts +141 -6
  77. package/src/scripts/_lib/self_repair_store.ts +273 -7
  78. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  79. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  80. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  81. package/src/scripts/check_pack_size.ts +321 -3
  82. package/src/scripts/handoff_context_hook.ts +54 -7
  83. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  84. package/src/scripts/install.ts +4 -4
  85. package/src/scripts/lint_agent_security.ts +316 -25
  86. package/src/scripts/lint_confusables.ts +42 -6
  87. package/src/scripts/lint_handoffs.ts +0 -132
  88. package/src/scripts/lint_hidden_unicode.ts +49 -7
  89. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  90. package/src/scripts/lint_mcp_config_security.ts +42 -6
  91. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  92. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  93. package/src/scripts/mcp_server/resources.ts +41 -0
  94. package/src/scripts/mcp_server/telemetry.ts +19 -4
  95. package/src/scripts/orchestration_record.ts +12 -1
  96. package/src/scripts/release.ts +24 -24
  97. package/src/scripts/release_publication.ts +6 -2
  98. package/src/scripts/self_repair_cli.ts +31 -1
  99. package/src/scripts/skill_scout.ts +69 -0
  100. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  101. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -55,9 +55,19 @@ const _TOKEN = TOKEN_RE;
55
55
  export const _classify_token = classifyToken;
56
56
 
57
57
  export function _scan(sf: sl.ScannedFile): sl.Finding[] {
58
- if (sf.pragma_allows(CHECK)) {
59
- return [];
58
+ // A BOUND pragma suppresses only the matches it fingerprints, so the scan
59
+ // runs and its output is filtered at the end. An UNBOUND one keeps the old
60
+ // whole-file bail and reports itself.
61
+ const pragmaForm = sf.pragma_form(CHECK);
62
+ if (pragmaForm === 'legacy') {
63
+ return [sl.legacy_pragma_finding(sf, CHECK)];
60
64
  }
65
+ // EVERY exit from this function goes through `finish`. Wrapping only the
66
+ // terminal `return` is the defect that shipped for one commit here: this
67
+ // scan has an early return for an artifact with no `execution:` block, and a
68
+ // bound pragma silently did not apply to exactly those artifacts.
69
+ const finish = (hits: sl.Finding[]): sl.Finding[] =>
70
+ pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
61
71
  const out: sl.Finding[] = [];
62
72
  for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true })) {
63
73
  const tokens = text.match(_TOKEN);
@@ -78,15 +88,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
78
88
  }
79
89
  }
80
90
  }
81
- return out;
91
+ return finish(out);
82
92
  }
83
93
 
84
94
  interface Args {
95
+ /** Scan base for the bounded root mode; absent = the package root. */
96
+ root?: string;
85
97
  json: boolean;
86
98
  }
87
99
 
88
100
  function _argError(msg: string): never {
89
- process.stderr.write('usage: lint_confusables [-h] [--json]\n');
101
+ process.stderr.write('usage: lint_confusables [-h] [--json] [--root DIR]\n');
90
102
  process.stderr.write(`lint_confusables: error: ${msg}\n`);
91
103
  process.exit(2);
92
104
  }
@@ -94,12 +106,28 @@ function _argError(msg: string): never {
94
106
  function parse_args(argv: readonly string[]): Args {
95
107
  const out: Args = { json: false };
96
108
  const extra: string[] = [];
109
+ let rootPending = false;
97
110
  for (const a of argv) {
111
+ if (rootPending) {
112
+ out.root = a;
113
+ rootPending = false;
114
+ continue;
115
+ }
98
116
  if (a === '-h' || a === '--help') {
99
- process.stdout.write('usage: lint_confusables [-h] [--json]\n');
117
+ process.stdout.write('usage: lint_confusables [-h] [--json] [--root DIR]\n');
100
118
  process.exit(0);
101
119
  } else if (a === '--json') {
102
120
  out.json = true;
121
+ } else if (a === '--root' || a.startsWith('--root=')) {
122
+ // Bounded root mode. Absent, this linter's own default roots stand;
123
+ // present, the same relative roots resolve against DIR instead. Used by
124
+ // the scout to scan a quarantined candidate, and by --self-test fixtures.
125
+ const eq = a.indexOf('=');
126
+ if (eq !== -1) {
127
+ out.root = a.slice(eq + 1);
128
+ } else {
129
+ rootPending = true;
130
+ }
103
131
  } else {
104
132
  extra.push(a);
105
133
  }
@@ -112,10 +140,11 @@ function parse_args(argv: readonly string[]): Args {
112
140
 
113
141
  export function main(argv: readonly string[] | null = null): number {
114
142
  const args = parse_args(argv ?? process.argv.slice(2));
143
+ const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
115
144
 
116
145
  const findings: sl.Finding[] = [];
117
146
  let scanned = 0;
118
- for (const sf of sl.iter_corpus()) {
147
+ for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
119
148
  // Counted on the corpus walk, not on `_scan`'s return: `iter_corpus`
120
149
  // skips a root that does not exist, so every root moving at once yields
121
150
  // zero files and the report line still says "clean (scanned <roots>)".
@@ -143,6 +172,13 @@ export function main(argv: readonly string[] | null = null): number {
143
172
  throw exc;
144
173
  }
145
174
 
175
+ // Publish what this child inspected so the umbrella can aggregate a real
176
+ // corpus size rather than a count of children. Same number the assertion
177
+ // above just accepted, so the published figure cannot drift from it.
178
+ if (args.json) {
179
+ sl.report_child_scanned(scanned);
180
+ }
181
+
146
182
  if (args.json) {
147
183
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
148
184
  process.stdout.write(payload + '\n');
@@ -821,119 +821,6 @@ function _pyRepr(s: string): string {
821
821
  return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
822
822
  }
823
823
 
824
- // ── HANDOFF.md artifact validation (workflow-contracts Phase 2) ─────────
825
- // SEPARATE concern from the cross-wing skill-handoff lint above: this
826
- // validates the OPTIONAL standing workflow-resume artifact
827
- // (agents/runtime/state/HANDOFF.md, written by /agent-handoff --file).
828
- // Same file on purpose — one linter owns everything named "handoff";
829
- // the naming reconciliation is this comment.
830
-
831
- export const HANDOFF_ARTIFACT_REQUIRED = [
832
- 'Mode',
833
- 'Contract received',
834
- 'Contract owed',
835
- 'Decisions',
836
- 'Open questions',
837
- 'Next command',
838
- ] as const;
839
-
840
- /** Returns missing required section headings ([] = valid). */
841
- export function validate_handoff_artifact(text: string): string[] {
842
- const missing: string[] = [];
843
- for (const heading of HANDOFF_ARTIFACT_REQUIRED) {
844
- const re = new RegExp(`^##\\s+${heading}\\s*$`, 'mi');
845
- if (!re.test(text)) {
846
- missing.push(heading);
847
- }
848
- }
849
- return missing;
850
- }
851
-
852
- /**
853
- * The body of a `## <heading>` section, up to the next heading of the same or a
854
- * higher level. Fenced blocks are stripped first so a `##` inside a code fence
855
- * cannot end the section early — a resume artefact routinely quotes commands.
856
- */
857
- export function handoff_section_body(text: string, heading: string): string | null {
858
- const withoutFences = text.replace(/^```[\s\S]*?^```\s*$/gm, '');
859
- const start = new RegExp(`^##\\s+${heading}\\s*$`, 'mi').exec(withoutFences);
860
- if (start === null) {
861
- return null;
862
- }
863
- const rest = withoutFences.slice(start.index + start[0].length);
864
- const next = /^#{1,2}\s+\S/m.exec(rest);
865
- return next === null ? rest : rest.slice(0, next.index);
866
- }
867
-
868
- // An explicit "there are none" answer. Kept as a closed list rather than a
869
- // heuristic so the escape cannot be widened by accident: a section saying
870
- // nothing and a section saying "none" are different facts, and only the first is
871
- // a defect.
872
- const _NO_OPEN_QUESTIONS = new Set(['none', 'keine', 'n/a', 'na', 'no open questions', 'nothing']);
873
-
874
- /**
875
- * Strip list markers, checkbox glyphs, emphasis and trailing sentence
876
- * punctuation. The trailing strip is skipped when it would empty the line: a
877
- * line that is ONLY punctuation (`...`) is a placeholder to name, not an empty
878
- * section to report as blank, and the two findings say different things.
879
- */
880
- function _bareLine(line: string): string {
881
- const stripped = line
882
- .trim()
883
- .replace(/^[-*+]\s*/, '')
884
- .replace(/^\[[ x~-]\]\s*/i, '')
885
- .replace(/[*_`]/g, '')
886
- .trim()
887
- .toLowerCase();
888
- const trimmed = stripped.replace(/[.!]+$/, '').trim();
889
- return trimmed.length > 0 ? trimmed : stripped;
890
- }
891
-
892
- /**
893
- * Shape check on the one required section whose emptiness is the defect rather
894
- * than a style nit. `validate_handoff_artifact` only tests that the heading
895
- * exists, so `## Open questions` followed by nothing passed while carrying
896
- * nothing — and a resume artefact whose open questions are blank hands the next
897
- * session a false all-clear.
898
- *
899
- * **The step this implements claimed the false-positive class was empty because
900
- * one `?`-terminated line is required. It is not, and the counterexample ships
901
- * in this repo:** `tests/scripts/lint_handoffs_artifact.test.ts` carries
902
- * `## Open questions` / `- none`, which is an honest answer and would be forced
903
- * to grow a fake question. So the check accepts EITHER a real question OR an
904
- * explicit none-marker, and fires only on a section that answers neither —
905
- * blank, or a bare placeholder. That keeps the defect the step names without
906
- * turning a truthful "none" into a lint error.
907
- *
908
- * Returns a finding string, or null when the shape holds. A MISSING section is
909
- * not this check's business — `validate_handoff_artifact` already reports it.
910
- */
911
- export function validate_handoff_open_questions(text: string): string | null {
912
- const body = handoff_section_body(text, 'Open questions');
913
- if (body === null) {
914
- return null;
915
- }
916
- const lines = body
917
- .split('\n')
918
- .map((l) => l.trim())
919
- .filter((l) => l.length > 0);
920
- if (lines.some((l) => l.length > 1 && l.endsWith('?'))) {
921
- return null;
922
- }
923
- const bare = lines.map(_bareLine).filter((l) => l.length > 0);
924
- if (bare.some((l) => _NO_OPEN_QUESTIONS.has(l))) {
925
- return null;
926
- }
927
- if (bare.length === 0) {
928
- return 'section `## Open questions` is empty — carry a `?`-terminated question, or state `none`';
929
- }
930
- const placeholder = new Set(['tbd', 'todo', 'wip', '...', '…', '?']);
931
- if (bare.every((l) => placeholder.has(l))) {
932
- return 'section `## Open questions` carries only a placeholder — carry a `?`-terminated question, or state `none`';
933
- }
934
- return null;
935
- }
936
-
937
824
  /** `git rev-parse HEAD`, or null outside a repository. */
938
825
  function _headCommit(): string | null {
939
826
  try {
@@ -978,25 +865,6 @@ export function main(argv?: readonly string[]): number {
978
865
  process.stdout.write(JSON.stringify(census, null, 2) + '\n');
979
866
  return 0;
980
867
  }
981
- if (args.length > 0 && String(args[0]).endsWith('HANDOFF.md')) {
982
- // artifact mode — validate the workflow-resume file's required fields
983
- const text = fs.readFileSync(_resolve(args[0] as string), 'utf-8');
984
- const missing = validate_handoff_artifact(text);
985
- const shape = validate_handoff_open_questions(text);
986
- if (missing.length === 0 && shape === null) {
987
- if (!QUIET) process.stdout.write('✅ HANDOFF artifact: all required fields present\n');
988
- return 0;
989
- }
990
- for (const m of missing) {
991
- process.stdout.write(`${args[0]}:1:handoff-artifact-missing-field: missing required section \`## ${m}\`\n`);
992
- }
993
- if (shape !== null) {
994
- process.stdout.write(`${args[0]}:1:handoff-artifact-empty-section: ${shape}\n`);
995
- }
996
- const problems = missing.length + (shape === null ? 0 : 1);
997
- process.stderr.write(`❌ ${problems} HANDOFF artifact problem(s)\n`);
998
- return 1;
999
- }
1000
868
  if (args.length > 0) {
1001
869
  skills_dir = _resolve(args[0] as string);
1002
870
  }
@@ -220,9 +220,19 @@ function _cpName(cp: number): string {
220
220
  }
221
221
 
222
222
  export function _scan(sf: sl.ScannedFile): sl.Finding[] {
223
- if (sf.pragma_allows(CHECK)) {
224
- return [];
223
+ // A BOUND pragma suppresses only the matches it fingerprints, so the scan
224
+ // runs and its output is filtered at the end. An UNBOUND one keeps the old
225
+ // whole-file bail and reports itself.
226
+ const pragmaForm = sf.pragma_form(CHECK);
227
+ if (pragmaForm === 'legacy') {
228
+ return [sl.legacy_pragma_finding(sf, CHECK)];
225
229
  }
230
+ // EVERY exit from this function goes through `finish`. Wrapping only the
231
+ // terminal `return` is the defect that shipped for one commit here: this
232
+ // scan has an early return for an artifact with no `execution:` block, and a
233
+ // bound pragma silently did not apply to exactly those artifacts.
234
+ const finish = (hits: sl.Finding[]): sl.Finding[] =>
235
+ pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
226
236
  const out: sl.Finding[] = [];
227
237
  for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true })) {
228
238
  let vs_run = 0;
@@ -260,7 +270,7 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
260
270
  );
261
271
  }
262
272
  }
263
- return out;
273
+ return finish(out);
264
274
  }
265
275
 
266
276
  // ---------------------------------------------------------------------------
@@ -464,12 +474,14 @@ export function _sanitize(filePath: string): string {
464
474
  }
465
475
 
466
476
  interface Args {
477
+ /** Scan base for the bounded root mode; absent = the package root. */
478
+ root?: string;
467
479
  json: boolean;
468
480
  fix: boolean;
469
481
  }
470
482
 
471
483
  function _argError(msg: string): never {
472
- process.stderr.write('usage: lint_hidden_unicode [-h] [--json] [--fix]\n');
484
+ process.stderr.write('usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n');
473
485
  process.stderr.write(`lint_hidden_unicode: error: ${msg}\n`);
474
486
  process.exit(2);
475
487
  }
@@ -477,14 +489,30 @@ function _argError(msg: string): never {
477
489
  function parse_args(argv: readonly string[]): Args {
478
490
  const out: Args = { json: false, fix: false };
479
491
  const extra: string[] = [];
492
+ let rootPending = false;
480
493
  for (const a of argv) {
494
+ if (rootPending) {
495
+ out.root = a;
496
+ rootPending = false;
497
+ continue;
498
+ }
481
499
  if (a === '-h' || a === '--help') {
482
- process.stdout.write('usage: lint_hidden_unicode [-h] [--json] [--fix]\n');
500
+ process.stdout.write('usage: lint_hidden_unicode [-h] [--json] [--fix] [--root DIR]\n');
483
501
  process.exit(0);
484
502
  } else if (a === '--json') {
485
503
  out.json = true;
486
504
  } else if (a === '--fix') {
487
505
  out.fix = true;
506
+ } else if (a === '--root' || a.startsWith('--root=')) {
507
+ // Bounded root mode. Absent, this linter's own default roots stand;
508
+ // present, the same relative roots resolve against DIR instead. Used by
509
+ // the scout to scan a quarantined candidate, and by --self-test fixtures.
510
+ const eq = a.indexOf('=');
511
+ if (eq !== -1) {
512
+ out.root = a.slice(eq + 1);
513
+ } else {
514
+ rootPending = true;
515
+ }
488
516
  } else {
489
517
  extra.push(a);
490
518
  }
@@ -497,11 +525,12 @@ function parse_args(argv: readonly string[]): Args {
497
525
 
498
526
  export function main(argv: readonly string[] | null = null): number {
499
527
  const args = parse_args(argv ?? process.argv.slice(2));
528
+ const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
500
529
 
501
530
  const findings: sl.Finding[] = [];
502
531
  const flagged = new Set<string>();
503
532
  let corpusFiles = 0;
504
- for (const sf of sl.iter_corpus()) {
533
+ for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
505
534
  corpusFiles += 1;
506
535
  const hits = _scan(sf);
507
536
  for (const h of hits) {
@@ -540,7 +569,12 @@ export function main(argv: readonly string[] | null = null): number {
540
569
  // corpus) and a narrower codepoint set (C0 only). `--fix` does not touch
541
570
  // these — rewriting a source file's bytes is the author's call, and the
542
571
  // finding names the escape to use.
543
- const sourceFiles = _eligibleSourceFiles();
572
+ // The source pass enumerates TRACKED files via `git ls-files`, which is a
573
+ // property of the repository and not of the scan base. Under `--root` it
574
+ // would import this repository's own files into a scan of somebody else's
575
+ // directory and attribute the findings to it, so bounded mode skips it and
576
+ // the `.md` corpus under DIR is the whole scan.
577
+ const sourceFiles = args.root === undefined ? _eligibleSourceFiles() : [];
544
578
  for (const h of _scanSourceControlBytes(sourceFiles)) {
545
579
  findings.push(h);
546
580
  }
@@ -566,6 +600,14 @@ export function main(argv: readonly string[] | null = null): number {
566
600
  }
567
601
  }
568
602
 
603
+ // Publish what this child inspected so the umbrella can aggregate a real
604
+ // corpus size rather than a count of children. BOTH passes count: the `.md`
605
+ // corpus and the raw-control-byte pass over tracked source files, which is a
606
+ // separate scope this linter genuinely reads.
607
+ if (args.json) {
608
+ sl.report_child_scanned(corpusFiles + (sourceFiles === null ? 0 : sourceFiles.length));
609
+ }
610
+
569
611
  if (args.json) {
570
612
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
571
613
  process.stdout.write(payload + '\n');
@@ -71,9 +71,19 @@ function _strip_inline_code(text: string): string {
71
71
  }
72
72
 
73
73
  export function _scan(sf: sl.ScannedFile): sl.Finding[] {
74
- if (sf.pragma_allows(CHECK)) {
75
- return [];
74
+ // A BOUND pragma suppresses only the matches it fingerprints, so the scan
75
+ // runs and its output is filtered at the end. An UNBOUND one keeps the old
76
+ // whole-file bail and reports itself.
77
+ const pragmaForm = sf.pragma_form(CHECK);
78
+ if (pragmaForm === 'legacy') {
79
+ return [sl.legacy_pragma_finding(sf, CHECK)];
76
80
  }
81
+ // EVERY exit from this function goes through `finish`. Wrapping only the
82
+ // terminal `return` is the defect that shipped for one commit here: this
83
+ // scan has an early return for an artifact with no `execution:` block, and a
84
+ // bound pragma silently did not apply to exactly those artifacts.
85
+ const finish = (hits: sl.Finding[]): sl.Finding[] =>
86
+ pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
77
87
  const out: sl.Finding[] = [];
78
88
  // prose = lines outside ANY fence; inline-code spans blanked.
79
89
  for (const [lineno, text] of sf.iter_lines({ skip_example_fence: true, skip_any_fence: true })) {
@@ -117,15 +127,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
117
127
  }
118
128
  }
119
129
  }
120
- return out;
130
+ return finish(out);
121
131
  }
122
132
 
123
133
  interface Args {
134
+ /** Scan base for the bounded root mode; absent = the package root. */
135
+ root?: string;
124
136
  json: boolean;
125
137
  }
126
138
 
127
139
  function _argError(msg: string): never {
128
- process.stderr.write('usage: lint_instruction_smuggling [-h] [--json]\n');
140
+ process.stderr.write('usage: lint_instruction_smuggling [-h] [--json] [--root DIR]\n');
129
141
  process.stderr.write(`lint_instruction_smuggling: error: ${msg}\n`);
130
142
  process.exit(2);
131
143
  }
@@ -133,12 +145,28 @@ function _argError(msg: string): never {
133
145
  function parse_args(argv: readonly string[]): Args {
134
146
  const out: Args = { json: false };
135
147
  const extra: string[] = [];
148
+ let rootPending = false;
136
149
  for (const a of argv) {
150
+ if (rootPending) {
151
+ out.root = a;
152
+ rootPending = false;
153
+ continue;
154
+ }
137
155
  if (a === '-h' || a === '--help') {
138
- process.stdout.write('usage: lint_instruction_smuggling [-h] [--json]\n');
156
+ process.stdout.write('usage: lint_instruction_smuggling [-h] [--json] [--root DIR]\n');
139
157
  process.exit(0);
140
158
  } else if (a === '--json') {
141
159
  out.json = true;
160
+ } else if (a === '--root' || a.startsWith('--root=')) {
161
+ // Bounded root mode. Absent, this linter's own default roots stand;
162
+ // present, the same relative roots resolve against DIR instead. Used by
163
+ // the scout to scan a quarantined candidate, and by --self-test fixtures.
164
+ const eq = a.indexOf('=');
165
+ if (eq !== -1) {
166
+ out.root = a.slice(eq + 1);
167
+ } else {
168
+ rootPending = true;
169
+ }
142
170
  } else {
143
171
  extra.push(a);
144
172
  }
@@ -151,10 +179,11 @@ function parse_args(argv: readonly string[]): Args {
151
179
 
152
180
  export function main(argv: readonly string[] | null = null): number {
153
181
  const args = parse_args(argv ?? process.argv.slice(2));
182
+ const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
154
183
 
155
184
  const findings: sl.Finding[] = [];
156
185
  let corpusFiles = 0;
157
- for (const sf of sl.iter_corpus()) {
186
+ for (const sf of sl.iter_corpus(sl.DEFAULT_SCAN_ROOTS, ['.md'], scanBase)) {
158
187
  corpusFiles += 1;
159
188
  for (const h of _scan(sf)) {
160
189
  findings.push(h);
@@ -181,6 +210,13 @@ export function main(argv: readonly string[] | null = null): number {
181
210
  throw exc;
182
211
  }
183
212
 
213
+ // Publish what this child inspected so the umbrella can aggregate a real
214
+ // corpus size rather than a count of children. Same number the assertion
215
+ // above just accepted, so the published figure cannot drift from it.
216
+ if (args.json) {
217
+ sl.report_child_scanned(corpusFiles);
218
+ }
219
+
184
220
  if (args.json) {
185
221
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
186
222
  process.stdout.write(payload + '\n');
@@ -119,9 +119,19 @@ function _suffix(p: string): string {
119
119
  }
120
120
 
121
121
  export function _scan(sf: sl.ScannedFile): sl.Finding[] {
122
- if (sf.pragma_allows(CHECK)) {
123
- return [];
122
+ // A BOUND pragma suppresses only the matches it fingerprints, so the scan
123
+ // runs and its output is filtered at the end. An UNBOUND one keeps the old
124
+ // whole-file bail and reports itself.
125
+ const pragmaForm = sf.pragma_form(CHECK);
126
+ if (pragmaForm === 'legacy') {
127
+ return [sl.legacy_pragma_finding(sf, CHECK)];
124
128
  }
129
+ // EVERY exit from this function goes through `finish`. Wrapping only the
130
+ // terminal `return` is the defect that shipped for one commit here: this
131
+ // scan has an early return for an artifact with no `execution:` block, and a
132
+ // bound pragma silently did not apply to exactly those artifacts.
133
+ const finish = (hits: sl.Finding[]): sl.Finding[] =>
134
+ pragmaForm === 'bound' ? sf.filter_bound_pragma(CHECK, hits) : hits;
125
135
  const out: sl.Finding[] = [];
126
136
  for (const [start, numbered] of _candidate_chunks(sf)) {
127
137
  for (const [lineno, text] of numbered) {
@@ -166,15 +176,17 @@ export function _scan(sf: sl.ScannedFile): sl.Finding[] {
166
176
  );
167
177
  }
168
178
  }
169
- return out;
179
+ return finish(out);
170
180
  }
171
181
 
172
182
  interface Args {
183
+ /** Scan base for the bounded root mode; absent = the package root. */
184
+ root?: string;
173
185
  json: boolean;
174
186
  }
175
187
 
176
188
  function _argError(msg: string): never {
177
- process.stderr.write('usage: lint_mcp_config_security [-h] [--json]\n');
189
+ process.stderr.write('usage: lint_mcp_config_security [-h] [--json] [--root DIR]\n');
178
190
  process.stderr.write(`lint_mcp_config_security: error: ${msg}\n`);
179
191
  process.exit(2);
180
192
  }
@@ -182,12 +194,28 @@ function _argError(msg: string): never {
182
194
  function parse_args(argv: readonly string[]): Args {
183
195
  const out: Args = { json: false };
184
196
  const extra: string[] = [];
197
+ let rootPending = false;
185
198
  for (const a of argv) {
199
+ if (rootPending) {
200
+ out.root = a;
201
+ rootPending = false;
202
+ continue;
203
+ }
186
204
  if (a === '-h' || a === '--help') {
187
- process.stdout.write('usage: lint_mcp_config_security [-h] [--json]\n');
205
+ process.stdout.write('usage: lint_mcp_config_security [-h] [--json] [--root DIR]\n');
188
206
  process.exit(0);
189
207
  } else if (a === '--json') {
190
208
  out.json = true;
209
+ } else if (a === '--root' || a.startsWith('--root=')) {
210
+ // Bounded root mode. Absent, this linter's own default roots stand;
211
+ // present, the same relative roots resolve against DIR instead. Used by
212
+ // the scout to scan a quarantined candidate, and by --self-test fixtures.
213
+ const eq = a.indexOf('=');
214
+ if (eq !== -1) {
215
+ out.root = a.slice(eq + 1);
216
+ } else {
217
+ rootPending = true;
218
+ }
191
219
  } else {
192
220
  extra.push(a);
193
221
  }
@@ -200,13 +228,14 @@ function parse_args(argv: readonly string[]): Args {
200
228
 
201
229
  export function main(argv: readonly string[] | null = null): number {
202
230
  const args = parse_args(argv ?? process.argv.slice(2));
231
+ const scanBase = args.root === undefined ? sl.ROOT : path.resolve(args.root);
203
232
 
204
233
  const findings: sl.Finding[] = [];
205
234
  // scan .md (fenced examples) under the default roots PLUS named MCP configs
206
235
  // under src/templates (where the shipped claude_desktop_config template lives).
207
236
  const roots = [...sl.DEFAULT_SCAN_ROOTS, 'src/templates'];
208
237
  let corpusFiles = 0;
209
- for (const sf of sl.iter_corpus(roots, ['.md', '.json', '.template'])) {
238
+ for (const sf of sl.iter_corpus(roots, ['.md', '.json', '.template'], scanBase)) {
210
239
  corpusFiles += 1;
211
240
  for (const h of _scan(sf)) {
212
241
  findings.push(h);
@@ -234,6 +263,13 @@ export function main(argv: readonly string[] | null = null): number {
234
263
  throw exc;
235
264
  }
236
265
 
266
+ // Publish what this child inspected so the umbrella can aggregate a real
267
+ // corpus size rather than a count of children. Same number the assertion
268
+ // above just accepted, so the published figure cannot drift from it.
269
+ if (args.json) {
270
+ sl.report_child_scanned(corpusFiles);
271
+ }
272
+
237
273
  if (args.json) {
238
274
  const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
239
275
  process.stdout.write(payload + '\n');