@event4u/agent-config 14.19.0 → 14.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -30,11 +30,15 @@
30
30
  */
31
31
 
32
32
  import { readFileSync } from "node:fs";
33
+ import type { MatchResult, RawOccurrence } from "./ai_tells_rules.js";
33
34
  import {
34
35
  ALL_TELL_RULES,
35
36
  DEFAULT_MAX_CLUSTER_SCORE,
36
37
  DEFAULT_MAX_DASH_DENSITY,
37
38
  DEFAULT_MAX_HARD,
39
+ MIN_CONSISTENT_OCCURRENCES,
40
+ MIN_CONSISTENT_SPREAD,
41
+ MIN_DENSITY_WORDS,
38
42
  sniffLanguage,
39
43
  } from "./ai_tells_rules.js";
40
44
  import { _classify } from "./lint_hidden_unicode.js";
@@ -48,6 +52,15 @@ import { _classify } from "./lint_hidden_unicode.js";
48
52
  */
49
53
  export const MAX_SCAN_CHARS = 100_000;
50
54
 
55
+ /** Where one match sits, so a finding can be located rather than only counted. */
56
+ export interface Occurrence {
57
+ /** 1-based. */
58
+ line: number;
59
+ /** 1-based, in characters. */
60
+ column: number;
61
+ text: string;
62
+ }
63
+
51
64
  export interface RuleHit {
52
65
  id: string;
53
66
  group: string;
@@ -55,6 +68,18 @@ export interface RuleHit {
55
68
  count: number;
56
69
  weight: number;
57
70
  samples: string[];
71
+ /** Every match, located. `count` is `occurrences.length`. */
72
+ occurrences: Occurrence[];
73
+ /**
74
+ * `uniform` when the pattern is used at least
75
+ * `MIN_CONSISTENT_OCCURRENCES` times across at least
76
+ * `MIN_CONSISTENT_SPREAD` of the document: a habit the author carries
77
+ * through the whole piece, which is evidence of style rather than of N
78
+ * independent tells. A uniform cluster pattern contributes its weight ONCE.
79
+ */
80
+ consistency: "uniform" | "scattered";
81
+ /** What the cluster score actually charged for this rule. */
82
+ scored_count: number;
58
83
  }
59
84
 
60
85
  /**
@@ -77,9 +102,16 @@ export interface TellReport {
77
102
  cluster_hits: RuleHit[];
78
103
  hard_total: number;
79
104
  cluster_score: number;
80
- cluster_score_per_500: number;
105
+ /**
106
+ * `null` below `MIN_DENSITY_WORDS`: a per-500-words rate extrapolated from a
107
+ * handful of words is an artifact of the denominator, not a reading of the
108
+ * prose. `null` says "not evaluated" where a `0` would say "measured clean".
109
+ */
110
+ cluster_score_per_500: number | null;
81
111
  dash_count: number;
82
- dash_density_per_500: number;
112
+ dash_density_per_500: number | null;
113
+ /** False when the text is under the floor and neither density was applied. */
114
+ density_evaluated: boolean;
83
115
  per_pattern: Record<string, number>;
84
116
  truncated: boolean;
85
117
  hidden_unicode: HiddenUnicodeFinding[];
@@ -220,27 +252,82 @@ export function stripExempt(text: string): { base: string; forQuotes: string } {
220
252
  const base = t
221
253
  .replace(/"[^"\n]{1,300}"/g, " ")
222
254
  .replace(/“[^”\n]{1,300}”/g, " ")
223
- .replace(/'[^'\n]{1,120}'/g, (m) => (m.includes(" ") ? " " : m));
255
+ // A single-quoted SPAN, never a pair of contractions. The previous form
256
+ // matched any two apostrophes on a line with a space between them, so
257
+ // "In today's fast-paced world, let's be honest" was read as a quotation
258
+ // and stripped whole — which silently removed two tell families from the
259
+ // scan. An opening quote must start a token and a closing quote must end
260
+ // one; an apostrophe inside a word is never either.
261
+ .replace(
262
+ /(^|[\s([{"“])'([^'\n]{1,120})'(?=[\s)\]},.!?;:"”]|$)/g,
263
+ (m, lead: string, inner: string) => (inner.includes(" ") ? `${lead} ` : m),
264
+ );
224
265
  return { base, forQuotes };
225
266
  }
226
267
 
227
- function countMatches(text: string, patterns: RegExp[]): { count: number; samples: string[] } {
228
- let count = 0;
229
- const samples: string[] = [];
268
+ function countMatches(text: string, patterns: RegExp[]): MatchResult {
269
+ const occurrences: RawOccurrence[] = [];
230
270
  for (const p of patterns) {
231
271
  const flags = p.flags.includes("g") ? p.flags : p.flags + "g";
232
272
  const re = new RegExp(p.source, flags);
233
273
  for (const m of text.matchAll(re)) {
234
- count += 1;
235
- if (samples.length < 3) samples.push(m[0].slice(0, 60).trim());
274
+ occurrences.push({ index: m.index ?? 0, text: m[0] });
236
275
  }
237
276
  }
238
- return { count, samples };
277
+ occurrences.sort((a, b) => a.index - b.index);
278
+ return {
279
+ count: occurrences.length,
280
+ samples: occurrences.slice(0, 3).map((o) => o.text.slice(0, 60).trim()),
281
+ occurrences,
282
+ };
283
+ }
284
+
285
+ /** Offset → 1-based line and column, computed once per scan text. */
286
+ function lineIndex(text: string): (index: number) => { line: number; column: number } {
287
+ const starts = [0];
288
+ for (let i = 0; i < text.length; i += 1) if (text[i] === "\n") starts.push(i + 1);
289
+ return (index: number) => {
290
+ let lo = 0;
291
+ let hi = starts.length - 1;
292
+ while (lo < hi) {
293
+ const mid = (lo + hi + 1) >> 1;
294
+ if ((starts[mid] as number) <= index) lo = mid;
295
+ else hi = mid - 1;
296
+ }
297
+ return { line: lo + 1, column: index - (starts[lo] as number) + 1 };
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Is this pattern a habit the author carries through the document, or a local
303
+ * repetition? Without a captured voice sample the skill falls back to defaults,
304
+ * so consistency is the only intent signal available from the text itself.
305
+ */
306
+ export function classifyConsistency(
307
+ occurrences: ReadonlyArray<RawOccurrence>,
308
+ textLength: number,
309
+ ): "uniform" | "scattered" {
310
+ if (occurrences.length < MIN_CONSISTENT_OCCURRENCES) return "scattered";
311
+ const first = occurrences[0]?.index ?? 0;
312
+ const last = occurrences[occurrences.length - 1]?.index ?? 0;
313
+ const spread = (last - first) / Math.max(textLength, 1);
314
+ return spread >= MIN_CONSISTENT_SPREAD ? "uniform" : "scattered";
315
+ }
316
+
317
+ export interface AnalyzeOptions {
318
+ /**
319
+ * Rule ids to leave out of the scan entirely — not merely out of the
320
+ * reported vector. Used by the paired bench to ask what a family was
321
+ * contributing: a metric that changes when the rule is removed is one the
322
+ * rule was carrying, and a metric that does not is one it was not.
323
+ */
324
+ exclude?: ReadonlySet<string>;
239
325
  }
240
326
 
241
327
  export function analyzeText(
242
328
  text: string,
243
329
  languageOpt: "en" | "de" | "auto" = "auto",
330
+ options: AnalyzeOptions = {},
244
331
  ): TellReport {
245
332
  // Scan the RAW text for hidden-instruction vectors before any stripping —
246
333
  // the smuggling layer is invisible and must be reported (untrusted-input).
@@ -253,16 +340,29 @@ export function analyzeText(
253
340
  const words = base.split(/\s+/).filter(Boolean).length || 1;
254
341
  const language = languageOpt === "auto" ? sniffLanguage(base) : languageOpt;
255
342
 
343
+ const locateBase = lineIndex(base);
344
+ const locateQuotes = lineIndex(forQuotes);
256
345
  const hardHits: RuleHit[] = [];
257
346
  const clusterHits: RuleHit[] = [];
258
347
  const perPattern: Record<string, number> = {};
259
348
 
260
349
  for (const rule of ALL_TELL_RULES) {
261
350
  if (rule.language !== "any" && rule.language !== language) continue;
351
+ if (options.exclude?.has(rule.id)) continue;
262
352
  const scanText = rule.id === "tell-curly-quotes" ? forQuotes : base;
263
- const { count, samples } = countMatches(scanText, rule.patterns);
353
+ const result: MatchResult = rule.match
354
+ ? rule.match(scanText, language)
355
+ : countMatches(scanText, rule.patterns);
356
+ const { count, samples, occurrences } = result;
264
357
  if (count === 0) continue;
265
358
  perPattern[rule.id] = count;
359
+ const locate = scanText === forQuotes ? locateQuotes : locateBase;
360
+ const consistency = classifyConsistency(occurrences, scanText.length);
361
+ // A uniform pattern is charged once. A HARD rule is never discounted: its
362
+ // members are chat artifacts and cutoff disclaimers, and repeating one
363
+ // through a document makes it more of a defect, not less of one.
364
+ const scoredCount =
365
+ rule.severity === "cluster" && consistency === "uniform" ? 1 : count;
266
366
  const hit: RuleHit = {
267
367
  id: rule.id,
268
368
  group: rule.group,
@@ -270,14 +370,19 @@ export function analyzeText(
270
370
  count,
271
371
  weight: rule.weight,
272
372
  samples,
373
+ occurrences: occurrences.map((o) => ({ ...locate(o.index), text: o.text.slice(0, 120) })),
374
+ consistency,
375
+ scored_count: scoredCount,
273
376
  };
274
377
  (rule.severity === "hard" ? hardHits : clusterHits).push(hit);
275
378
  }
276
379
 
277
380
  const hardTotal = hardHits.reduce((s, h) => s + h.count, 0);
278
- const clusterScore = clusterHits.reduce((s, h) => s + h.count * h.weight, 0);
381
+ const clusterScore = clusterHits.reduce((s, h) => s + h.scored_count * h.weight, 0);
279
382
  const dashCount = (forQuotes.match(/[—–]/g) ?? []).length;
280
- const per500 = (n: number) => Math.round((n / words) * 500 * 100) / 100;
383
+ const densityEvaluated = words >= MIN_DENSITY_WORDS;
384
+ const per500 = (n: number) =>
385
+ densityEvaluated ? Math.round((n / words) * 500 * 100) / 100 : null;
281
386
 
282
387
  return {
283
388
  words,
@@ -289,6 +394,7 @@ export function analyzeText(
289
394
  cluster_score_per_500: per500(clusterScore),
290
395
  dash_count: dashCount,
291
396
  dash_density_per_500: per500(dashCount),
397
+ density_evaluated: densityEvaluated,
292
398
  per_pattern: perPattern,
293
399
  truncated,
294
400
  hidden_unicode: hiddenUnicode,
@@ -299,9 +405,9 @@ export function exceedsThresholds(r: TellReport, t: Thresholds): string[] {
299
405
  const reasons: string[] = [];
300
406
  if (r.hard_total > t.maxHard)
301
407
  reasons.push(`hard hits ${r.hard_total} > ${t.maxHard}`);
302
- if (r.cluster_score_per_500 > t.maxScore)
408
+ if (r.cluster_score_per_500 !== null && r.cluster_score_per_500 > t.maxScore)
303
409
  reasons.push(`cluster score ${r.cluster_score_per_500}/500w > ${t.maxScore}`);
304
- if (r.dash_density_per_500 > t.maxDashDensity)
410
+ if (r.dash_density_per_500 !== null && r.dash_density_per_500 > t.maxDashDensity)
305
411
  reasons.push(`dash density ${r.dash_density_per_500}/500w > ${t.maxDashDensity}`);
306
412
  return reasons;
307
413
  }
@@ -309,15 +415,31 @@ export function exceedsThresholds(r: TellReport, t: Thresholds): string[] {
309
415
  function humanSummary(name: string, r: TellReport, reasons: string[]): string {
310
416
  const lines: string[] = [];
311
417
  const verdict = reasons.length === 0 ? "✅" : "❌";
418
+ const density = r.density_evaluated
419
+ ? `cluster ${r.cluster_score_per_500}/500w · dashes ${r.dash_density_per_500}/500w`
420
+ : `density not evaluated (${r.words} words < ${MIN_DENSITY_WORDS}-word floor; ` +
421
+ `${r.cluster_score} cluster weight, ${r.dash_count} dash(es) counted)`;
312
422
  lines.push(
313
- `${verdict} ${name} — ${r.words} words (${r.language}) · hard ${r.hard_total} · ` +
314
- `cluster ${r.cluster_score_per_500}/500w · dashes ${r.dash_density_per_500}/500w`,
423
+ `${verdict} ${name} — ${r.words} words (${r.language}) · hard ${r.hard_total} · ${density}`,
315
424
  );
316
425
  for (const h of [...r.hard_hits, ...r.cluster_hits]) {
317
- lines.push(
318
- ` ${h.severity === "hard" ? "‼" : "·"} ${h.id} ×${h.count}` +
319
- (h.samples.length ? ` (${h.samples.join(" | ")})` : ""),
320
- );
426
+ const where = h.occurrences
427
+ .slice(0, 3)
428
+ .map((o) => `${o.line}:${o.column}`)
429
+ .join(", ");
430
+ if (h.consistency === "uniform") {
431
+ lines.push(
432
+ ` ≡ ${h.id} — used consistently throughout (${h.count} occurrences, ` +
433
+ `${where}${h.occurrences.length > 3 ? ", …" : ""}); read as style, ` +
434
+ `charged once, not as ${h.count} independent tells`,
435
+ );
436
+ } else {
437
+ lines.push(
438
+ ` ${h.severity === "hard" ? "‼" : "·"} ${h.id} ×${h.count}` +
439
+ (where ? ` @ ${where}${h.occurrences.length > 3 ? ", …" : ""}` : "") +
440
+ (h.samples.length ? ` (${h.samples.join(" | ")})` : ""),
441
+ );
442
+ }
321
443
  }
322
444
  for (const reason of reasons) lines.push(` → over threshold: ${reason}`);
323
445
  for (const h of r.hidden_unicode) {
@@ -37,10 +37,12 @@ import { fileURLToPath } from 'node:url';
37
37
  import { collectRepoAnchor, describeDrift } from './_lib/envelope_grounding.js';
38
38
  import { readHookStdin } from './hooks/hook_stdin.js';
39
39
  import {
40
- RECYCLE_CONSUMED_REL,
41
- RECYCLE_ENVELOPE_REL,
42
40
  RECYCLE_MAX_AGE_HOURS,
41
+ predecessorTracePresent,
42
+ recycle_consumed_rel,
43
+ resolveContinuityRecord,
43
44
  } from './_lib/recycle_envelope_paths.js';
45
+ import { env_session_id } from './sessions_cli.js';
44
46
  import {
45
47
  hasBoundaryMarker,
46
48
  scanEnvelopeDirectives,
@@ -153,8 +155,33 @@ export function consume_handoff_context(root: string, now: Date = new Date()): C
153
155
  * - focus (Phase 3.4): `AGENT_RESUME_FOCUS` narrows what the successor
154
156
  * attacks first — the consumer-side mirror of `next_task`.
155
157
  */
156
- export function consume_recycle_envelope(root: string, now: Date = new Date()): ConsumeDecision {
157
- const target = process.env.AGENT_RECYCLE_ENVELOPE_FILE || path.join(root, RECYCLE_ENVELOPE_REL);
158
+ export function consume_recycle_envelope(
159
+ root: string,
160
+ now: Date = new Date(),
161
+ session_id: string | null | undefined = undefined,
162
+ ): ConsumeDecision {
163
+ // Phase 2.1: which record belongs to THIS session is a resolution, not a
164
+ // path constant. `resolveContinuityRecord` returns `null` with a reason
165
+ // when it cannot tell — the reader then starts clean and says the reason
166
+ // out loud rather than resuming from whichever file is newest.
167
+ //
168
+ // The id is a PARAMETER with an env fallback, not an env read. The
169
+ // dispatcher already carries `session_id` in the envelope, and reading the
170
+ // ambient variable instead means any process that inherits a host's
171
+ // `CLAUDE_CODE_SESSION_ID` — a test runner in an agent session, most
172
+ // obviously — resolves a foreign identity and reports the record absent.
173
+ const sessionId = session_id === undefined ? env_session_id() : session_id;
174
+ const override = process.env.AGENT_RECYCLE_ENVELOPE_FILE;
175
+ let target: string;
176
+ if (override !== undefined && override !== '') {
177
+ target = override;
178
+ } else {
179
+ const resolved = resolveContinuityRecord(root, sessionId);
180
+ if (resolved.file === null) {
181
+ return { action: 'absent', reason: resolved.reason };
182
+ }
183
+ target = resolved.file;
184
+ }
158
185
  let text: string;
159
186
  try {
160
187
  text = fs.readFileSync(target, 'utf-8');
@@ -164,7 +191,10 @@ export function consume_recycle_envelope(root: string, now: Date = new Date()):
164
191
 
165
192
  const consume = (): void => {
166
193
  try {
167
- fs.renameSync(target, path.join(path.dirname(target), path.basename(RECYCLE_CONSUMED_REL)));
194
+ fs.renameSync(
195
+ target,
196
+ path.join(path.dirname(target), path.basename(recycle_consumed_rel(sessionId))),
197
+ );
168
198
  } catch {
169
199
  try {
170
200
  fs.unlinkSync(target); // fallback — never let it survive
@@ -212,6 +242,19 @@ export function consume_recycle_envelope(root: string, now: Date = new Date()):
212
242
  return { action: 'discard', reason: 'recycle envelope workspace does not resolve' };
213
243
  }
214
244
 
245
+ // Lineage (Phase 2.3): a record that NAMES a predecessor is claiming a
246
+ // chain. If nothing in this workspace corroborates that session, the claim
247
+ // is unverifiable and the record is refused — never resolved to whatever
248
+ // else is lying about.
249
+ const predecessor = String(envelope['predecessor'] ?? '').trim();
250
+ if (predecessor !== '' && !predecessorTracePresent(root, predecessor)) {
251
+ consume();
252
+ return {
253
+ action: 'discard',
254
+ reason: `recycle envelope names predecessor "${predecessor}", which has no trace in this workspace`,
255
+ };
256
+ }
257
+
215
258
  // Drift first: a stale resume against the wrong tree is the failure the
216
259
  // reader must see before anything else in the block.
217
260
  const drift = describeDrift(envelope, collectRepoAnchor(root));
@@ -221,7 +264,7 @@ export function consume_recycle_envelope(root: string, now: Date = new Date()):
221
264
  const focus = (process.env.AGENT_RESUME_FOCUS ?? '').trim();
222
265
  const block = wrapAsPriorSessionData(JSON.stringify(envelope, null, 2), {
223
266
  kind: 'recycle-envelope',
224
- source: RECYCLE_ENVELOPE_REL,
267
+ source: path.relative(root, target) || path.basename(target),
225
268
  warnings: [
226
269
  ...drift,
227
270
  ...(focus ? [`FOCUS: attack "${focus}" first — the rest of this envelope is context.`] : []),
@@ -265,7 +308,11 @@ export function main(): number {
265
308
  }
266
309
  if (event === 'session_start') {
267
310
  const handoff = consume_handoff_context(root);
268
- const recycle = consume_recycle_envelope(root);
311
+ const recycle = consume_recycle_envelope(
312
+ root,
313
+ new Date(),
314
+ String(envelope['session_id'] ?? '').trim() || env_session_id(),
315
+ );
269
316
  const blocks: string[] = [];
270
317
  const reasons: string[] = [];
271
318
  // Recycle envelope first — it is the task-state restore the