@dzhechkov/harness-core 0.8.11 → 0.8.21

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 (205) hide show
  1. package/.dz-manifest.json +399 -139
  2. package/LICENSE +13 -0
  3. package/README.md +420 -6
  4. package/dist/agents-policy.d.ts +15 -1
  5. package/dist/agents-policy.d.ts.map +1 -1
  6. package/dist/agents-policy.js +27 -1
  7. package/dist/agents-policy.js.map +1 -1
  8. package/dist/amendment-trace.d.ts +72 -0
  9. package/dist/amendment-trace.d.ts.map +1 -1
  10. package/dist/amendment-trace.js +385 -17
  11. package/dist/amendment-trace.js.map +1 -1
  12. package/dist/backlog-public.d.ts +153 -0
  13. package/dist/backlog-public.d.ts.map +1 -0
  14. package/dist/backlog-public.js +415 -0
  15. package/dist/backlog-public.js.map +1 -0
  16. package/dist/backlog-transitions.d.ts +48 -0
  17. package/dist/backlog-transitions.d.ts.map +1 -0
  18. package/dist/backlog-transitions.js +64 -0
  19. package/dist/backlog-transitions.js.map +1 -0
  20. package/dist/backlog.d.ts.map +1 -1
  21. package/dist/backlog.js +13 -0
  22. package/dist/backlog.js.map +1 -1
  23. package/dist/claim-check.d.ts.map +1 -1
  24. package/dist/claim-check.js +24 -2
  25. package/dist/claim-check.js.map +1 -1
  26. package/dist/claude-hooks-assets.d.ts +93 -0
  27. package/dist/claude-hooks-assets.d.ts.map +1 -0
  28. package/dist/claude-hooks-assets.js +208 -0
  29. package/dist/claude-hooks-assets.js.map +1 -0
  30. package/dist/clean-room-smoke.d.ts +83 -0
  31. package/dist/clean-room-smoke.d.ts.map +1 -0
  32. package/dist/clean-room-smoke.js +138 -0
  33. package/dist/clean-room-smoke.js.map +1 -0
  34. package/dist/cmd-usage.d.ts.map +1 -1
  35. package/dist/cmd-usage.js +36 -6
  36. package/dist/cmd-usage.js.map +1 -1
  37. package/dist/codex-hooks-assets.d.ts +25 -7
  38. package/dist/codex-hooks-assets.d.ts.map +1 -1
  39. package/dist/codex-hooks-assets.js +138 -30
  40. package/dist/codex-hooks-assets.js.map +1 -1
  41. package/dist/codex-hooks.d.ts +21 -1
  42. package/dist/codex-hooks.d.ts.map +1 -1
  43. package/dist/codex-hooks.js +21 -1
  44. package/dist/codex-hooks.js.map +1 -1
  45. package/dist/course-staleness.d.ts +19 -0
  46. package/dist/course-staleness.d.ts.map +1 -0
  47. package/dist/course-staleness.js +95 -0
  48. package/dist/course-staleness.js.map +1 -0
  49. package/dist/destructive-guard-hook.d.ts +40 -0
  50. package/dist/destructive-guard-hook.d.ts.map +1 -0
  51. package/dist/destructive-guard-hook.js +109 -0
  52. package/dist/destructive-guard-hook.js.map +1 -0
  53. package/dist/destructive-guard.d.ts +27 -0
  54. package/dist/destructive-guard.d.ts.map +1 -0
  55. package/dist/destructive-guard.js +2808 -0
  56. package/dist/destructive-guard.js.map +1 -0
  57. package/dist/discrimination-gate.d.ts +28 -3
  58. package/dist/discrimination-gate.d.ts.map +1 -1
  59. package/dist/discrimination-gate.js +76 -16
  60. package/dist/discrimination-gate.js.map +1 -1
  61. package/dist/feature-adr-routing.d.ts +95 -1
  62. package/dist/feature-adr-routing.d.ts.map +1 -1
  63. package/dist/feature-adr-routing.js +193 -27
  64. package/dist/feature-adr-routing.js.map +1 -1
  65. package/dist/guard.d.ts +90 -0
  66. package/dist/guard.d.ts.map +1 -1
  67. package/dist/guard.js +271 -0
  68. package/dist/guard.js.map +1 -1
  69. package/dist/harness-core-location.d.ts +18 -0
  70. package/dist/harness-core-location.d.ts.map +1 -0
  71. package/dist/harness-core-location.js +42 -0
  72. package/dist/harness-core-location.js.map +1 -0
  73. package/dist/index.d.ts +24 -6
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +24 -3
  76. package/dist/index.js.map +1 -1
  77. package/dist/lead-shift-gate.d.ts +95 -0
  78. package/dist/lead-shift-gate.d.ts.map +1 -0
  79. package/dist/lead-shift-gate.js +100 -0
  80. package/dist/lead-shift-gate.js.map +1 -0
  81. package/dist/ledger-backfill.d.ts +11 -1
  82. package/dist/ledger-backfill.d.ts.map +1 -1
  83. package/dist/ledger-backfill.js +19 -0
  84. package/dist/ledger-backfill.js.map +1 -1
  85. package/dist/loop-blobs.generated.d.ts +1 -1
  86. package/dist/loop-blobs.generated.d.ts.map +1 -1
  87. package/dist/loop-blobs.generated.js +12 -3
  88. package/dist/loop-blobs.generated.js.map +1 -1
  89. package/dist/loop-lint.d.ts.map +1 -1
  90. package/dist/loop-lint.js +56 -7
  91. package/dist/loop-lint.js.map +1 -1
  92. package/dist/loop-plan-graph.d.ts +1 -3
  93. package/dist/loop-plan-graph.d.ts.map +1 -1
  94. package/dist/loop-plan-graph.js +70 -1
  95. package/dist/loop-plan-graph.js.map +1 -1
  96. package/dist/loop-trace.d.ts.map +1 -1
  97. package/dist/loop-trace.js +16 -2
  98. package/dist/loop-trace.js.map +1 -1
  99. package/dist/managed-hooks.d.ts +5 -6
  100. package/dist/managed-hooks.d.ts.map +1 -1
  101. package/dist/managed-hooks.js +2 -2
  102. package/dist/managed-hooks.js.map +1 -1
  103. package/dist/model-recommender.d.ts.map +1 -1
  104. package/dist/model-recommender.js +14 -3
  105. package/dist/model-recommender.js.map +1 -1
  106. package/dist/operations.d.ts.map +1 -1
  107. package/dist/operations.js +82 -0
  108. package/dist/operations.js.map +1 -1
  109. package/dist/patterns.d.ts +23 -0
  110. package/dist/patterns.d.ts.map +1 -1
  111. package/dist/patterns.js +10 -0
  112. package/dist/patterns.js.map +1 -1
  113. package/dist/publish.d.ts +11 -0
  114. package/dist/publish.d.ts.map +1 -1
  115. package/dist/publish.js +16 -2
  116. package/dist/publish.js.map +1 -1
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +3 -2
  119. package/dist/registry.js.map +1 -1
  120. package/dist/score.d.ts.map +1 -1
  121. package/dist/score.js +15 -4
  122. package/dist/score.js.map +1 -1
  123. package/dist/session-retro.d.ts +121 -2
  124. package/dist/session-retro.d.ts.map +1 -1
  125. package/dist/session-retro.js +454 -11
  126. package/dist/session-retro.js.map +1 -1
  127. package/dist/setup.d.ts +28 -0
  128. package/dist/setup.d.ts.map +1 -1
  129. package/dist/setup.js +223 -6
  130. package/dist/setup.js.map +1 -1
  131. package/dist/sign.d.ts.map +1 -1
  132. package/dist/sign.js +18 -1
  133. package/dist/sign.js.map +1 -1
  134. package/dist/skills-verify.d.ts +34 -1
  135. package/dist/skills-verify.d.ts.map +1 -1
  136. package/dist/skills-verify.js +82 -0
  137. package/dist/skills-verify.js.map +1 -1
  138. package/dist/stage-line.d.ts +68 -0
  139. package/dist/stage-line.d.ts.map +1 -0
  140. package/dist/stage-line.js +129 -0
  141. package/dist/stage-line.js.map +1 -0
  142. package/dist/statusline.d.ts +99 -0
  143. package/dist/statusline.d.ts.map +1 -1
  144. package/dist/statusline.js +310 -44
  145. package/dist/statusline.js.map +1 -1
  146. package/dist/store-counts.d.ts +26 -0
  147. package/dist/store-counts.d.ts.map +1 -0
  148. package/dist/store-counts.js +125 -0
  149. package/dist/store-counts.js.map +1 -0
  150. package/dist/store-guard.d.ts +106 -0
  151. package/dist/store-guard.d.ts.map +1 -0
  152. package/dist/store-guard.js +294 -0
  153. package/dist/store-guard.js.map +1 -0
  154. package/dist/swarm-brief.d.ts +95 -0
  155. package/dist/swarm-brief.d.ts.map +1 -0
  156. package/dist/swarm-brief.js +660 -0
  157. package/dist/swarm-brief.js.map +1 -0
  158. package/dist/trace-bundle.d.ts +8 -0
  159. package/dist/trace-bundle.d.ts.map +1 -1
  160. package/dist/trace-bundle.js +11 -0
  161. package/dist/trace-bundle.js.map +1 -1
  162. package/package.json +12 -11
  163. package/sbom.json +804 -154
  164. package/src/agents-policy.ts +46 -2
  165. package/src/amendment-trace.ts +441 -19
  166. package/src/backlog-public.ts +503 -0
  167. package/src/backlog-transitions.ts +77 -0
  168. package/src/backlog.ts +12 -0
  169. package/src/claim-check.ts +25 -2
  170. package/src/claude-hooks-assets.ts +227 -0
  171. package/src/clean-room-smoke.ts +195 -0
  172. package/src/cmd-usage.ts +29 -5
  173. package/src/codex-hooks-assets.ts +140 -30
  174. package/src/codex-hooks.ts +21 -1
  175. package/src/course-staleness.ts +125 -0
  176. package/src/destructive-guard-hook.ts +151 -0
  177. package/src/destructive-guard.ts +3027 -0
  178. package/src/discrimination-gate.ts +98 -19
  179. package/src/feature-adr-routing.ts +220 -22
  180. package/src/guard.ts +318 -0
  181. package/src/harness-core-location.ts +44 -0
  182. package/src/index.ts +111 -3
  183. package/src/lead-shift-gate.ts +145 -0
  184. package/src/ledger-backfill.ts +20 -1
  185. package/src/loop-blobs.generated.ts +12 -3
  186. package/src/loop-lint.ts +52 -7
  187. package/src/loop-plan-graph.ts +66 -1
  188. package/src/loop-trace.ts +13 -1
  189. package/src/managed-hooks.ts +5 -6
  190. package/src/model-recommender.ts +14 -3
  191. package/src/operations.ts +75 -0
  192. package/src/patterns.ts +33 -0
  193. package/src/publish.ts +27 -2
  194. package/src/registry.ts +3 -2
  195. package/src/score.ts +16 -4
  196. package/src/session-retro.ts +466 -11
  197. package/src/setup.ts +246 -9
  198. package/src/sign.ts +18 -1
  199. package/src/skills-verify.ts +99 -1
  200. package/src/stage-line.ts +151 -0
  201. package/src/statusline.ts +396 -47
  202. package/src/store-counts.ts +154 -0
  203. package/src/store-guard.ts +388 -0
  204. package/src/swarm-brief.ts +661 -0
  205. package/src/trace-bundle.ts +10 -0
