@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
@@ -30,14 +30,27 @@ import { createHash } from 'node:crypto';
30
30
  import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
31
31
  import { is_kernel_rule } from './kernel_rules.js';
32
32
 
33
- /** Where a record came from. */
34
- export type DefectSource = 'user-reported' | 'self-detected';
33
+ /**
34
+ * Where a record came from.
35
+ *
36
+ * `user-reported` and `self-detected` both require something to have FIRED — a
37
+ * user complaint, or a deterministic detector. `model-noticed` is the third
38
+ * intake: a model notices an edge case in a rule, or a methodology the user
39
+ * just explained that no skill carries. Nothing fired, and before this source
40
+ * existed such an observation had no record type at all and was handed to a
41
+ * host-native memory tool no reader of this repository can query.
42
+ *
43
+ * It is a SOURCE and not a second store: the record shape, the fingerprint
44
+ * derivation, the noclobber path and the creation cap are all unchanged.
45
+ */
46
+ export type DefectSource = 'user-reported' | 'self-detected' | 'model-noticed';
35
47
 
36
48
  /** The defect classes a record can carry. One class = one detector. */
37
49
  export type DefectClass =
38
50
  | 'user-reported'
39
51
  | 'council-availability-claim'
40
- | 'language-mirror';
52
+ | 'language-mirror'
53
+ | 'model-noticed';
41
54
 
42
55
  /**
43
56
  * Runtime mirror of `DefectClass`, in declaration order. The upstream issue
@@ -49,6 +62,7 @@ export const DEFECT_CLASSES: readonly DefectClass[] = [
49
62
  'user-reported',
50
63
  'council-availability-claim',
51
64
  'language-mirror',
65
+ 'model-noticed',
52
66
  ];
53
67
 
54
68
  /** How far a record may travel outward. */
@@ -61,14 +75,94 @@ export interface DefectFinding {
61
75
  evidence: string;
62
76
  /** One line naming what the fix should change. */
63
77
  suggested_surface: string;
78
+ /**
79
+ * The assets this record is ABOUT, drawn from a closed vocabulary and
80
+ * resolved against the tree at write time (see `partitionTargets`).
81
+ *
82
+ * `suggested_surface` is free text, so no record could ever be JOINED to
83
+ * the asset it describes — two records about one rule's weakness were two
84
+ * unrelated strings. A target is machine-addressable, which is what makes
85
+ * the per-target occurrence count in `targetCounts` possible at all.
86
+ *
87
+ * An EMPTY list stays legal: a real observation with no identified home is
88
+ * information, not an error. That is deliberately different from an
89
+ * UNRESOLVABLE entry, which lands in `proposes` instead.
90
+ */
91
+ target?: string[];
92
+ /**
93
+ * Target-shaped strings that do not resolve in the tree — a rule that does
94
+ * not exist yet, a skill the record is proposing. Kept rather than
95
+ * rejected, because "this should exist" is a legitimate observation; kept
96
+ * SEPARATE from `target`, because a silently accepted unresolvable id is
97
+ * worse than free text — it looks joined and joins to nothing.
98
+ */
99
+ proposes?: string[];
64
100
  }
65
101
 
