@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
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Where a release may START — the branch it may run from, and whether a dirty
3
+ * working tree is tolerated there.
4
+ *
5
+ * Its own module because `release.ts` sits 456 lines over the 1500-line source
6
+ * ratchet, so growing it is a gate failure by construction. `_lib/` is where
7
+ * `release_highlights.ts` and `release_material.ts` already put the predicates
8
+ * the pipeline and its gates share.
9
+ */
10
+
11
+ export interface PositionVerdict {
12
+ proceed: boolean;
13
+ /** Printed before the run continues. Never a refusal. */
14
+ notice?: string;
15
+ /** Printed on refusal (no trailing newline). */
16
+ message?: string;
17
+ }
18
+
19
+ /**
20
+ * Where a release may START, resolved as a pure verdict (no I/O, so it is
21
+ * unit-testable — same shape as `confirmGate`).
22
+ *
23
+ * THE DEADLOCK THIS REMOVES.
24
+ *
25
+ * `guard_release_curation` refuses BETWEEN step 2 and step 3: step 1 has
26
+ * already created and checked out `release/{target}`, step 2 has already
27
+ * written the bump, the changelog section and every regenerated derived file,
28
+ * and nothing is committed yet. Its message says to curate the head and re-run
29
+ * `task release`. Measured on 14.19.0, neither re-run reached step 1:
30
+ *
31
+ * - `task release` → `release must run from 'main'` (this check)
32
+ * - `task release --resume` → `working tree is not clean` (this check)
33
+ *
34
+ * So the guard prescribed a remedy the preflight refused, in both spellings,
35
+ * and the only way through was to hand-craft the `release: X.Y.Z` commit the
36
+ * pipeline makes for itself one step later.
37
+ *
38
+ * `docs/release-runbook.md` already claimed this was closed on 2026-09-03 and
39
+ * the drill pins `plain-run-reuses-an-existing-branch` for it. Both were true
40
+ * of the fix that landed — in `checkout_release_branch`, which is STEP 1. The
41
+ * preflight runs before step 1, so from the release branch the run never
42
+ * arrived at the code that had been repaired. The claim was not wrong about
43
+ * step 1; it was wrong that step 1 was reachable from that position.
44
+ *
45
+ * Why `release/{target}` is a legitimate start, with or without `--resume`:
46
+ *
47
+ * It is the position the pipeline itself leaves behind. `--resume` remains
48
+ * meaningful for what it was always for — skipping a COMMIT, a PR, a tag or a
49
+ * Release that already exists — and the `!resume` tag-exists check in
50
+ * `preflight` still refuses a plain re-run of an already-tagged version.
51
+ *
52
+ * Why a dirty tree is accepted THERE and nowhere else:
53
+ *
54
+ * On `release/{target}` the dirt is this pipeline's own step-2 output, and
55
+ * step 3 sweeps it with `git add -A` regardless. Refusing it can therefore
56
+ * only ever fire against the tool's own state. On `main` the same tree is an
57
+ * operator's unrelated work about to be swept into a release commit, so the
58
+ * refusal stays exactly as it was. The accepted files are PRINTED rather than
59
+ * absorbed silently, and `check_release_pr_shape` still refuses anything
60
+ * outside the version-bump allowlist on the resulting PR — two nets under the
61
+ * one place this relaxes.
62
+ */
63
+ export function preflightPosition(opts: {
64
+ branch: string;
65
+ mainBranch: string;
66
+ releaseBranch: string;
67
+ porcelain: string;
68
+ }): PositionVerdict {
69
+ const { branch, mainBranch, releaseBranch, porcelain } = opts;
70
+ if (branch !== mainBranch && branch !== releaseBranch) {
71
+ return {
72
+ proceed: false,
73
+ message:
74
+ `release must run from '${mainBranch}' or '${releaseBranch}', ` +
75
+ `currently on '${branch}'`,
76
+ };
77
+ }
78
+ if (!porcelain.trim()) {
79
+ return { proceed: true };
80
+ }
81
+ if (branch === mainBranch) {
82
+ return { proceed: false, message: 'working tree is not clean; commit or stash first' };
83
+ }
84
+ return {
85
+ proceed: true,
86
+ notice:
87
+ `working tree is not clean, and on '${releaseBranch}' that is this pipeline's own ` +
88
+ 'step-2 output — step 3 commits it with `git add -A`. Files that will be swept ' +
89
+ 'into the release commit:\n' +
90
+ porcelain
91
+ .split('\n')
92
+ .filter((l) => l.trim())
93
+ .map((l) => ` ${l}`)
94
+ .join('\n'),
95
+ };
96
+ }
@@ -34,6 +34,7 @@
34
34
  */