package/src/guard.ts CHANGED
@@ -51,12 +51,20 @@ export interface Violation {
51
51
  readonly contentAnchor?: string;
52
52
  }
53
53
 
54
+ /** A publish-secret waiver: exact repo-relative path, with a mandatory explanation. */
55
+ interface SecretWaiver {
56
+ readonly path?: string;
57
+ readonly reason?: string;
58
+ }
59
+
54
60
  export interface GuardResult {
55
61
  readonly op: GuardOp;
56
62
  readonly verdict: GuardVerdict;
57
63
  readonly violations: readonly Violation[];
58
64
  /** ids of the rules that ran for this op (so a report can show what was checked, not just what failed). */
59
65
  readonly checked: readonly string[];
66
+ /** ids of the rules that ran for this op but received no input to examine. */
67
+ readonly notEstablished: readonly string[];
60
68
  /**
61
69
  * Informational notes (FN-7): things a rule wants ON THE RECORD that are NOT violations and never
62
70
  * touch the verdict — e.g. "no-stubs: N changed scannable file(s) not scanned". A fail-open skip
@@ -75,6 +83,45 @@ export interface GuardFacts {
75
83
  readonly volume?: VolumeShadowInput;
76
84
  /** for no-workspace-star: each publishable package's deps map. */
77
85
  readonly packages?: readonly { readonly name: string; readonly deps: Readonly<Record<string, string>> }[];
86
+ /**
87
+ * for sibling-dep-protocol: КАЖДАЯ зависимость на соседний пакет монорепозитория, как она
88
+ * записана НА ДИСКЕ (без подстановки версии), с полем, в котором она объявлена. Поле нужно:
89
+ * `dependencies` и `devDependencies` обязаны идти через workspace-протокол, а `peerDependencies`
90
+ * и `optionalDependencies` — НЕТ, и это не послабление, а разная семантика (см. правило).
91
+ */
92
+ /**
93
+ * for plugin-manifest-audit: каждый найденный `.claude-plugin/plugin.json` и объявленные им
94
+ * поля. `parseError` заполнен ⇒ манифест не разобрался; остальные поля тогда не смотрят.
95
+ */
96
+ readonly pluginManifests?: readonly {
97
+ readonly path: string;
98
+ readonly parseError?: string;
99
+ readonly name?: string;
100
+ readonly version?: string;
101
+ readonly description?: string;
102
+ /** Имена навыков, ОБЪЯВЛЕННЫЕ манифестом. */
103
+ readonly declaredSkills?: readonly string[];
104
+ /** Имена навыков, НАЙДЕННЫЕ на диске рядом с манифестом. */
105
+ readonly skillsOnDisk?: readonly string[];
106
+ }[];
107
+ readonly siblingDeps?: readonly {
108
+ readonly name: string;
109
+ readonly field: string;
110
+ readonly dep: string;
111
+ readonly spec: string;
112
+ }[];
113
+ /**
114
+ * for backlog-covers-features: каталоги фич, дата их ПЕРВОГО коммита, тексты записей бэклога и
115
+ * базовая дата. Базовая дата обязательна и делает правило зелёным на приходе: 336 существующих
116
+ * каталогов заведены до правила, и краснеть на них — значит учить себя игнорировать (ИЗМЕРЕНО
117
+ * 2026-09-03: без базы 236 нарушений, в окне 7 дней — 48). Отсутствие факта ⇒ правило молчит:
118
+ * страж без улик не выдумывает вердикт.
119
+ */
120
+ readonly featureBacklog?: {
121
+ readonly baseline: string;
122
+ readonly features: readonly { readonly slug: string; readonly createdIso: string; readonly waiver?: string }[];
123
+ readonly backlogTexts: readonly string[];
124
+ };
78
125
  /** for no-skill-drift: the names that byte-drift between copies (from sweepSkillDrift). */
79
126
  readonly drift?: readonly string[];
80
127
  /**
@@ -84,6 +131,10 @@ export interface GuardFacts {
84
131
  readonly workflowScripts?: readonly { readonly path: string; readonly text: string }[];
85
132
  /** for no-secrets: labelled blobs to scan (lesson text, staged files). */
86
133
  readonly secretTargets?: readonly { readonly label: string; readonly text: string }[];
134
+ /** for no-secrets: exact path waivers; entries without a non-empty reason are ignored and noted. */
135
+ readonly secretWaivers?: readonly SecretWaiver[];
136
+ /** Publish secret-scan coverage gaps; skips are informational and never verdict inputs. */
137
+ readonly secretScan?: { readonly skipped: number };
87
138
  /** for readme-consistency: labelled (a,b) count pairs that must be equal. */
88
139
  readonly counts?: readonly { readonly label: string; readonly a: number; readonly b: number }[];
89
140
  /** for store-bloat-cap: current learned-store size vs its cap. */
@@ -299,7 +350,10 @@ function unquoteYaml(s: string): string {
299
350
  /** The built-in rule set (works with no config). Ops are the mutating operations each rule guards. */
300
351
  export const DEFAULT_RULES: readonly GuardRule[] = [
301
352
  { id: 'no-workspace-star', severity: 'hard', ops: ['publish'], description: 'a published package.json must carry no workspace:* dep (npm ships it verbatim → the install breaks)' },
353
+ { id: 'plugin-manifest-audit', severity: 'hard', ops: ['publish'], description: 'every .claude-plugin/plugin.json parses and declares a non-empty name, description and a STRICT N.N.N version' },
354
+ { id: 'sibling-dep-protocol', severity: 'hard', ops: ['publish'], description: 'a dependencies/devDependencies entry on a sibling @dzhechkov package must use the workspace: protocol on disk (peer/optional deps are deliberately exempt — a range is their point)' },
302
355
  { id: 'no-skill-drift', severity: 'hard', ops: ['publish', 'consolidate'], description: 'no unexpected byte-drift between shared skill copies' },
356
+ { id: 'backlog-covers-features', severity: 'soft', ops: ['publish', 'consolidate'], description: 'каталог фичи, заведённый после базовой даты, назван записью бэклога — либо несёт именованную оговорку с причиной' },
303
357
  { id: 'no-secrets', severity: 'hard', ops: ['teach', 'publish'], description: 'no private key or API token in lesson text or a published file' },
304
358
  { id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
305
359
  { id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
@@ -346,7 +400,91 @@ export function scanSecrets(text: unknown): { readonly name: string }[] {
346
400
  return [...hits].map((name) => ({ name }));
347
401
  }
348
402
 
403
+ function secretWaiverState(waivers: readonly SecretWaiver[] | undefined): { waived: ReadonlySet<string>; reasonless: number } {
404
+ const waived = new Set<string>();
405
+ let reasonless = 0;
406
+ for (const waiver of Array.isArray(waivers) ? waivers : []) {
407
+ if (!waiver || typeof waiver !== 'object' || typeof waiver.path !== 'string' || waiver.path.trim() === '') continue;
408
+ const reason = typeof waiver.reason === 'string' ? waiver.reason.trim() : '';
409
+ if (reason.length > 0) waived.add(waiver.path);
410
+ else reasonless++;
411
+ }
412
+ return { waived, reasonless };
413
+ }
414
+
349
415
  /** Per-rule pure checkers. Each returns the violations it found (empty ⇒ clean). Missing evidence ⇒ []. */
416
+ /**
417
+ * Какие каталоги фич заведены после базовой даты и НЕ названы ни одной записью бэклога.
418
+ *
419
+ * БАЗОВАЯ ДАТА — не украшение, а условие осмысленности. ИЗМЕРЕНО 2026-09-03: без неё правило даёт
420
+ * 236 нарушений из 336 каталогов, а в окне «последние 7 дней» — 48 из 85. Проверка, изобретающая
421
+ * полсотни нарушений в первый день, учит людей себя игнорировать, то есть хуже отсутствующей.
422
+ * База делает правило зелёным на приходе и красным ровно на новом.
423
+ *
424
+ * ДАТА ПЕРВОГО КОММИТА, А НЕ mtime. Время правки меняет любой посторонний процесс — пересборка,
425
+ * перенос, чтение с обновлением. Дата появления каталога в истории неподвижна. ЧЕСТНАЯ ГРАНИЦА:
426
+ * функция ДОВЕРЯЕТ переданной строке и происхождение её не подтверждает — обязанность подать
427
+ * именно git-дату лежит на вызывающем (сбор фактов в cli.ts). Здесь проверяется только то, что
428
+ * строка вообще разбирается в дату.
429
+ *
430
+ * ЧТО ИМЕННО ПРОВЕРЯЕТСЯ В ОГОВОРКЕ — сказано точно, потому что ревью 2026-09-03 поймало
431
+ * расхождение обещания с кодом. Машинно проверяется РОВНО одно: строка непуста после обрезки
432
+ * пробелов. Осмысленность причины машинно не проверяема, и оговорка `x` пройдёт. Это сознательная
433
+ * граница слоя: гейт заставляет РЕШЕНИЕ БЫТЬ ЗАПИСАННЫМ, а качество формулировки остаётся делом
434
+ * человека — ровно как у освобождений заставы секретов и списка исключений дрейфа.
435
+ *
436
+ * БАЗА ВКЛЮЧИТЕЛЬНА: каталог, заведённый В САМ день базы, правилом контролируется («не раньше
437
+ * базы», а не «после базы»).
438
+ *
439
+ * ЧЕГО ЭТА ПРОВЕРКА НЕ ЛОВИТ, названо честно: работу БЕЗ каталога фичи — разбор, ремонт, рой,
440
+ * обещание «вернёмся». Машинного следа у них нет, и они остаются на слое 2 (текст правила в
441
+ * CLAUDE.md). Утверждать, что правило покрыто целиком, было бы ложной гарантией.
442
+ */
443
+ export function backlogCoversFeatures(
444
+ features: readonly { readonly slug: string; readonly createdIso: string; readonly waiver?: string }[],
445
+ backlogTexts: readonly string[],
446
+ baseline: string,
447
+ ): readonly string[] {
448
+ // ДАТЫ СРАВНИВАЮТСЯ ЧИСЛАМИ, А НЕ СТРОКАМИ. Кросс-семейное ревью 2026-09-03 (gpt-5.6-sol, Grade D)
449
+ // предъявило три входа, на которых строковое сравнение ISO даёт неверный ответ, и я воспроизвёл их
450
+ // прогоном: `" "` (пробелы лексикографически меньше любой даты — каталог освобождался),
451
+ // `"2026-09-02T23:30:00-02:00"` (фактически ПОЗЖЕ базы, строково раньше), и разные формы записи
452
+ // одной даты. Разбор в число снимает весь класс разом.
453
+ const baseMs = Date.parse(baseline);
454
+ const blob = backlogTexts.join('\n').toLowerCase();
455
+ const out: string[] = [];
456
+ for (const f of features) {
457
+ if (typeof f.slug !== 'string' || f.slug.trim() === '') continue;
458
+ // НЕРАЗБИРАЕМАЯ ИЛИ ОТСУТСТВУЮЩАЯ ДАТА — НЕ ОСВОБОЖДЕНИЕ. Каталог, чью дату появления не удалось
459
+ // установить, считается новым: это отказ в сторону строгости. Обратный выбор превращал бы порчу
460
+ // входа в способ обойти правило.
461
+ const bornMs = typeof f.createdIso === 'string' ? Date.parse(f.createdIso.trim()) : Number.NaN;
462
+ const born = Number.isFinite(bornMs) ? bornMs : Number.POSITIVE_INFINITY;
463
+ if (Number.isFinite(baseMs) && born < baseMs) continue;
464
+ if (typeof f.waiver === 'string' && f.waiver.trim() !== '') continue;
465
+ if (mentionsSlug(blob, f.slug)) continue;
466
+ out.push(f.slug);
467
+ }
468
+ return out;
469
+ }
470
+
471
+ /**
472
+ * Назван ли slug в тексте КАК ОТДЕЛЬНОЕ СЛОВО.
473
+ *
474
+ * Голый `includes` засчитывал совпадение внутри чужого слова: slug `log` считался покрытым записью
475
+ * «обновить catalog schema», а `api` — записью «починить capitalization report» (примеры из
476
+ * кросс-семейного ревью 2026-09-03, воспроизведены прогоном). Ложное покрытие опаснее ложного
477
+ * срабатывания: оно ТИХО гасит правило ровно там, где оно нужно.
478
+ *
479
+ * Границей считается всё, кроме латинской буквы, цифры, подчёркивания и дефиса — дефис входит в
480
+ * слово, потому что сами slug'и кебабные и `date-layers` не должен совпасть внутри
481
+ * `dashboard-date-layers`.
482
+ */
483
+ function mentionsSlug(haystackLower: string, slug: string): boolean {
484
+ const s = slug.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
485
+ return new RegExp(`(^|[^a-z0-9_-])${s}($|[^a-z0-9_-])`).test(haystackLower);
486
+ }
487
+
350
488
  const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[]> = {
351
489
  'no-workspace-star': (f, sev) => {
352
490
  const out: Violation[] = [];
@@ -359,13 +497,146 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
359
497
  }
360
498
  return out;
361
499
  },
500
+ 'plugin-manifest-audit': (f, sev) => {
501
+ /**
502
+ * ЗНАЧЕНИЕ ЕСТЬ, ТОЛЬКО ЕСЛИ ОНО ВИДНО. `trim()` не убирает нулевой ширины пробел и его
503
+ * родню, поэтому описание из одного `\u200B` проходило как заполненное — назвал независимый
504
+ * ревьюер 2026-09-04. Считается видимым лишь то, что остаётся после удаления пробельных И
505
+ * невидимых символов.
506
+ */
507
+ const visible = (v: unknown): v is string =>
508
+ typeof v === 'string' && v.replace(/[\s\u00a0\u180e\u200b-\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\ufeff]/g, '') !== '';
509
+ /** Путь состава в сравнимом виде: `./skills/a` и `skills/a` — один каталог, а не два. */
510
+ const normPath = (v: unknown): string | null =>
511
+ typeof v === 'string'
512
+ ? v.normalize('NFC').replace(/^\.\//, '').replace(/\/+$/, '').replace(/\/{2,}/g, '/')
513
+ : null;
514
+ // ПРОВЕРКА ПАСПОРТА НА ВХОДЕ. Правило `marketplace-parity` отвечает на другой вопрос — совпадает
515
+ // ли витрина со свежей регенерацией. Если ГЕНЕРАТОР выдаст пустое описание или версию `1.0`,
516
+ // parity будет доволен: копия совпадает с оригиналом, оба неверны. Здесь проверяется САМО
517
+ // содержимое.
518
+ //
519
+ // ЧЕГО ЗДЕСЬ НЕТ И ПОЧЕМУ: приёма «имя манифеста равно имени каталога». ИЗМЕРЕНО 2026-09-04 на
520
+ // 10 манифестах дерева — расхождение ровно одно и оно НАМЕРЕННОЕ: пакет называется
521
+ // `loop-designer-plugin`, а плагин внутри него — `loop-designer`. Имя плагина есть
522
+ // опубликованная личность и законно отличается от имени каталога, поэтому такая проверка
523
+ // краснела бы на верном коде. Правило, изобретающее нарушения, учит людей себя игнорировать.
524
+ // Ведущие нули запрещены (`01.2.3` — не строгая форма), и значение НЕ обрезается: пробел
525
+ // вокруг версии в манифесте есть дефект манифеста, а не мелочь, которую следует простить.
526
+ const STRICT_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
527
+ const out: Violation[] = [];
528
+ for (const m of f.pluginManifests ?? []) {
529
+ if (m.parseError !== undefined) {
530
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: манифест не разобрался — ${m.parseError}` });
531
+ continue; // остальные поля у неразобранного манифеста не существуют
532
+ }
533
+ for (const field of ['name', 'description', 'version'] as const) {
534
+ // СОБСТВЕННОЕ свойство: манифест, чьи поля приходят из прототипа, ничего не объявил.
535
+ const value = Object.hasOwn(m, field) ? (m as Record<string, unknown>)[field] : undefined;
536
+ if (!visible(value)) {
537
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: поле ${field} пусто или отсутствует` });
538
+ }
539
+ }
540
+ const rawVersion = Object.hasOwn(m, 'version') ? (m as Record<string, unknown>)['version'] : undefined;
541
+ if (visible(rawVersion) && !STRICT_SEMVER.test(rawVersion)) {
542
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: version = ${JSON.stringify(rawVersion)} — нужна строгая форма N.N.N без ведущих нулей и без пробелов` });
543
+ }
544
+ // ИНВЕНТАРИЗАЦИЯ В ОБЕ СТОРОНЫ: всё, что записано в накладной, лежит на складе, И всё, что
545
+ // лежит на складе, вписано в накладную. Одна сторона ловит обещание без товара, другая —
546
+ // товар, о котором никто не узнает. Класс, который эта пара закрывает: «счётчики поправили,
547
+ // файлы не тронули».
548
+ //
549
+ // Обе стороны проверяются ТОЛЬКО когда обе улики есть: манифест без поля `skills` не
550
+ // объявляет состава вовсе, и требовать от него совпадения значило бы выдумать обязательство.
551
+ const declared = Array.isArray(m.declaredSkills) ? m.declaredSkills : undefined;
552
+ const onDisk = Array.isArray(m.skillsOnDisk) ? m.skillsOnDisk : undefined;
553
+ // ПОЛОВИНА УЛИК — НЕ УЛИКИ. Прежде состав, объявленный без осмотра диска, ТИХО пропускался,
554
+ // а правило продолжало числиться проверенным: то самое «успех из тишины», против которого
555
+ // оно и заведено (назвал независимый ревьюер 2026-09-04).
556
+ if ((declared === undefined) !== (onDisk === undefined)) {
557
+ out.push({
558
+ rule: 'plugin-manifest-audit', severity: sev,
559
+ detail: `${m.path}: есть только одна половина улик о составе (${declared !== undefined ? 'объявленное без осмотра диска' : 'осмотр диска без объявленного'}) — сверить нечем`,
560
+ });
561
+ } else if (declared !== undefined && onDisk !== undefined) {
562
+ const normList = (xs: readonly unknown[]): string[] => xs.map(normPath).filter((x): x is string => x !== null && x !== '');
563
+ // Элемент не-строка — не путь. Молча его пропустить значило бы сверять неполные списки.
564
+ const badDeclared = declared.filter((x) => normPath(x) === null).length;
565
+ const badOnDisk = onDisk.filter((x) => normPath(x) === null).length;
566
+ if (badDeclared > 0 || badOnDisk > 0) {
567
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: в составе есть элементы, которые не являются путями (объявлено ${badDeclared}, на диске ${badOnDisk})` });
568
+ }
569
+ const have = new Set(normList(onDisk));
570
+ const said = new Set(normList(declared));
571
+ const missing = [...said].filter((n) => !have.has(n));
572
+ const undeclared = [...have].filter((n) => !said.has(n));
573
+ if (missing.length > 0) {
574
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: объявлены, но НЕ найдены на диске: ${missing.join(', ')}` });
575
+ }
576
+ if (undeclared.length > 0) {
577
+ out.push({ rule: 'plugin-manifest-audit', severity: sev, detail: `${m.path}: лежат на диске, но НЕ объявлены: ${undeclared.join(', ')}` });
578
+ }
579
+ }
580
+ }
581
+ return out;
582
+ },
583
+ 'sibling-dep-protocol': (f, sev) => {
584
+ // ПАРА К `no-workspace-star`, а не противоречие ему: они говорят о РАЗНЫХ МОМЕНТАХ. На ДИСКЕ
585
+ // сиблинг-зависимость обязана быть `workspace:` — тогда pnpm подставит реальную версию при
586
+ // паковке. В ОПУБЛИКОВАННОМ манифесте `workspace:` быть не должно — npm отправляет его
587
+ // дословно и ломает установку. Одно правило охраняет вход, другое выход.
588
+ //
589
+ // ОБЪЁМ СУЖЕН ИЗМЕРЕНИЕМ, а не осторожностью. Запись бэклога требовала протокол для ЛЮБОЙ
590
+ // сиблинг-зависимости. Замер 2026-09-03 (56 пакетов, 60 сиблинг-зависимостей) показал 8
591
+ // исключений — и ВСЕ восемь оказались `peerDependencies` (7) и `optionalDependencies` (1), где
592
+ // диапазон и есть смысл записи: peer-зависимость объявляет, чему обязан удовлетворять
593
+ // ПОТРЕБИТЕЛЬ, а `workspace:` при паковке превращается в точный пин, враждебный потребителю.
594
+ // Одна из восьми — намеренная схема «dz как ОБНАРУЖИВАЕМАЯ необязательная зависимость».
595
+ // То есть правило в исходной формулировке сломало бы работающий замысел; здесь оно охраняет
596
+ // ровно те два поля, где протокол уместен, и на них нарушений сегодня НОЛЬ.
597
+ const out: Violation[] = [];
598
+ for (const d of f.siblingDeps ?? []) {
599
+ // Поля читаются как ПРИМИТИВНЫЕ строки. Объект с унаследованным `startsWith`, всегда
600
+ // возвращающим true, прежде проходил мимо правила — назвал независимый ревьюер 2026-09-04.
601
+ const field = typeof d?.field === 'string' ? d.field : '';
602
+ const spec = typeof d?.spec === 'string' ? d.spec : null;
603
+ if (field !== 'dependencies' && field !== 'devDependencies') continue;
604
+ if (spec === null) {
605
+ out.push({ rule: 'sibling-dep-protocol', severity: sev, detail: `${String(d?.name)}: ${field}.${String(d?.dep)} — спецификатор не строка, сверить нечем` });
606
+ continue;
607
+ }
608
+ if (spec.startsWith('workspace:')) continue;
609
+ out.push({
610
+ rule: 'sibling-dep-protocol',
611
+ severity: sev,
612
+ detail: `${d.name}: ${field}.${d.dep} = "${spec}" — a sibling package must be referenced through the workspace: protocol on disk, so pnpm substitutes the real version at pack time`,
613
+ });
614
+ }
615
+ return out;
616
+ },
362
617
  'no-skill-drift': (f, sev) => {
363
618
  const drifted = Array.isArray(f.drift) ? f.drift.filter((d) => typeof d === 'string') : [];
364
619
  return drifted.length === 0 ? [] : [{ rule: 'no-skill-drift', severity: sev, detail: `${drifted.length} skill(s) drift between copies: ${drifted.slice(0, 8).join(', ')}${drifted.length > 8 ? '…' : ''} — heal with dz sync-canonical` }];
365
620
  },
621
+ 'backlog-covers-features': (f, sev) => {
622
+ const ev = f.featureBacklog;
623
+ if (!ev) return [];
624
+ const uncovered = backlogCoversFeatures(ev.features, ev.backlogTexts, ev.baseline);
625
+ if (uncovered.length === 0) return [];
626
+ return [{
627
+ rule: 'backlog-covers-features',
628
+ severity: sev,
629
+ detail: `${uncovered.length} фич(и) заведены после ${ev.baseline} и не названы ни одной записью бэклога: `
630
+ + `${uncovered.slice(0, 8).join(', ')}${uncovered.length > 8 ? '…' : ''}`
631
+ + ` — заведи запись (dz backlog add) ЛИБО впиши оговорку в features/<slug>/README.md строкой`
632
+ + ` "Backlog: не заведено — <причина>". Оговорка без причины не считается.`,
633
+ }];
634
+ },
366
635
  'no-secrets': (f, sev) => {
367
636
  const out: Violation[] = [];
637
+ const waived = secretWaiverState(f.secretWaivers).waived;
368
638
  for (const t of f.secretTargets ?? []) {
639
+ if (waived.has(t.label)) continue;
369
640
  for (const hit of scanSecrets(t.text)) {
370
641
  out.push({ rule: 'no-secrets', severity: sev, detail: `${t.label}: looks like a ${hit.name} — do not teach/publish a credential` });
371
642
  }
@@ -638,6 +909,20 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
638
909
  },
639
910
  };
640
911
 
912
+ /** Per-rule evidence predicates. No entry preserves the rule's existing checked behaviour exactly. */
913
+ const HAS_INPUT: Partial<Record<string, (f: GuardFacts) => boolean>> = {
914
+ // `!== undefined` пропускал `null`: правило объявлялось проверенным и возвращало чисто по
915
+ // ветке «улик нет». Это буквально отсутствие улик, отчитанное как проверка (назвал независимый
916
+ // ревьюер 2026-09-04).
917
+ 'backlog-covers-features': (f) => typeof f.featureBacklog === 'object' && f.featureBacklog !== null,
918
+ // Страж без улик не выдумывает вердикт: в дереве без сиблинг-зависимостей правилу нечего
919
+ // сказать, и «прошло» тут значило бы «не смотрели».
920
+ 'sibling-dep-protocol': (f) => Array.isArray(f.siblingDeps) && f.siblingDeps.length > 0,
921
+ // Дерево без плагин-манифестов правилу нечего сказать: «прошло» тут значило бы «не смотрели».
922
+ 'plugin-manifest-audit': (f) => Array.isArray(f.pluginManifests) && f.pluginManifests.length > 0,
923
+ 'no-secrets': (f) => Array.isArray(f.secretTargets) && f.secretTargets.length > 0,
924
+ };
925
+
641
926
  /**
642
927
  * Rules that may NEVER be promoted to HARD, whatever a config says. A rule whose evidence comes from a
643
928
  * deliberately tolerant parser must not be able to BLOCK an operation: the parser's own design admits it
@@ -740,6 +1025,7 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
740
1025
  );
741
1026
  const violations: Violation[] = [];
742
1027
  const checked: string[] = [];
1028
+ const notEstablished: string[] = [];
743
1029
  const observations: GuardObservation[] = [];
744
1030
  let volumeResult: VolumeShadowResult | undefined;
745
1031
  let volumeEvaluated = false;
@@ -757,6 +1043,27 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
757
1043
  return volumeResult;
758
1044
  };
759
1045
  for (const r of active) {
1046
+ const hasInput = HAS_INPUT[r.id];
1047
+ // ПРЕДИКАТ УЛИК ИСПОЛНЯЕТСЯ ПОД ЗАЩИТОЙ. Прежде он стоял вне `try`, и факт с бросающим
1048
+ // геттером ронял весь страж вместо того, чтобы стать отказом (назвал независимый ревьюер
1049
+ // 2026-09-04). Гейт, падающий на враждебном входе, не даёт вердикта вообще.
1050
+ let inputPresent: boolean;
1051
+ try {
1052
+ inputPresent = hasInput === undefined ? true : hasInput(facts);
1053
+ } catch (error) {
1054
+ violations.push({
1055
+ rule: r.id, severity: r.severity,
1056
+ detail: `улики правила нечитаемы (${error instanceof Error ? error.message : String(error)}) — правило считается НАРУШЕННЫМ, а не пройденным`,
1057
+ });
1058
+ checked.push(r.id);
1059
+ continue;
1060
+ }
1061
+ if (!inputPresent) {
1062
+ // A rule with nothing to measure cannot produce a positive receipt. Keep the verdict unchanged,
1063
+ // but record the missing input explicitly instead of calling the rule checked.
1064
+ notEstablished.push(r.id);
1065
+ continue;
1066
+ }
760
1067
  checked.push(r.id);
761
1068
  if ((VOLUME_SHADOW_RULE_IDS as readonly string[]).includes(r.id)) {
762
1069
  const emission = volume();
@@ -795,6 +1102,16 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
795
1102
  notes.push(`no-stubs: ${skipped} changed scannable file(s) not scanned (deleted/oversize/unreadable/beyond the file cap) — the stub scan is fail-open, so this is a coverage gap on the record, not a violation`);
796
1103
  }
797
1104
  }
1105
+ if (checked.includes('no-secrets') || notEstablished.includes('no-secrets')) {
1106
+ const reasonless = secretWaiverState(facts.secretWaivers).reasonless;
1107
+ if (reasonless > 0) {
1108
+ notes.push(`no-secrets: ${reasonless} reasonless secret waiver(s) ignored — add a non-empty reason or remove the entry`);
1109
+ }
1110
+ const skipped = facts.secretScan?.skipped;
1111
+ if (typeof skipped === 'number' && Number.isFinite(skipped) && skipped > 0) {
1112
+ notes.push(`no-secrets: ${skipped} packed inventory item(s) not scanned (oversize/unreadable/binary) — the secret scan is fail-open, so this is a coverage gap on the record, not a violation`);
1113
+ }
1114
+ }
798
1115
  if (checked.includes('review-round') && facts.reviewRound?.gathered === false) {
799
1116
  // A HARD gate that passes SILENTLY when it could not gather its evidence is a gate you cannot
800
1117
  // tell from one that checked and approved (raised by cross-family review). It still does not
@@ -821,6 +1138,7 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
821
1138
  verdict,
822
1139
  violations,
823
1140
  checked,
1141
+ notEstablished,
824
1142
  ...(notes.length > 0 ? { notes } : {}),
825
1143
  ...(observations.length > 0 ? { observations } : {}),
826
1144
  };
@@ -0,0 +1,44 @@
1
+ // Where THIS harness-core installation lives on disk (feature `destructive-command-guard`, task T8,
2
+ // cross-family review round 3, both P1 findings).
3
+ //
4
+ // WHY A GUARD NEEDS TO KNOW ITS OWN ADDRESS. Both emitted hook bodies — the Claude PreToolUse hook
5
+ // and the Codex veto helper — are files that run OUTSIDE this package: one in a consumer's
6
+ // `.claude/hooks/`, one in `$CODEX_HOME/dz-hooks/`. They cannot `require('@dzhechkov/harness-core')`,
7
+ // because Node resolves a bare specifier from the FILE's location, and neither location has the
8
+ // package above it. So they resolve by PATH — and the paths they were given were all relative to the
9
+ // project being guarded. MEASURED 2026-09-05, the documented global-install workflow
10
+ // (`npm install -g @dzhechkov/harness-cli`, then a target repo with no local install):
11
+ // every candidate → ERR_MODULE_NOT_FOUND, helper exits 0, `rm -rf .agentic-qe` ALLOWED.
12
+ // The guard was advertised, installed, trusted — and inert. The tests hid it by symlinking the
13
+ // package into the temp project, which is exactly the condition a real target repo does not have.
14
+ //
15
+ // The fix is to bake the EMITTING installation's absolute path into the body at emission time, as a
16
+ // FALLBACK after the project-local candidates: a consumer that has its own harness-core keeps using
17
+ // its own copy, and everyone else reaches the installation that wrote the hook.
18
+
19
+ import { dirname } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ /**
23
+ * The directory holding this module's own compiled siblings.
24
+ *
25
+ * In a published install that is `<pkg>/dist`; under vitest, where the SOURCE is executed, it is
26
+ * `<pkg>/src`. Callers that need a BUILT sibling must therefore ask for {@link harnessCoreDistDir},
27
+ * which normalises the second case — a body that baked `<pkg>/src/foo.js` would be a path that
28
+ * exists in no installation at all.
29
+ */
30
+ export function harnessCoreModuleDir(): string {
31
+ return dirname(fileURLToPath(import.meta.url));
32
+ }
33
+
34
+ /**
35
+ * The directory the emitted hook bodies must point at: the one holding the BUILT modules.
36
+ *
37
+ * `src` → its sibling `dist`, anything else → itself. The mapping is deliberately this narrow: the
38
+ * only two layouts that exist are "running the build" and "running the sources under the test
39
+ * runner", and inventing a search would turn a fact into a guess.
40
+ */
41
+ export function harnessCoreDistDir(): string {
42
+ const dir = harnessCoreModuleDir();
43
+ return dir.endsWith('/src') || dir.endsWith('\\src') ? `${dir.slice(0, -3)}dist` : dir;
44
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,39 @@ export const HARNESS_CORE_VERSION: string =
12
12
 
13
13
  export * from './skills.js';
14
14
  export * from './apply.js';
15
+ export {
16
+ buildPublicSnapshot,
17
+ assertPublicSafe,
18
+ applyDeltaRule,
19
+ HOLD_CAP_DAYS,
20
+ PUBLIC_SCHEMA,
21
+ MIN_GROUP,
22
+ TTT_BUCKETS,
23
+ type PrivateRecord,
24
+ type PublicSnapshot,
25
+ type BuildResult,
26
+ type RefusalCode,
27
+ type EgressVerdict,
28
+ type DeltaOutcome,
29
+ } from './backlog-public.js';
30
+
31
+ export {
32
+ appendTransition,
33
+ readTransitions,
34
+ transitionLogPath,
35
+ type TransitionRow,
36
+ } from './backlog-transitions.js';
37
+
38
+ export {
39
+ decideStop,
40
+ hasProgress,
41
+ BLOCK_BUDGET,
42
+ FRESH_HUMAN_MINUTES,
43
+ type LeadShift,
44
+ type ProgressEvidence,
45
+ type StopDecision,
46
+ } from './lead-shift-gate.js';
47
+
15
48
  export { bundleSkills } from './bundle.js';
16
49
  export type { BundleOptions, BundleResult, BundledSkill } from './bundle.js';
17
50
  export * from './targets.js';
@@ -75,6 +108,7 @@ export * from './trace-bundle.js';
75
108
  export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
76
109
  export type { LoopBlob } from './loop-blobs.generated.js';
77
110
  export * from './sign.js';
111
+ export * from './swarm-brief.js';
78
112
  export * from './skill-schema.js';
79
113
  export { createSkill } from './create-skill.js';
80
114
  export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
@@ -192,8 +226,32 @@ export type {
192
226
  } from './vector-tier.js';
193
227
  export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION,
194
228
  agentdbStorePath, agentdbMcpStorePath, agentdbStoreSeparationProblem } from './setup.js';
195
- export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath } from './statusline.js';
196
- export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
229
+ export { countLearningStoreRowsReadonly } from './store-counts.js';
230
+ export type { LearningStoreRowCounts } from './store-counts.js';
231
+ export {
232
+ STORE_GUARD_VERSION,
233
+ STORE_COLLAPSE_MAX_FRACTION,
234
+ STORE_COLLAPSE_LAST_ROWS,
235
+ storeGuardPath,
236
+ storeSnapshotPath,
237
+ readStoreMark,
238
+ writeStoreMark,
239
+ resetStoreMark,
240
+ checkStoreHealth,
241
+ } from './store-guard.js';
242
+ export type {
243
+ StoreMark,
244
+ StoreHealth,
245
+ StoreHealthInput,
246
+ StoreHealthVerdict,
247
+ StoreCountSnapshot,
248
+ StoreCountSource,
249
+ StoreMarkObservation,
250
+ StoreMarkWriteOptions,
251
+ StoreResetReceipt,
252
+ } from './store-guard.js';
253
+ export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath, writeFeatureAdrStateDetailed, renderFeatureAdrPhaseLine } from './statusline.js';
254
+ export type { StatuslineData, StatuslineStoreHealth, FeatureAdrState, WriteFeatureAdrStateInput, WriteFeatureAdrStateResult } from './statusline.js';
197
255
  export {
198
256
  ETA_MAX_STAGE_MS,
199
257
  estimateEta,
@@ -362,6 +420,10 @@ export {
362
420
  amendmentsMissingFromPlan,
363
421
  amendmentSubject,
364
422
  extractTestTitles,
423
+ amendmentIdsIn,
424
+ amendmentSectionCount,
425
+ amendmentDeclarationAmbiguity,
426
+ mentionsAmendmentId,
365
427
  } from './amendment-trace.js';
366
428
  export {
367
429
  RECORD_MAX_LINE_CHARS,
@@ -372,7 +434,7 @@ export {
372
434
  export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
373
435
  export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
374
436
  export type { RecordKind, RecordVerdict, RecordDecision } from './run-records.js';
375
- export type { AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
437
+ export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
376
438
  // contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
377
439
  // exact per-item verification. Filesystem discovery/containment stays in harness-cli.
378
440
  export {
@@ -598,6 +660,7 @@ export type {
598
660
  ChainedJournal,
599
661
  } from './event-chain.js';
600
662
  export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
663
+ export * from './course-staleness.js';
601
664
  export { fetchAllDownloads } from './downloads.js';
602
665
  export type { PackageDownloads, DownloadsReport } from './downloads.js';
603
666
  export { discoverInstalled, checkUpgrades } from './upgrade.js';
@@ -778,10 +841,17 @@ export {
778
841
  codexQeDeclineReason,
779
842
  codexDeclineReason,
780
843
  parseCodexReviewResult,
844
+ resolveStageDecision,
845
+ effectiveSpec,
846
+ specFamily,
847
+ qeReasonForFamilies,
848
+ STAGE_DECISION_REASONS,
781
849
  } from './feature-adr-routing.js';
782
850
  export type {
783
851
  StageOpts,
784
852
  RoutingEnv,
853
+ StageDecision,
854
+ StageDecisionReason,
785
855
  CrossFamilyQeReport,
786
856
  ModeBScopeVerdict,
787
857
  PartitionedFindings,
@@ -812,6 +882,15 @@ export type {
812
882
  CodexQeDeclineDetail,
813
883
  CodexReviewResult,
814
884
  } from './feature-adr-routing.js';
885
+ export {
886
+ STAGE_LINE_BLOB_VERSION,
887
+ STAGE_LINE_REASON_LABELS,
888
+ STAGE_LINE_BARRIER_STAGES,
889
+ renderStageIntentLine,
890
+ renderStageOutcomeLine,
891
+ renderStageLine,
892
+ } from './stage-line.js';
893
+ export type { StageLineDecision, StageLineOutcome, StageLineOutcomeState } from './stage-line.js';
815
894
  export {
816
895
  PARSER_SAFE_REGION_START,
817
896
  PARSER_SAFE_REGION_END,
@@ -1043,3 +1122,32 @@ export type {
1043
1122
  } from './profile.js';
1044
1123
  export * from './codex-invoke.js';
1045
1124
  export * from './skill-selection.js';
1125
+
1126
+ // destructive-guard (feature destructive-command-guard, ADR-001) — pure pre-execution verdict on a
1127
+ // shell command. Refuses ONLY on a literal path into a protected store; `undecidable` is never
1128
+ // passed off as `refuse`.
1129
+ export { classifyDestructive, DESTRUCTIVE_RULES } from './destructive-guard.js';
1130
+ export type { DestructiveVerdict } from './destructive-guard.js';
1131
+
1132
+ // The CONSUMER half (task T8): one decision for both hosts — the Claude PreToolUse hook
1133
+ // (`.claude/hooks/destructive-guard.cjs`) and the emitted Codex veto helper call the SAME function,
1134
+ // so a refusal is worded identically on both runtimes.
1135
+ export {
1136
+ decideDestructiveHook,
1137
+ DESTRUCTIVE_REFUSE_MARKER,
1138
+ DESTRUCTIVE_WARN_MARKER,
1139
+ } from './destructive-guard-hook.js';
1140
+ export type { DestructiveHookDecision, DestructiveHookHost } from './destructive-guard-hook.js';
1141
+
1142
+ // Delivery of the guard into OTHER projects (task T8, review round 3): the installable Claude hook
1143
+ // body + its registry entry, and the locator that lets an emitted body find the installation that
1144
+ // wrote it. Both hosts' bodies resolve harness-core by PATH, never by bare specifier.
1145
+ export {
1146
+ generateClaudeDestructiveHook,
1147
+ CLAUDE_DESTRUCTIVE_HOOK_COMMAND,
1148
+ CLAUDE_DESTRUCTIVE_HOOK_MATCHER,
1149
+ CLAUDE_DESTRUCTIVE_HOOK_RELPATH,
1150
+ DZ_CLAUDE_HOOK_VERSION,
1151
+ } from './claude-hooks-assets.js';
1152
+ export type { ClaudeHookAssetOptions } from './claude-hooks-assets.js';
1153
+ export { harnessCoreDistDir, harnessCoreModuleDir } from './harness-core-location.js';