@event4u/agent-config 14.19.0 → 14.21.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 (161) 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 +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -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');
@@ -14,6 +14,19 @@
14
14
  * (`- **Run:**`). A gate that claims to be executable without naming the
15
15
  * command is the same defect as a decision with no option set: it reads
16
16
  * as actionable and is not.
17
+ * 4. An annotation pointing at a USER-DECISION blocker (one whose `Owner:`
18
+ * is the maintainer, the user, or the owner) carries an `asked:` field
19
+ * recording whether the question was put and, when it was not, why —
20
+ * `<!-- blocked-by: <id> | asked: no — <reason> -->`. A decision only the
21
+ * user can make, filed in a roadmap without ever being put, is the defect
22
+ * road-to-asked-not-parked exists to stop; without this field a marker
23
+ * cannot distinguish a declined decision from an unoffered one.
24
+ *
25
+ * HARD, not ratcheted, and it can be: no active roadmap carries a real
26
+ * checkbox annotation today, so on the day it ships the rule fires on
27
+ * nothing — the same "no backlog to grandfather" argument the `Class:`
28
+ * contract above makes. `later/` and `archive/` are outside this gate's
29
+ * glob and are untouched by it.
17
30
  *
18
31
  * Fenced code blocks are stripped before scanning so a roadmap that shows
19
32
  * the `## Blockers` shape as a documentation example is not flagged.
@@ -42,7 +55,19 @@ const BLOCKER_HEADING_RE = /^###[ \t]+blocker:[ \t]*(.+?)[ \t]*$/gim;
42
55
  // wrapped continuation line or inline-code documentation example of the
43
56
  // syntax (as this very lint script's own roadmap step describes it) must
44
57
  // not be mistaken for a live cross-reference.
45
- const BLOCKED_BY_LINE_RE = /^-[ \t]*\[[ xX~-]\].*<!--[ \t]*blocked-by:[ \t]*([a-z0-9-]+)[ \t]*-->/i;
58
+ const BLOCKED_BY_LINE_RE =
59
+ /^-[ \t]*\[[ xX~-]\].*<!--[ \t]*blocked-by:[ \t]*([a-z0-9-]+)[ \t]*(?:\|[ \t]*asked:[ \t]*(yes|no)[ \t]*(?:[\u2014-][ \t]*([^>]*?))?[ \t]*)?-->/i;
60
+
61
+ /**
62
+ * The owners whose blockers are the USER-DECISION class.
63
+ *
64
+ * Read off `Owner:` rather than guessed from prose: `terminal-states.md`
65
+ * defines `blocked` to include "a decision only the user can make", and the
66
+ * owner field is the only place a roadmap says which blockers those are.
67
+ * `council`, `implementer` and `agent` are deliberately outside the class —
68
+ * those are not decisions the user is waiting to be asked.
69
+ */
70
+ const USER_DECISION_OWNER_RE = /^-[ \t]*\*\*Owner:\*\*[ \t]*(maintainer|user|owner)\b/im;
46
71
 
47
72
  const REQUIRED_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
48
73
  ['Status', /^-[ \t]*\*\*Status:\*\*/im],
@@ -154,6 +179,7 @@ function _scanBoth(rawText: string): ScanResult {
154
179
  const decidability: Violation[] = [];
155
180
  const text = _stripFencedCode(rawText);
156
181
  const declaredIds = new Set<string>();
182
+ const userDecisionIds = new Set<string>();
157
183
 
158
184
  const sectionMatch = BLOCKERS_SECTION_RE.exec(text);
159
185
  if (sectionMatch) {
@@ -181,6 +207,9 @@ function _scanBoth(rawText: string): ScanResult {
181
207
  declaredIds.add(cur.id);
182
208
  const bodyEnd = i + 1 < heads.length ? (heads[i + 1] as { start: number }).start : section.length;
183
209
  const body = section.slice(cur.end, bodyEnd);
210
+ if (USER_DECISION_OWNER_RE.test(body)) {
211
+ userDecisionIds.add(cur.id);
212
+ }
184
213
  const missing = REQUIRED_FIELDS.filter(([, re]) => !re.test(body)).map(([name]) => name);
185
214
  if (missing.length) {
186
215
  violations.push({
@@ -256,6 +285,27 @@ function _scanBoth(rawText: string): ScanResult {
256
285
  `(no matching '### blocker: ${id}' in this file)`,
257
286
  });
258
287
  }
288
+ if (userDecisionIds.has(id)) {
289
+ const asked = m[2] === undefined ? null : (m[2] as string).toLowerCase();
290
+ const reason = ((m[3] as string | undefined) ?? '').trim();
291
+ if (asked === null) {
292
+ violations.push({
293
+ line: i + 1,
294
+ message:
295
+ `blocked-by '${id}' is a user-decision blocker and carries no ` +
296
+ 'asked: field — write `| asked: yes` when the question was put, ' +
297
+ 'or `| asked: no — <reason>` when it was not',
298
+ });
299
+ } else if (asked === 'no' && reason === '') {
300
+ violations.push({
301
+ line: i + 1,
302
+ message:
303
+ `blocked-by '${id}' says asked: no with no reason — a marker that ` +
304
+ 'does not say why the question was never put is the parking lot ' +
305
+ 'this field exists to end',
306
+ });
307
+ }
308
+ }
259
309
  }
260
310
  violations.sort((a, b) => a.line - b.line);
261
311
  decidability.sort((a, b) => a.line - b.line);