35
35
  export type MutationMeasure = 'exact' | 'capped_approximation';
36
36
 
37
+ import { appliedIds } from './audit_field_provenance.js';
37
38
  import type { NoFreeForm } from './runtime_journal.js';
38
39
  import { type PrivacyClass } from './privacy_class.js';
39
40
 
@@ -55,6 +56,20 @@ export interface ReviewSkippedInput {
55
56
  /** Stable id (ULID, UUID, or content hash); caller supplies. */
56
57
  id: string;
57
58
  work_id?: string | undefined;
59
+ /**
60
+ * Stable rule ids the run ACTUALLY carried. Absent means `[]`, an honest
61
+ * absence — this writer observes a review that did NOT happen, so it
62
+ * usually has no rules observation to offer in either direction.
63
+ *
64
+ * It wrote the literal `['delegation-policy']` on every line until
65
+ * 2026-09-07, which made the whole audit stream's most frequent "pattern" a
66
+ * property of the writer. See `_lib/audit_field_provenance.ts` for the
67
+ * measurement, and note the difference from `skills_applied` below: that
68
+ * field is OMITTED because audit-log-v1 lets absence mean "not recorded",
69
+ * while `rules_applied` is mandatory on every line and `[]` is how absence
70
+ * is spelled there.
71
+ */
72
+ rules_applied?: string[] | null | undefined;
58
73
  }
59
74
 
