@event4u/agent-config 14.18.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 (210) 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 +120 -129
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +44 -43
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  17. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  18. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  22. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  23. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  24. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  25. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  26. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  27. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  28. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  29. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  30. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  31. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  32. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  33. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  34. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  35. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  36. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  37. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  38. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  39. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  40. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  41. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  42. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  43. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  44. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  45. package/dist/agent-src/templates/roadmaps.md +14 -1
  46. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  47. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  48. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  49. package/dist/cli/commands/recordTriggerEval.js +2 -2
  50. package/dist/cli/main.js +1 -1
  51. package/dist/cli/mcp/dispatch.js +10 -0
  52. package/dist/cli/mcp/stdio.js +33 -2
  53. package/dist/cli/mcp/telemetry.js +227 -0
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
  55. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  56. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
  57. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
  58. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  59. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  60. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  61. package/dist/cli-delegate/cmd_conformance.js +489 -121
  62. package/dist/cli-delegate/cmd_doctor.js +2 -2
  63. package/dist/cli-delegate/cmd_export.js +2 -2
  64. package/dist/cli-delegate/cmd_handoff.js +42 -12
  65. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  66. package/dist/cli-delegate/cmd_prune.js +2 -2
  67. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  68. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  69. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  70. package/dist/cli-delegate/cmd_sync.js +2 -2
  71. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  72. package/dist/cli-delegate/cmd_validate.js +2 -2
  73. package/dist/discovery/deprecation-report.md +1 -1
  74. package/dist/discovery/discovery-manifest.json +34 -33
  75. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  76. package/dist/discovery/discovery-manifest.summary.md +1 -1
  77. package/dist/discovery/dormancy-report.md +1 -1
  78. package/dist/discovery/orphan-report.md +1 -1
  79. package/dist/discovery/packs.json +2 -2
  80. package/dist/discovery/trust-report.md +1 -1
  81. package/dist/discovery/workspaces.json +2 -2
  82. package/dist/hooks/dispatch.js +4455 -3025
  83. package/dist/install/install.mjs +975 -689
  84. package/dist/mcp/registry-manifest.json +2 -2
  85. package/dist/mcp/server.json +2 -2
  86. package/dist/mcp/server.mjs +2087 -1134
  87. package/dist/scripts/_lib/model_tier.js +1 -1
  88. package/dist/scripts/mcp_server/telemetry.js +148 -0
  89. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  90. package/docs/guidelines/agent-infra/naming.md +1 -1
  91. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  92. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  93. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  94. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  95. package/docs/guidelines/design-antipatterns.md +87 -64
  96. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  97. package/package.json +1 -1
  98. package/src/agent-src/README.md +1 -1
  99. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  100. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  101. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  102. package/src/config/assurance-capability-registry.json +9 -0
  103. package/src/config/ci-local-parity.yml +6 -0
  104. package/src/config/gate-coverage.yml +225 -0
  105. package/src/config/gate-violation-baselines.json +18 -6
  106. package/src/config/release-gate-locality.yml +12 -0
  107. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  108. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  109. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  110. package/src/scripts/_cli/cmd_update.ts +1 -1
  111. package/src/scripts/_cli/cmd_versions.ts +1 -1
  112. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  113. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  114. package/src/scripts/_lib/consequence_objects.ts +152 -0
  115. package/src/scripts/_lib/exec_evidence.ts +7 -0
  116. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  117. package/src/scripts/_lib/host_capability.ts +38 -0
  118. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  119. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  120. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  121. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  122. package/src/scripts/_lib/model_tier.ts +1 -1
  123. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  124. package/src/scripts/_lib/orchestration_record.ts +23 -1
  125. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  126. package/src/scripts/_lib/release_highlights.ts +93 -0
  127. package/src/scripts/_lib/release_material.ts +64 -1
  128. package/src/scripts/_lib/release_position.ts +96 -0
  129. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  130. package/src/scripts/_lib/runtime_journal.ts +26 -3
  131. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  132. package/src/scripts/_lib/security_lint.ts +179 -7
  133. package/src/scripts/_lib/self_repair.ts +141 -6
  134. package/src/scripts/_lib/self_repair_store.ts +273 -7
  135. package/src/scripts/_lib/session_register.ts +11 -6
  136. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  137. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  138. package/src/scripts/_lib/surface.ts +86 -0
  139. package/src/scripts/_lib/ui_authority.ts +116 -0
  140. package/src/scripts/adr/regenerate_index.ts +54 -1
  141. package/src/scripts/adr_cite_check.ts +71 -15
  142. package/src/scripts/ai_council/clients.ts +1 -1
  143. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  144. package/src/scripts/build_proof.ts +15 -2
  145. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  146. package/src/scripts/check_beta_review_markers.ts +141 -17
  147. package/src/scripts/check_enforcement_coverage.ts +1 -1
  148. package/src/scripts/check_enforcement_denominator.ts +121 -11
  149. package/src/scripts/check_finding_dispositions.ts +66 -9
  150. package/src/scripts/check_gate_coverage.ts +96 -0
  151. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  152. package/src/scripts/check_pack_size.ts +321 -3
  153. package/src/scripts/check_release_highlights.ts +74 -1
  154. package/src/scripts/check_skill_activation_claim.ts +179 -0
  155. package/src/scripts/check_trigger_evals.ts +1 -1
  156. package/src/scripts/conformance_scan.ts +44 -3
  157. package/src/scripts/explain_run.ts +1 -1
  158. package/src/scripts/generate_capability_matrix.ts +1 -1
  159. package/src/scripts/git_authorization_hook.ts +75 -1
  160. package/src/scripts/handoff_context_hook.ts +54 -7
  161. package/src/scripts/hook_manifest.json +1 -1
  162. package/src/scripts/hook_manifest.yaml +14 -11
  163. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  164. package/src/scripts/hooks/category_a.ts +291 -0
  165. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  166. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  167. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  168. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  169. package/src/scripts/hooks/host_lowering.ts +215 -0
  170. package/src/scripts/hooks/host_lowering.yaml +185 -0
  171. package/src/scripts/hooks/host_semantics.ts +105 -37
  172. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  173. package/src/scripts/hooks/permission_gate.ts +117 -0
  174. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  175. package/src/scripts/hooks_status.ts +12 -10
  176. package/src/scripts/install.ts +57 -86
  177. package/src/scripts/install_anthropic_key.sh +2 -2
  178. package/src/scripts/lint_agent_security.ts +316 -25
  179. package/src/scripts/lint_confusables.ts +42 -6
  180. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  181. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  182. package/src/scripts/lint_handoffs.ts +0 -132
  183. package/src/scripts/lint_hidden_unicode.ts +49 -7
  184. package/src/scripts/lint_hook_manifest.ts +87 -3
  185. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  186. package/src/scripts/lint_mcp_config_security.ts +42 -6
  187. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  188. package/src/scripts/lint_readme_size.ts +158 -12
  189. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  190. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  191. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  192. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  193. package/src/scripts/mcp_server/resources.ts +41 -0
  194. package/src/scripts/mcp_server/telemetry.ts +19 -4
  195. package/src/scripts/measure_sibling_naming.ts +150 -0
  196. package/src/scripts/orchestration_record.ts +12 -1
  197. package/src/scripts/redact_hook_capture.ts +1 -1
  198. package/src/scripts/release.ts +24 -24
  199. package/src/scripts/release_publication.ts +6 -2
  200. package/src/scripts/report_skill_activation.ts +319 -7
  201. package/src/scripts/schemas/review-findings.schema.json +5 -0
  202. package/src/scripts/schemas/skill.schema.json +2 -2
  203. package/src/scripts/self_repair_cli.ts +31 -1
  204. package/src/scripts/session_canary_hook.ts +1 -1
  205. package/src/scripts/session_register_hook.ts +4 -3
  206. package/src/scripts/skill_scout.ts +69 -0
  207. package/src/scripts/trigger_eval_grandfather.json +5 -1
  208. package/src/scripts/validate_agent_settings.ts +1 -1
  209. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  210. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -55,15 +55,38 @@ import { parseHonestNull, parseMarkerLine, parseSkipDeclaration } from './check_