102
+ /** The four asset kinds a `target` entry may address. */
103
+ export const TARGET_KINDS = ['rule', 'skill', 'command', 'hook'] as const;
104
+ export type TargetKind = (typeof TARGET_KINDS)[number];
105
+
106
+ /** `<kind>:<id>` — closed prefix, id-shaped remainder. Never free text. */
107
+ export const TARGET_RE = /^(rule|skill|command|hook):([a-z0-9][a-z0-9._-]*)$/;
108
+
109
+ /** Shape only — says nothing about whether the id exists. */
110
+ export function isWellFormedTarget(value: string): boolean {
111
+ return TARGET_RE.test(value);
112
+ }
113
+
114
+ /** Split a well-formed target into its kind and id, or null. */
115
+ export function parseTarget(value: string): { kind: TargetKind; id: string } | null {
116
+ const m = TARGET_RE.exec(value);
117
+ return m === null ? null : { kind: m[1] as TargetKind, id: m[2] as string };
118
+ }
119
+
120
+ /**
121
+ * What a record is doing now.
122
+ *
123
+ * The pair `open | released` could express neither a DECLINED record (which
124
+ * stayed `open` in the queue line for ever) nor a partially actioned one, so
125
+ * the only way a record left the queue was to be released. Six states, closed:
126
+ *
127
+ * - `open` — queued, nothing decided.
128
+ * - `candidate` — accepted as worth acting on, not yet acted on.
129
+ * - `actioned` — a fix or a decision landed.
130
+ * - `declined` — deliberately not acted on. Terminal, and NOT a failure.
131
+ * - `superseded` — another record covers it.
132
+ * - `parked` — waiting on a named condition; REQUIRES `parked_until`,
133
+ * because a park with no wake condition is a deferral
134
+ * wearing a condition's clothes.
135
+ */
136
+ export type DefectStatus =
137
+ | 'open'
138
+ | 'candidate'
139
+ | 'actioned'
140
+ | 'declined'
141
+ | 'superseded'
142
+ | 'parked';
143
+
144
+ export const DEFECT_STATUSES: readonly DefectStatus[] = [
145
+ 'open',
146
+ 'candidate',
147
+ 'actioned',
148
+ 'declined',
149
+ 'superseded',
150
+ 'parked',
151
+ ];
152
+
153
+ /** Statuses that keep a record in the queue line. */
154
+ export const ACTIVE_STATUSES: readonly DefectStatus[] = ['open', 'candidate'];
155
+
66
156
  export interface DefectRecord extends DefectFinding {
67
157
  fingerprint: string;
68
158
  first_seen: string;
69
159
  last_seen: string;
70
160
  occurrences: number;
71
- status: 'open' | 'released';
161
+ status: DefectStatus;
162
+ /** MANDATORY when `status === 'parked'`: when the record comes back. */
163
+ parked_until?: string;
164
+ /** One line saying why the record reached a terminal status. */
165
+ resolution?: string;
72
166
  /**
73
167
  * Failed egress attempts from the most recent `release` call, sanitized at
74
168
  * write time. Local-only bookkeeping: `renderReport` never includes them,
@@ -617,6 +711,38 @@ export function creationCapReached(
617
711
  return recentCreations(existing, finding.source, now) >= NEW_RECORDS_PER_SOURCE_PER_WINDOW;
618
712
  }
619
713
 
714
+ /**
715
+ * Shape validation for a stored record. Returns the problems, `[]` when valid.
716
+ *
717
+ * The one obligation that is NOT a style nit: a `parked` record without a
718
+ * `parked_until` is a deferral nothing can wake. Everything else here is the
719
+ * closed-vocabulary check the free-text era could not perform.
720
+ */
721
+ export function validateRecord(record: DefectRecord): string[] {
722
+ const out: string[] = [];
723
+ if (!(DEFECT_STATUSES as readonly string[]).includes(record.status)) {
724
+ out.push(`status '${String(record.status)}' is outside the closed enum`);
725
+ }
726
+ if (record.status === 'parked' && (record.parked_until ?? '').trim() === '') {
727
+ out.push('status is `parked` but `parked_until` is missing — a park with no wake condition is a deferral, not a park');
728
+ }
729
+ for (const value of record.target ?? []) {
730
+ if (!isWellFormedTarget(value)) {
731
+ out.push(`target '${value}' is not \`<rule|skill|command|hook>:<id>\``);
732
+ }
733
+ }
734
+ return out;
735
+ }
736
+
737
+ /** Union of two id lists, order-stable and de-duplicated. */
738
+ function _mergeIds(a: readonly string[] | undefined, b: readonly string[] | undefined): string[] {
739
+ const out: string[] = [];
740
+ for (const v of [...(a ?? []), ...(b ?? [])]) {
741
+ if (!out.includes(v)) out.push(v);
742
+ }
743
+ return out;
744
+ }
745
+
620
746
  export function mergeRecord(
621
747
  existing: DefectRecord | null,
622
748
  finding: DefectFinding,
@@ -624,13 +750,22 @@ export function mergeRecord(
624
750
  ): DefectRecord {
625
751
  const fp = fingerprint(finding.defect_class, finding.evidence);
626
752
  if (existing !== null && existing.fingerprint === fp) {
627
- return {
753
+ const target = _mergeIds(existing.target, finding.target);
754
+ const proposes = _mergeIds(existing.proposes, finding.proposes);
755
+ const merged: DefectRecord = {
628
756
  ...existing,
629
757
  last_seen: now,
630
758
  occurrences: existing.occurrences + 1,
631
- // A record the user re-reports after a release is open again.
759
+ // A record re-reported after it left the queue is open again. The
760
+ // widened enum does not change this: a recurrence refutes whatever
761
+ // disposition was taken, which is exactly `recurring-criticism`.
632
762
  status: 'open',
763
+ ...(target.length > 0 ? { target } : {}),
764
+ ...(proposes.length > 0 ? { proposes } : {}),
633
765
  };
766
+ // `open` carries no wake condition; a stale one would read as a park.
767
+ delete merged.parked_until;
768
+ return merged;
634
769
  }
635
770
  return {
636
771
  ...finding,
@@ -12,12 +12,16 @@ import * as fs from 'node:fs';
12
12
  import * as path from 'node:path';
13
13
 
14
14
  import {
15
+ ACTIVE_STATUSES,
15
16
  creationCapReached,
16
17
  type DefectFinding,
17
18
  type DefectRecord,
18
19
  type DefectSource,
19
20
  fingerprint,
21
+ isWellFormedTarget,
20
22
  mergeRecord,
23
+ parseTarget,
24
+ validateRecord,
21
25
  } from './self_repair.js';
22
26
 
23
27
  export const STORE_REL = path.join('agents', 'runtime', 'self-repair');
@@ -48,7 +52,16 @@ export function readRecord(root: string, fp: string): DefectRecord | null {
48
52
  }
49
53
  }
50
54
 
55
+ /**
56
+ * Write one record. VALIDATES first — a `parked` record with no wake condition
57
+ * or a malformed target never reaches the store, because a store that accepts
58
+ * an unwakeable park has the same defect the park was supposed to fix.
59
+ */
51
60
  export function writeRecord(root: string, record: DefectRecord): void {
61
+ const problems = validateRecord(record);
62
+ if (problems.length > 0) {
63
+ throw new Error(`refusing to write an invalid record: ${problems.join('; ')}`);
64
+ }
52
65
  const dir = storeDir(root);
53
66
  fs.mkdirSync(dir, { recursive: true });
54
67
  fs.writeFileSync(
@@ -71,7 +84,7 @@ export function listRecords(root: string): DefectRecord[] {
71
84
  // The overflow counter shares the directory and the extension but is not
72
85
  // a record. `isRecord` would reject it anyway; naming it here keeps the
73
86
  // exclusion intentional rather than incidental.
74
- if (!n.endsWith('.json') || n === OVERFLOW_FILE) {
87
+ if (!n.endsWith('.json') || n === OVERFLOW_FILE || n === TARGET_INDEX_FILE) {
75
88
  continue;
76
89
  }
77
90
  const rec = readRecord(root, n.slice(0, -'.json'.length));
@@ -83,8 +96,16 @@ export function listRecords(root: string): DefectRecord[] {
83
96
  return out;
84
97
  }
85
98
 
99
+ /**
100
+ * The records still in the queue. Since the status enum widened past
101
+ * `open | released`, "still queued" is `open` OR `candidate` — a record someone
102
+ * accepted but has not yet acted on has not left the queue, and a `declined`,
103
+ * `superseded`, `actioned` or `parked` one has.
104
+ */
86
105
  export function openRecords(root: string): DefectRecord[] {
87
- return listRecords(root).filter((r) => r.status === 'open');
106
+ return listRecords(root).filter((r) =>
107
+ (ACTIVE_STATUSES as readonly string[]).includes(r.status),
108
+ );
88
109
  }
89
110
 
90
111
  /**
@@ -134,29 +155,60 @@ export function upsertFinding(
134
155
  root: string,
135
156
  finding: DefectFinding,
136
157
  now: string,
158
+ // The tree the targets resolve against. Defaults to the store root, which
159
+ // is the production case (both are the repo). A test sandboxes the STORE in
160
+ // a tmp dir while still resolving `rule:scope-control` against the real
161
+ // tree, and folding the two roots together would make that untestable.
162
+ opts?: { treeRoot?: string },
137
163
  ): DefectRecord | null {
138
164
  const fp = fingerprint(finding.defect_class, finding.evidence);
165
+ // Targets are resolved against the tree HERE, at the write boundary: the
166
+ // pure half cannot read the filesystem, and validating anywhere later would
167
+ // mean a dangling id had already been stored.
168
+ const split = partitionTargets(opts?.treeRoot ?? root, [
169
+ ...(finding.target ?? []),
170
+ ...(finding.proposes ?? []),
171
+ ]);
172
+ const resolved: DefectFinding = {
173
+ ...finding,
174
+ ...(split.target.length > 0 ? { target: split.target } : {}),
175
+ ...(split.proposes.length > 0 ? { proposes: split.proposes } : {}),
176
+ };
177
+ if (split.target.length === 0) delete resolved.target;
178
+ if (split.proposes.length === 0) delete resolved.proposes;
139
179
  const existing = readRecord(root, fp);
140
180
  // The fold path never consults the cap: a fingerprint already in the store
141
181
  // adds no file, so capping it would silence a tracked defect for no bound.
142
- if (existing === null && creationCapReached(listRecords(root), finding, now)) {
143
- bumpOverflow(root, finding.source, now);
182
+ if (existing === null && creationCapReached(listRecords(root), resolved, now)) {
183
+ bumpOverflow(root, resolved.source, now);
144
184
  return null;
145
185
  }
146
- const merged = mergeRecord(existing, finding, now);
186
+ const merged = mergeRecord(existing, resolved, now);
147
187
  writeRecord(root, merged);
148
188
  return merged;
149
189
  }
150
190
 
151
- /** Mark a record released (a PR or issue now carries it). */
191
+ /**
192
+ * Mark a record released — a PR or issue now carries it.
193
+ *
194
+ * The stored status is `actioned`, not the retired `released`: releasing IS the
195
+ * action, and the widened enum has no separate slot for it. The function name
196
+ * is kept because the CALLER's event is still "a release happened".
197
+ */
152
198
  export function markReleased(root: string, fp: string, now: string): DefectRecord | null {
153
199
  const rec = readRecord(root, fp);
154
200
  if (rec === null) {
155
201
  return null;
156
202
  }
157
203
  // A successful release supersedes any earlier failed attempts.
158
- const next: DefectRecord = { ...rec, status: 'released', last_seen: now };
204
+ const next: DefectRecord = {
205
+ ...rec,
206
+ status: 'actioned',
207
+ resolution: rec.resolution ?? 'released — a pull request or issue now carries this record',
208
+ last_seen: now,
209
+ };
159
210
  delete next.release_errors;
211
+ delete next.parked_until;
160
212
  writeRecord(root, next);
161
213
  return next;
162
214
  }
@@ -186,3 +238,217 @@ export function attachReleaseErrors(
186
238
  writeRecord(root, next);
187
239
  return next;
188
240
  }
241
+
242
+ // target resolution
243
+ //
244
+ // `suggested_surface` is free text, so nothing could join a record to the asset
245
+ // it is about. A `target` entry is `<kind>:<id>` from a closed vocabulary AND it
246
+ // is resolved against the tree at WRITE time — an unresolvable entry is not
247
+ // rejected (a record proposing an asset that should exist is real information)
248
+ // and is not silently accepted either (a dangling id looks joined and joins to
249
+ // nothing). It is moved to `proposes`, where a reader can see it for what it is.
250
+
251
+ /** Where each target kind lives in the source tree. */
252
+ function _targetExists(root: string, kind: string, id: string): boolean {
253
+ if (kind === 'rule') {
254
+ return fs.existsSync(path.join(root, 'src', 'rules', `${id}.md`));
255
+ }
256
+ if (kind === 'skill') {
257
+ return fs.existsSync(path.join(root, 'src', 'skills', id, 'SKILL.md'));
258
+ }
259
+ if (kind === 'command') {
260
+ return _commandIds(root).has(id);
261
+ }
262
+ if (kind === 'hook') {
263
+ return _concernIds(root).has(id);
264
+ }
265
+ return false;
266
+ }
267
+
268
+ /**
269
+ * Command ids, read from the pack tree: every `command.md` under
270
+ * `src/domains/<pack>/…` contributes the name of its containing directory, plus
271
+ * the `parent-child` form a clustered command is referred to by.
272
+ */
273
+ function _commandIds(root: string): ReadonlySet<string> {
274
+ const out = new Set<string>();
275
+ const base = path.join(root, 'src', 'domains');
276
+ const walk = (dir: string, trail: string[]): void => {
277
+ let entries: fs.Dirent[];
278
+ try {
279
+ entries = fs.readdirSync(dir, { withFileTypes: true });
280
+ } catch {
281
+ return;
282
+ }
283
+ for (const ent of entries) {
284
+ if (ent.isDirectory()) {
285
+ walk(path.join(dir, ent.name), [...trail, ent.name]);
286
+ } else if (ent.name === 'command.md' && trail.length > 0) {
287
+ out.add(trail[trail.length - 1] as string);
288
+ if (trail.length > 1) {
289
+ out.add(trail.slice(-2).join('-'));
290
+ }
291
+ }
292
+ }
293
+ };
294
+ walk(base, []);
295
+ return out;
296
+ }
297
+
298
+ /**
299
+ * Hook-concern ids, read from the compiled manifest rather than re-parsing the
300
+ * YAML: `hook_manifest.json` is the artifact the dispatcher itself reads, so a
301
+ * concern that resolves here is a concern that actually exists at runtime.
302
+ */
303
+ function _concernIds(root: string): ReadonlySet<string> {
304
+ try {
305
+ const raw = fs.readFileSync(
306
+ path.join(root, 'src', 'scripts', 'hook_manifest.json'),
307
+ 'utf-8',
308
+ );
309
+ const parsed = JSON.parse(raw) as { manifest?: { concerns?: Record<string, unknown> } };
310
+ return new Set(Object.keys(parsed.manifest?.concerns ?? {}));
311
+ } catch {
312
+ return new Set<string>();
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Split a caller's target list into the entries that resolve in the tree and
318
+ * the entries that do not. Malformed entries go to `proposes` too — a string
319
+ * that is not `<kind>:<id>` cannot be resolved, and dropping it would lose the
320
+ * observation.
321
+ */
322
+ export function partitionTargets(
323
+ root: string,
324
+ values: readonly string[],
325
+ ): { target: string[]; proposes: string[] } {
326
+ const target: string[] = [];
327
+ const proposes: string[] = [];
328
+ for (const raw of values) {
329
+ const value = raw.trim();
330
+ if (value === '') continue;
331
+ const parsed = isWellFormedTarget(value) ? parseTarget(value) : null;
332
+ if (parsed !== null && _targetExists(root, parsed.kind, parsed.id)) {
333
+ if (!target.includes(value)) target.push(value);
334
+ } else if (!proposes.includes(value)) {
335
+ proposes.push(value);
336
+ }
337
+ }
338
+ return { target, proposes };
339
+ }
340
+
341
+ // per-target occurrence index
342
+ //
343
+ // `occurrences` counts a FINGERPRINT, which is class plus normalised evidence
344
+ // shape — so two different manifestations of one rule's weakness are two
345
+ // records with one occurrence each, and the third-recurrence escalation stated
346
+ // in `skill-improvement-pipeline` had nothing that could ever establish a third
347
+ // recurrence. This is a DERIVED read over the record files: regenerable,
348
+ // byte-stable, and gitignored with the rest of `agents/runtime/`.
349
+
350
+ // cache-invalidation: regenerated WHOLESALE from the record files on every run
351
+ // (`writeTargetIndex` recomputes from scratch — no incremental or partial
352
+ // state), and the output is deterministic by construction, so a stale read is
353
+ // impossible and a version namespace would name a format nothing reads across
354
+ // runs. The shape change that would matter is a shape change to the records
355
+ // themselves, which `validateRecord` already gates.
356
+ export const TARGET_INDEX_FILE = '_target-index.json';
357
+
358
+ export interface TargetCount {
359
+ target: string;
360
+ /** Sum of `occurrences` across every record naming this target. */
361
+ occurrences: number;
362
+ /** Fingerprints naming it — a per-target count of DISTINCT manifestations. */
363
+ records: string[];
364
+ }
365
+
366
+ function targetIndexPath(root: string): string {
367
+ return path.join(storeDir(root), TARGET_INDEX_FILE);
368
+ }
369
+
370
+ /** Aggregate occurrences by target. Pure over the store's record files. */
371
+ export function targetCounts(root: string): TargetCount[] {
372
+ const acc = new Map<string, { occurrences: number; records: string[] }>();
373
+ for (const rec of listRecords(root)) {
374
+ for (const value of rec.target ?? []) {
375
+ const prev = acc.get(value) ?? { occurrences: 0, records: [] };
376
+ prev.occurrences += Number.isFinite(rec.occurrences) ? rec.occurrences : 0;
377
+ if (!prev.records.includes(rec.fingerprint)) prev.records.push(rec.fingerprint);
378
+ acc.set(value, prev);
379
+ }
380
+ }
381
+ return [...acc.entries()]
382
+ .map(([target, v]) => ({
383
+ target,
384
+ occurrences: v.occurrences,
385
+ records: [...v.records].sort(),
386
+ }))
387
+ .sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0));
388
+ }
389
+
390
+ /**
391
+ * Write the derived index. Deterministic by construction — sorted keys, sorted
392
+ * fingerprints, no timestamp — so deleting it and rebuilding produces the same
393
+ * bytes. A timestamp here would make the artifact look fresh while saying
394
+ * nothing, and would destroy the one property that makes a rebuild routine.
395
+ */
396
+ export function writeTargetIndex(root: string): TargetCount[] {
397
+ const counts = targetCounts(root);
398
+ fs.mkdirSync(storeDir(root), { recursive: true });
399
+ fs.writeFileSync(targetIndexPath(root), `${JSON.stringify(counts, null, 2)}\n`, 'utf-8');
400
+ return counts;
401
+ }
402
+
403
+ // status transitions
404
+
405
+ /**
406
+ * Migrate `released` records — the retired status — to `actioned`, carrying a
407
+ * resolution line that says the migration is why. Idempotent: a record already
408
+ * on the new enum is left byte-identical, so running it twice over a store
409
+ * produces no second diff.
410
+ *
411
+ * Returns the fingerprints it changed.
412
+ */
413
+ export function migrateReleasedRecords(root: string): string[] {
414
+ const changed: string[] = [];
415
+ for (const rec of listRecords(root)) {
416
+ if ((rec.status as string) !== 'released') continue;
417
+ const next: DefectRecord = {
418
+ ...rec,
419
+ status: 'actioned',
420
+ resolution:
421
+ rec.resolution ??
422
+ 'migrated from the retired `released` status; the release itself is the action recorded',
423
+ };
424
+ writeRecord(root, next);
425
+ changed.push(rec.fingerprint);
426
+ }
427
+ return changed.sort();
428
+ }
429
+
430
+ /** Move a record to a terminal or parked status, validating the result. */
431
+ export function setStatus(
432
+ root: string,
433
+ fp: string,
434
+ next: DefectRecord['status'],
435
+ now: string,
436
+ extra?: { parked_until?: string; resolution?: string },
437
+ ): DefectRecord | null {
438
+ const rec = readRecord(root, fp);
439
+ if (rec === null) return null;
440
+ const updated: DefectRecord = {
441
+ ...rec,
442
+ status: next,
443
+ last_seen: now,
444
+ ...(extra?.parked_until !== undefined ? { parked_until: extra.parked_until } : {}),
445
+ ...(extra?.resolution !== undefined ? { resolution: extra.resolution } : {}),
446
+ };
447
+ if (next !== 'parked') delete updated.parked_until;
448
+ const problems = validateRecord(updated);
449
+ if (problems.length > 0) {
450
+ throw new Error(`refusing to write an invalid record: ${problems.join('; ')}`);
451
+ }
452
+ writeRecord(root, updated);
453
+ return updated;
454
+ }
@@ -175,14 +175,19 @@ export function stop_means_session_end(platform: string | null | undefined): boo
175
175
  * Deliberately **not** derived from the frequency lattice. That instrument
176
176
  * models *slot presence*, and two hosts have a slot that never fires:
177
177
  *
178
- * - `cursor` — `user_prompt_submit` and `stop` are **IDE-only**; the CLI fires
179
- * only shell-execution hooks (`hook_manifest.yaml:365-366`,
180
- * `chat-history-platform-hooks.md:214`). The lattice has no IDE/CLI dimension,
181
- * so it reports cursor covered.
178
+ * - `cursor` — excluded because reachability there is UNESTABLISHED, not
179
+ * because it is known absent. The earlier wording asserted the CLI fires only
180
+ * shell-execution hooks, sourced to a 2026-01 reading that carried no expiry
181
+ * and could not be re-verified in either direction. The safe direction is the
182
+ * one taken: an unverified slot must not read as covered. The lattice has no
183
+ * IDE/CLI dimension of its own, so it would report cursor covered; the
184
+ * envelope now carries a `surface` field (`_lib/surface.ts`) that records
185
+ * which surface a dispatch came from, and it answers `unknown` on cursor
186
+ * because no marker distinguishes the two.
182
187
  * - `cowork` — slots are structurally wired but lifecycle events do not fire
183
188
  * (`hook_manifest.yaml:346-351`).
184
- * - `copilot` — no hook surface at all (`hook_manifest.yaml:428-429`), excluded
185
- * from the join by declaration.
189
+ * - `copilot` — nothing bound by this package (`fallback_only: true` in the
190
+ * manifest), excluded from the join by declaration.
186
191
  *
187
192
  * A session on an excluded host still *registers* if a `session_start` reaches
188
193
  * it, and simply stops being visible after its TTL. It is never reported as
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Schema helpers for the `node:sqlite` stores in this tree.
3
+ */
4
+ import type { DatabaseSync } from 'node:sqlite';
5
+
6
+ /**
7
+ * Add a column to an existing table when it is absent.
8
+ *
9
+ * `CREATE TABLE IF NOT EXISTS` is a no-op on a database that predates a
10
+ * column, so a store created by an earlier build needs the explicit add or the
11
+ * field is silently missing from every read.
12
+ *
13
+ * `PRAGMA table_info` rather than a catch around `ALTER TABLE`: swallowing the
14
+ * error would also swallow a genuine schema failure, and a store that silently
15
+ * fails to migrate reads exactly like one that never needed to.
16
+ */
17
+ export function addColumnIfMissing(db: DatabaseSync, table: string, column: string, decl: string): void {
18
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
19
+ if (cols.length === 0) return;
20
+ if (cols.some((c) => c.name === column)) return;
21
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
22
+ }