60
75
  export interface BuiltReviewSkippedLine {
@@ -102,7 +117,8 @@ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSk
102
117
  risk_class: input.diff_lines >= HIGH_RISK_DIFF_LINES ? 'high' : 'medium',
103
118
  memory: { asks: 0, hits: 0 },
104
119
  verify: { claims: 0, first_try_passes: 0 },
105
- rules_applied: ['delegation-policy'],
120
+ // COMPUTED, never a producer constant.
121
+ rules_applied: appliedIds(input.rules_applied),
106
122
  // MANDATORY on every audit line, same as the orchestration producer.
107
123
  privacy_class: PRODUCER_PRIVACY_CLASS,
108
124
  // `skills_applied` is OMITTED here, deliberately and not by oversight.
@@ -174,6 +174,8 @@ import type { DatabaseSync } from 'node:sqlite';
174
174
  import { current_branch, git_common_dir, git_dir } from './git_common_dir.js';
175
175
  import type { TerminalState } from './outcome_envelope.js';
176
176
  import { readRunTerminalState, RUN_TERMINAL_STATES } from './outcome_vocabularies.js';
177
+ import { addColumnIfMissing } from './sqlite_schema.js';
178
+ import { readSurface, type Surface } from './surface.js';
177
179
  import {
178
180
  isSqliteAvailableSync,
179
181
  loadSqliteSync,
@@ -408,6 +410,14 @@ export interface JournalEvent {
408
410
  at: string;
409
411
  /** Bounded identifier: the hook or command name. Never free text. */
410
412
  capability: string;
413
+ /**
414
+ * IDE / CLI / cloud dimension of the dispatch. Closed vocabulary, and
415
+ * almost always `unknown`: no host observed here sends a distinguishing
416
+ * marker and the detector refuses to infer one. Recorded anyway because
417
+ * "did this come from the surface the lattice assumed" has no answer at
418
+ * all while the field is absent.
419
+ */
420
+ surface: Surface;
411
421
  /** Imported from `outcome_envelope.ts`; null until an episode terminates. */
412
422
  terminal_state: TerminalState | null;
413
423
  /** Repo-relative locator for the return. Never an absolute path. */
@@ -447,6 +457,7 @@ export const JOURNAL_RECORD_KEYS = Object.freeze([
447
457
  'worktree_id',
448
458
  'at',
449
459
  'capability',
460
+ 'surface',
450
461
  'terminal_state',
451
462
  'return_ref',
452
463
  'verification_ref',
@@ -759,6 +770,7 @@ function createSchema(db: DatabaseSync): void {
759
770
  worktree_id TEXT NOT NULL,
760
771
  at TEXT NOT NULL,
761
772
  capability TEXT NOT NULL,
773
+ surface TEXT NOT NULL DEFAULT 'unknown',
762
774
  terminal_state TEXT,
763
775
  return_ref TEXT,
764
776
  verification_ref TEXT,
@@ -767,6 +779,9 @@ function createSchema(db: DatabaseSync): void {
767
779
  amends_seq INTEGER
768
780
  )`,
769
781
  );
782
+ // A pre-existing row takes the default, which is the honest value: that
783
+ // dispatch was never asked what surface it came from.
784
+ addColumnIfMissing(db, 'journal_event', 'surface', "TEXT NOT NULL DEFAULT 'unknown'");
770
785
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_episode ON journal_event(repository_id, episode_id, seq)');
771
786
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_session ON journal_event(repository_id, session_id, seq)');
772
787
  db.exec('CREATE INDEX IF NOT EXISTS idx_event_task ON journal_event(repository_id, task_id)');
@@ -984,6 +999,8 @@ export interface RecordEventInput {
984
999
  prompt_id?: string | null;
985
1000
  episode_id?: string | null;
986
1001
  terminal_state?: TerminalState | null;
1002
+ /** Closed vocabulary; anything else (including absent) records `unknown`. */
1003
+ surface?: Surface | null;
987
1004
  return_ref?: string | null;
988
1005
  verification_ref?: string | null;
989
1006
  consumption?: Consumption | null;
@@ -1057,6 +1074,10 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1057
1074
  worktree_id,
1058
1075
  at,
1059
1076
  capability: input.capability,
1077
+ // Tolerant by design: a surface this build cannot place is `unknown`,
1078
+ // never a throw. The field answers "which surface" and must not be able
1079
+ // to fail a write that would otherwise have succeeded.
1080
+ surface: readSurface(input.surface),
1060
1081
  terminal_state: input.terminal_state ?? null,
1061
1082
  return_ref: requireRef('return_ref', input.return_ref),
1062
1083
  verification_ref: requireRef('verification_ref', input.verification_ref),
@@ -1070,9 +1091,9 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1070
1091
  `INSERT INTO journal_event
1071
1092
  (event, episode_id, session_id, task_id, prompt_id, boundary_status,
1072
1093
  boundary_rule_version, repository_id, worktree_id, at, capability,
1073
- terminal_state, return_ref, verification_ref, consumption, retain_until,
1074
- amends_seq)
1075
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1094
+ surface, terminal_state, return_ref, verification_ref, consumption,
1095
+ retain_until, amends_seq)
1096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1076
1097
  )
1077
1098
  .run(
1078
1099
  row.event,
@@ -1086,6 +1107,7 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
1086
1107
  row.worktree_id,
1087
1108
  row.at,
1088
1109
  row.capability,
1110
+ row.surface,
1089
1111
  row.terminal_state,
1090
1112
  row.return_ref,
1091
1113
  row.verification_ref,
@@ -1111,6 +1133,7 @@ function toEvent(r: Record<string, unknown>): JournalEvent {
1111
1133
  worktree_id: String(r['worktree_id']),
1112
1134
  at: String(r['at']),
1113
1135
  capability: String(r['capability']),
1136
+ surface: readSurface(r['surface']),
1114
1137
  // Tolerant, not a cast: a value this build cannot place reads as null.
1115
1138
  terminal_state: readRunTerminalState(r['terminal_state']),
1116
1139
  return_ref: (r['return_ref'] as string | null) ?? null,
@@ -28,6 +28,10 @@ import process from 'node:process';
28
28
  import { parse as parseYaml } from 'yaml';
29
29
 
30
30
  import { inheritedGitOverrides } from './repo_root.js';
31
+ import {
32
+ checkHostPermissionSettings,
33
+ defaultHostPermissionDeps,
34
+ } from './host_permission_checks.js';
31
35
 
32
36
  /** The verdict shape `cmd_doctor` renders. Mirrors its internal `Dict` rows. */
33
37
  export interface WiringCheck {
@@ -291,7 +295,7 @@ export function checkInheritedGitEnv(env: NodeJS.ProcessEnv = process.env): Wiri
291
295
  * two spreads.
292
296
  */
293
297
  /** The four ids, in render order. Spread into `cmd_doctor`'s two id registries. */
294
- export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch'] as const;
298
+ export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch', 'host-permission-settings'] as const;
295
299
 
296
300
 
297
301
  /**
@@ -333,6 +337,7 @@ export function checkHooksDisabled(env: NodeJS.ProcessEnv = process.env): Wiring
333
337
 
334
338
  export function wiringRunners(opts: {
335
339
  packageRoot: string;
340
+ projectRoot: string;
336
341
  iterOverrides: () => Iterable<[string, unknown, string]>;
337
342
  }): Record<string, () => WiringCheck> {
338
343
  return {
@@ -344,6 +349,12 @@ export function wiringRunners(opts: {
344
349
  'hook-resolution': () => checkHookResolution(opts.packageRoot).check,
345
350
  'inherited-git-env': () => checkInheritedGitEnv(),
346
351
  'hooks-kill-switch': () => checkHooksDisabled(),
352
+ // The host's own permission settings, which decide how often a call is
353
+ // confirmed. It lives with this family rather than its own because
354
+ // `checkHookWiring` below already reads the same `~/.claude/settings.json`
355
+ // for the same host — the question is the same surface, one key over.
356
+ 'host-permission-settings': () =>
357
+ checkHostPermissionSettings(defaultHostPermissionDeps(opts.projectRoot)),
347
358
  };
348
359
  }
349
360
 
@@ -13,8 +13,12 @@
13
13
  // skipped by every check. Grep-auditable, scoped to the block.
14
14
  // 2. Confidence weighting — a match in a doc / example / template / evals
15
15
  // file scores at 0.25x; below the FAIL threshold it is a WARN.
16
- // 3. Per-file pragma — `<!-- security-lint: allow <check> "<reason>" -->`
17
- // anywhere in the file suppresses one check for that file.
16
+ // 3. Bound pragma — `<!-- security-lint: allow <check> "<reason>" sha256:<hex> -->`
17
+ // anywhere in the file suppresses ONE check for the specific evidence whose
18
+ // fingerprint is listed. Repeat `sha256:<hex>` once per accepted match. The
19
+ // unbound form (no hash) still suppresses the whole file and reports itself
20
+ // as a `legacy-pragma` finding, because a key lookup with no content
21
+ // binding accepts whatever that line is later changed to say.
18
22
  //
19
23
  // There is no global allowlist — that is the rejected pattern.
20
24
  //
@@ -27,6 +31,7 @@
27
31
  // - `report()` reproduces the glyphs (🔴 / ⚠️), the `(-rank, path, line)`
28
32
  // sort, the weight note `(weight 0.25)`, and every printed string verbatim.
29
33
 
34
+ import { createHash } from 'node:crypto';
30
35
  import * as fs from 'node:fs';
31
36
  import * as path from 'node:path';
32
37
  import { fileURLToPath } from 'node:url';
@@ -61,11 +66,18 @@ export const DEFAULT_SCAN_ROOTS: readonly string[] = [
61
66
  const _EXAMPLE_PATH =
62
67
  /(^|\/)(docs|evals|tests?|fixtures?)(\/|$)|example|template|sample|\/_template/i;
63
68
 
64
- // Python: re.compile(r'<!--\s*security-lint:\s*allow\s+(?P<check>[\w-]+)\s+"(?P<reason>[^"]+)"\s*-->')
69
+ // Grammar: `<!-- security-lint: allow <check> "<reason>" [sha256:<hex>]... -->`
65
70
  // JS `\w` is ASCII-only by default (matches Python re's `[\w-]` for the ASCII
66
71
  // check ids used here). Global flag so finditer-style iteration works.
72
+ //
73
+ // The hash group is OPTIONAL and repeatable. Optional because the unbound form
74
+ // must keep working — a grammar change that invalidates every existing pragma
75
+ // at once turns a hardening into an outage, and the migration is what removes
76
+ // the unbound population, not the parser. Repeatable because one file can
77
+ // legitimately carry several accepted matches of the same check, and a single
78
+ // hash would force either a second pragma line or a whole-file fallback.
67
79
  const _PRAGMA =
68
- /<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"\s*-->/g;
80
+ /<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"((?:\s+sha256:[0-9a-f]{64})*)\s*-->/g;
69
81
 
70
82
  // Python: re.compile(r"^(\s*)(`{3,}|~{3,})\s*([\w-]*)\s*$")
71
83
  const _FENCE = /^(\s*)(`{3,}|~{3,})\s*([A-Za-z0-9_-]*)\s*$/;
@@ -136,6 +148,21 @@ export function path_weight(rel_path: string): number {
136
148
  return is_example_path(rel_path) ? 0.25 : 1.0;
137
149
  }
138
150
 
151
+ const PROJECTION_PREFIX = 'dist/agent-src/';
152
+
153
+ /**
154
+ * Fold a projected path back onto the source it was copied from.
155
+ *
156
+ * `dist/agent-src/` is a byte-for-byte copy of `src/` with paths rewritten
157
+ * (ADR-201), so an artifact and its projection are the same artifact for the
158
+ * purpose of "which evidence did a human accept". Two identities would mean two
159
+ * fingerprints for one accepted line, and only one of them could be written
160
+ * into the pragma the projection copies verbatim.
161
+ */
162
+ export function source_identity(rel: string): string {
163
+ return rel.startsWith(PROJECTION_PREFIX) ? `src/${rel.slice(PROJECTION_PREFIX.length)}` : rel;
164
+ }
165
+
139
166
  /** A file pre-split into lines with a fence/pragma mask the linters reuse. */
140
167
  export class ScannedFile {
141
168
  path: string; // absolute or as-given path
@@ -145,6 +172,8 @@ export class ScannedFile {
145
172
  in_example_fence: boolean[];
146
173
  in_any_fence: boolean[];
147
174
  pragmas: Record<string, string>; // check id → reason
175
+ /** check id → the sha256 fingerprints that pragma accepts. Empty = unbound. */
176
+ pragma_hashes: Record<string, readonly string[]>;
148
177
  weight: number;
149
178
 
150
179
  constructor(
@@ -155,6 +184,7 @@ export class ScannedFile {
155
184
  in_any_fence: boolean[],
156
185
  pragmas: Record<string, string>,
157
186
  weight: number,
187
+ pragma_hashes: Record<string, readonly string[]> = {},
158
188
  ) {
159
189
  this.path = pathArg;
160
190
  this.rel = rel;
@@ -163,12 +193,82 @@ export class ScannedFile {
163
193
  this.in_any_fence = in_any_fence;
164
194
  this.pragmas = pragmas;
165
195
  this.weight = weight;
196
+ this.pragma_hashes = pragma_hashes;
166
197
  }
167
198
 
168
199
  pragma_allows(check: string): boolean {
169
200
  return Object.prototype.hasOwnProperty.call(this.pragmas, check);
170
201
  }
171
202
 
203
+ /**
204
+ * Which form of pragma this file carries for `check`.
205
+ *
206
+ * `absent` — no pragma; scan normally.
207
+ * `legacy` — a pragma with no fingerprint. Suppresses the whole file, as it
208
+ * always did, and says so: a key lookup accepts whatever the
209
+ * line is later changed to say, which is the defect being paid
210
+ * down, not a mode anybody should choose.
211
+ * `bound` — a pragma listing fingerprints. Suppresses exactly the matches
212
+ * whose evidence hashes to one of them; anything else survives.
213
+ */
214
+ pragma_form(check: string): 'absent' | 'legacy' | 'bound' {
215
+ if (!this.pragma_allows(check)) return 'absent';
216
+ return (this.pragma_hashes[check] ?? []).length > 0 ? 'bound' : 'legacy';
217
+ }
218
+
219
+ /**
220
+ * The fingerprint one finding's evidence hashes to.
221
+ *
222
+ * WHAT IS HASHED, and each part earns its place:
223
+ * check id — the same line may legitimately be accepted for one check
224
+ * and not another.
225
+ * rel path — the location IDENTITY. Not the line NUMBER: an unrelated
226
+ * insert above would otherwise break every pragma in the
227
+ * file, and a suppression that breaks on unrelated edits
228
+ * gets deleted rather than re-derived. A `dist/agent-src/`
229
+ * path folds to its `src/` original first, because the
230
+ * projection is byte-exact by contract (ADR-201) and
231
+ * carries the pragma verbatim — without the fold, every
232
+ * migrated pragma would suppress the source and leave its
233
+ * own copy flagged, which is a gate that reds on a file
234
+ * nobody may edit.
235
+ * matched line, — the evidence. ASCII whitespace runs collapse so a reflow
236
+ * normalized does not invalidate the binding, `sha256:<hex>` tokens are
237
+ * removed, and NOTHING else is stripped: the zero-width
238
+ * characters this suite hunts are exactly what a wider
239
+ * normalizer would erase.
240
+ *
241
+ * WHY THE HASH TOKENS COME OUT. A pragma's own reason string can be the
242
+ * matched evidence — two in this tree are, because they quote the very
243
+ * phrases their check detects in order to explain themselves. Without this
244
+ * the fingerprint would have to be computed over a line that already
245
+ * contains it, which has no fixed point: writing the hash changes the line,
246
+ * which changes the hash. Stripping the tokens makes the fingerprint stable
247
+ * under the act of recording it, and costs nothing elsewhere — a `sha256:`
248
+ * token in ordinary prose is not evidence of anything a linter here checks.
249
+ *
250
+ * Deliberately NOT the whole file: incidental content elsewhere would make
251
+ * every unrelated edit re-fire the pragma, which is the noise that produces
252
+ * blanket suppressions.
253
+ */
254
+ evidence_fingerprint(finding: Finding): string {
255
+ const text = finding.line > 0 ? (this.lines[finding.line - 1] ?? '') : '';
256
+ const normalized = text
257
+ .replace(/\s*sha256:[0-9a-f]{64}/g, '')
258
+ .replace(/[ \t]+/g, ' ')
259
+ .trim();
260
+ return createHash('sha256')
261
+ .update(`${finding.check}\n${source_identity(this.rel)}\n${normalized}`, 'utf-8')
262
+ .digest('hex');
263
+ }
264
+
265
+ /** Drop the findings this file's BOUND pragma for `check` accepts. */
266
+ filter_bound_pragma(check: string, findings: readonly Finding[]): Finding[] {
267
+ const accepted = new Set(this.pragma_hashes[check] ?? []);
268
+ if (accepted.size === 0) return [...findings];
269
+ return findings.filter((f) => !accepted.has(this.evidence_fingerprint(f)));
270
+ }
271
+
172
272
  /** Yield [lineno, text] honouring the fence masks (mirrors iter_lines). */
173
273
  *iter_lines(
174
274
  opts: { skip_example_fence?: boolean; skip_any_fence?: boolean } = {},
@@ -286,11 +386,21 @@ export function scan_file(filePath: string): ScannedFile {
286
386
  // Pragmas are explicit, grep-auditable opt-out markers — honour them
287
387
  // anywhere in the file.
288
388
  const pragmas: Record<string, string> = {};
389
+ const pragmaHashes: Record<string, readonly string[]> = {};
289
390
  for (const text of lines) {
290
391
  _PRAGMA.lastIndex = 0;
291
392
  let m: RegExpExecArray | null;
292
393
  while ((m = _PRAGMA.exec(text)) !== null) {
293
- pragmas[m[1] as string] = m[2] as string;
394
+ const check = m[1] as string;
395
+ pragmas[check] = m[2] as string;
396
+ const hashes = (m[3] ?? '').match(/sha256:([0-9a-f]{64})/g) ?? [];
397
+ // Union across repeated pragmas for one check rather than replace:
398
+ // two lines each accepting one match is a legitimate spelling, and
399
+ // a last-one-wins read would silently drop the first.
400
+ pragmaHashes[check] = [
401
+ ...(pragmaHashes[check] ?? []),
402
+ ...hashes.map((h) => h.slice('sha256:'.length)),
403
+ ];
294
404
  if (m.index === _PRAGMA.lastIndex) {
295
405
  _PRAGMA.lastIndex += 1;
296
406
  }
@@ -305,6 +415,7 @@ export function scan_file(filePath: string): ScannedFile {
305
415
  in_any,
306
416
  pragmas,
307
417
  path_weight(rel),
418
+ pragmaHashes,
308
419
  );
309
420
  }
310
421
 
@@ -330,6 +441,7 @@ export function scan_path(filePath: string, base: string): ScannedFile {
330
441
  sf.in_any_fence,
331
442
  sf.pragmas,
332
443
  path_weight(rel),
444
+ sf.pragma_hashes,
333
445
  );
334
446
  }
335
447
 
@@ -379,12 +491,23 @@ function _pathCmp(a: string, b: string): number {
379
491
  }
380
492
 
381
493
  /** Yield ScannedFile for every matching file under the given roots. */
494
+ /**
495
+ * Walk a corpus.
496
+ *
497
+ * `scanBase` exists for the bounded-root mode every child linter now carries
498
+ * (`--root DIR`): the roots stay relative, the base they resolve against moves.
499
+ * Absent, it is the package root and the walk is exactly what it always was —
500
+ * `scan_path(p, ROOT)` computes the same `rel` and the same `path_weight` as the
501
+ * `scan_file(p)` this used to call, so a flagless run reads the identical corpus
502
+ * with identical finding paths.
503
+ */
382
504
  export function* iter_corpus(
383
505
  roots: readonly string[] = DEFAULT_SCAN_ROOTS,
384
506
  exts: readonly string[] = ['.md'],
507
+ scanBase: string = ROOT,
385
508
  ): Generator<ScannedFile> {
386
509
  for (const root of roots) {
387
- const base = path.join(ROOT, root);
510
+ const base = path.join(scanBase, root);
388
511
  if (!fs.existsSync(base)) {
389
512
  continue;
390
513
  }
@@ -396,12 +519,61 @@ export function* iter_corpus(
396
519
  continue;
397
520
  }
398
521
  if (st.isFile() && exts.includes(path.extname(p))) {
399
- yield scan_file(p);
522
+ yield scan_path(p, scanBase);
400
523
  }
401
524
  }
402
525
  }
403
526
  }
404
527
 
528
+ /**
529
+ * The prefix a child linter uses to publish what it inspected, under `--json`.
530
+ *
531
+ * WHY STDERR, AND WHY ONLY UNDER `--json`. Under `--json` a child's STDOUT is a
532
+ * findings array with one consumer — `lint_agent_security`, which parses it.
533
+ * A count printed there would corrupt the payload. Stderr is free on that path
534
+ * and already carries the dead-scope diagnostic, so the umbrella reads the
535
+ * count from the same stream it would read a failure on.
536
+ *
537
+ * The umbrella SUMS these across its five children and publishes the total as
538
+ * its own `scanned:` line. The sum counts artifact INSPECTIONS, not distinct
539
+ * artifacts: a file in more than one child's corpus is counted once per child
540
+ * that read it. That is deliberate and is the stronger collapse detector — a
541
+ * distinct-union count would stay high when one child's corpus moved, as long
542
+ * as another child still covered those paths, which is exactly the failure
543
+ * `gate-coverage.yml` exists to catch.
544
+ */
545
+ /** The check id an unbound pragma reports itself under. */
546
+ export const LEGACY_PRAGMA_CHECK = 'legacy-pragma';
547
+
548
+ /**
549
+ * The finding an unbound pragma emits about itself.
550
+ *
551
+ * LOW, so it never blocks: the unbound form is legal and this is a
552
+ * migration signal, not a violation. It is a FINDING rather than a printed
553
+ * aside so it travels every path the linters already have — the human report,
554
+ * the `--json` payload the umbrella aggregates, and the SARIF file — without
555
+ * a second reporting channel that only one of them reads.
556
+ */
557
+ export function legacy_pragma_finding(sf: ScannedFile, check: string): Finding {
558
+ return new Finding(
559
+ sf.rel,
560
+ 0,
561
+ LEGACY_PRAGMA_CHECK,
562
+ 'LOW',
563
+ `unbound \`security-lint: allow ${check}\` pragma suppresses this whole file — ` +
564
+ 'it accepts whatever the matched text is later changed to say. Bind it with ' +
565
+ '`sha256:<hex>` per accepted match.',
566
+ sf.weight,
567
+ );
568
+ }
569
+
570
+ export const CHILD_SCANNED_PREFIX = 'scanned: ';
571
+
572
+ /** Publish a child linter's inspected count on stderr. See {@link CHILD_SCANNED_PREFIX}. */
573
+ export function report_child_scanned(count: number): void {
574
+ process.stderr.write(`${CHILD_SCANNED_PREFIX}${String(count)}\n`);
575
+ }
576
+
405
577
  /**
406
578
  * Print findings grouped by severity; return an exit code.
407
579
  *