55
55
 
56
56
  export const EVIDENCE_ROOT = path.join('agents', 'evidence');
57
57
 
58
- /** The five types the contract defines. Anything else is an error, not a warning. */
58
+ /** The types the contract defines. Anything else is an error, not a warning. */
59
59
  export const EVIDENCE_TYPES = [
60
60
  'original-review',
61
61
  'current-binding',
62
62
  'declared-skip',
63
63
  'honest-null',
64
64
  'analysis',
65
+ 'feel',
65
66
  ] as const;
66
67
 
68
+ /**
69
+ * The closed method vocabulary a `feel` artifact must name.
70
+ *
71
+ * Closed on purpose. An evidence class nothing can emit is a vocabulary entry,
72
+ * not a control, and the cheapest way to satisfy a perceptual floor is to write
73
+ * the word "feel" and move on. Requiring one of four named methods makes the
74
+ * claim falsifiable: each says how the motion was actually looked at.
75
+ */
76
+ export const FEEL_METHODS = ['slow-motion', 'frame-step', 'device', 'next-day'] as const;
77
+ export type FeelMethod = (typeof FEEL_METHODS)[number];
78
+
79
+ const FEEL_LINE_RE = new RegExp(
80
+ String.raw`^\*\*Feel:\*\*\s+(${FEEL_METHODS.join('|')})\s+[\u2014-]\s+(\S.*)$`,
81
+ );
82
+
83
+ /** The method and outcome a `feel` line declares, or `null` when it declares neither. */
84
+ export function parseFeelLine(line: string): { method: FeelMethod; outcome: string } | null {
85
+ const m = FEEL_LINE_RE.exec(line.trim());
86
+ if (m === null) return null;
87
+ return { method: m[1] as FeelMethod, outcome: (m[2] ?? '').trim() };
88
+ }
89
+
67
90
  export type EvidenceType = (typeof EVIDENCE_TYPES)[number];
68
91
 
69
92
  /** The explicit marker, for the two types with no pre-existing grammar. */
@@ -202,6 +225,27 @@ export function checkFiles(root: string, files: readonly string[], ledger?: Gate
202
225
  });
203
226
  continue;
204
227
  }
228
+ // Risk 5 of road-to-one-motion-authority: the floor is the METHOD
229
+ // token, not the word. An outcome of `unbacked` is legal — a
230
+ // perceptual check that found nothing is still a check that ran — but
231
+ // an absent line is not, and neither is a method outside the four.
232
+ if (res.type === 'feel') {
233
+ const declared = contents
234
+ .split('\n')
235
+ .slice(0, MARKER_SCAN_LINES)
236
+ .some((l) => parseFeelLine(l) !== null);
237
+ if (!declared) {
238
+ ledger?.fail(rel, 'feel artifact with no method line');
239
+ findings.push({
240
+ file: rel,
241
+ reason:
242
+ 'declares `evidence-type: feel` but carries no `**Feel:** <method> — <outcome>` '
243
+ + `line naming one of ${FEEL_METHODS.join(' | ')}. An outcome of \`unbacked\` is legal; `
244
+ + 'an absent method is not',
245
+ });
246
+ continue;
247
+ }
248
+ }
205
249
  if (res.type === null) {
206
250
  ledger?.fail(rel, 'no evidence type declared');
207
251
  findings.push({
@@ -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');
@@ -13,8 +13,11 @@
13
13
  * - a platform binding referencing an unknown event
14
14
  * - a native_event_aliases block referencing an unknown event or platform
15
15
  * - an orphan `<platform>-dispatcher.sh` trampoline without a manifest block
16
+ * - a `host_lowering.yaml` row whose `verified.expires` has passed (or which
17
+ * was never verified) while it still carries a blocking binding
16
18
  *
17
- * Soft-warns on placeholder platform blocks and dead concerns.
19
+ * Soft-warns on placeholder platform blocks, dead concerns, an expired row
20
+ * carrying no blocking binding, and an incomplete `verified` provenance block.
18
21
  *
19
22
  * Exit codes:
20
23
  * 0 — clean (warnings allowed)
@@ -29,6 +32,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
29
32
  import { parse as parseYaml } from "yaml";
30
33
 
31
34
  import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
35
+ import { asOf } from "./_lib/as_of.js";
36
+ import { parseHostLowering, type HostLowering } from "./hooks/host_lowering.js";
32
37
  import { RE_ARM_EVENTS } from "./_lib/prefix_stable_surfaces.js";
33
38
 
34
39
  // src/scripts/lint_hook_manifest.ts → two levels up is the repo root.
@@ -44,6 +49,7 @@ const DEFAULT_MANIFEST = path.join(
44
49
  "hook_manifest.yaml",
45
50
  );
46
51
  const HOOKS_DIR = path.join(REPO_ROOT, "src", "scripts", "hooks");
52
+ const DEFAULT_HOST_LOWERING = path.join(HOOKS_DIR, "host_lowering.yaml");
47
53
 
48
54
  // Canonical event vocabulary — keep in lock-step with
49
55
  // docs/contracts/hook-architecture-v1.md and dispatch_hook.EVENT_VOCABULARY.
@@ -635,7 +641,80 @@ export function _check_roles(
635
641
  }
636
642
  }
637
643
 
638
- export function lint(manifestPath: string, strict: boolean): number {
644
+ /**
645
+ * A capability fact that cannot expire is a capability fact nobody re-checks.
646
+ *
647
+ * Three rounds of corrections to the same host claims is what this gate exists
648
+ * to stop, so the rule is mechanical rather than editorial: an unverified row —
649
+ * and an expired one is unverified, by the same definition `host_semantics`
650
+ * uses — may not carry a blocking binding. Everything softer warns, because a
651
+ * lapse on an advisory row must not red an unrelated pull request.
652
+ */
653
+ export function _check_host_lowering(
654
+ tablePath: string,
655
+ errors: string[],
656
+ warnings: string[],
657
+ today: string = asOf().toISOString().slice(0, 10),
658
+ ): void {
659
+ let table: HostLowering;
660
+ try {
661
+ table = parseHostLowering(fs.readFileSync(tablePath, "utf-8"));
662
+ } catch (exc) {
663
+ errors.push(
664
+ `host_lowering: ${tablePath} could not be read or parsed: ` +
665
+ `${exc instanceof Error ? exc.message : String(exc)}`,
666
+ );
667
+ return;
668
+ }
669
+ for (const [host, surfaces] of table) {
670
+ for (const [surface, row] of surfaces) {
671
+ const blocking = [...row.slots.entries()]
672
+ .filter(([, s]) => s.block_exit !== null)
673
+ .map(([slot]) => slot);
674
+ const expired = row.verified !== null && row.verified.expires < today;
675
+ const where = `host_lowering ${host}/${surface}`;
676
+
677
+ if (row.verified === null) {
678
+ if (blocking.length > 0) {
679
+ errors.push(
680
+ `${where}: carries a blocking binding (${blocking.join(", ")}) with \`verified: null\`. ` +
681
+ "Establish the row with a live deny probe and a dated `verified` block, or set " +
682
+ "`block_exit: null` on those slots.",
683
+ );
684
+ }
685
+ continue;
686
+ }
687
+ if (expired && blocking.length > 0) {
688
+ errors.push(
689
+ `${where}: \`verified.expires\` was ${row.verified.expires} and today is ${today}, ` +
690
+ `while the row still carries a blocking binding (${blocking.join(", ")}). ` +
691
+ "Re-establish it with a live deny probe on that host and move `expires` forward, " +
692
+ "or set `block_exit: null` on those slots — an expired row reads as unverified.",
693
+ );
694
+ } else if (expired) {
695
+ warnings.push(
696
+ `${where}: \`verified.expires\` was ${row.verified.expires} and today is ${today}. ` +
697
+ "The row carries no blocking binding, so nothing is degraded; re-establish it " +
698
+ "before attaching one.",
699
+ );
700
+ }
701
+ for (const field of ["docs_at", "host_version"] as const) {
702
+ if (row.verified[field] === null) {
703
+ warnings.push(`${where}: \`verified.${field}\` is null — the row is admissible but not fully cited.`);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ }
709
+
710
+ export function lint(
711
+ manifestPath: string,
712
+ strict: boolean,
713
+ hostLoweringPath: string = DEFAULT_HOST_LOWERING,
714
+ // Threaded so a fixture test can pin the day the verdict is read against.
715
+ // Left undefined the check resolves it through `asOf()`.
716
+ today?: string,
717
+ ): number {
639
718
  if (!_isFile(manifestPath)) {
640
719
  process.stderr.write(
641
720
  `lint_hook_manifest: file not found: ${manifestPath}\n`,
@@ -664,6 +743,7 @@ export function lint(manifestPath: string, strict: boolean): number {
664
743
  _check_aliases(manifest, errors);
665
744
  _check_orphan_trampolines(manifest, errors);
666
745
  _check_dead_concerns(concernNames, bound, warnings);
746
+ _check_host_lowering(hostLoweringPath, errors, warnings, today);
667
747
 
668
748
  for (const w of warnings) {
669
749
  process.stderr.write(`warn: ${w}\n`);
@@ -703,17 +783,21 @@ export function lint(manifestPath: string, strict: boolean): number {
703
783
 
704
784
  export function main(argv: string[]): number {
705
785
  let manifest = DEFAULT_MANIFEST;
786
+ let hostLowering = DEFAULT_HOST_LOWERING;
706
787
  let strict = false;
707
788
  for (let i = 0; i < argv.length; i += 1) {
708
789
  const a = argv[i];
709
790
  if (a === "--manifest") {
710
791
  manifest = argv[i + 1] ?? manifest;
711
792
  i += 1;
793
+ } else if (a === "--host-lowering") {
794
+ hostLowering = argv[i + 1] ?? hostLowering;
795
+ i += 1;
712
796
  } else if (a === "--strict") {
713
797
  strict = true;
714
798
  }
715
799
  }
716
- return lint(manifest, strict);
800
+ return lint(manifest, strict, hostLowering);
717
801
  }
718
802
 
719
803
  function _isCliEntry(): boolean {
@@ -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');