@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.
- package/.dz-manifest.json +399 -139
- package/LICENSE +13 -0
- package/README.md +420 -6
- package/dist/agents-policy.d.ts +15 -1
- package/dist/agents-policy.d.ts.map +1 -1
- package/dist/agents-policy.js +27 -1
- package/dist/agents-policy.js.map +1 -1
- package/dist/amendment-trace.d.ts +72 -0
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +385 -17
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog-public.d.ts +153 -0
- package/dist/backlog-public.d.ts.map +1 -0
- package/dist/backlog-public.js +415 -0
- package/dist/backlog-public.js.map +1 -0
- package/dist/backlog-transitions.d.ts +48 -0
- package/dist/backlog-transitions.d.ts.map +1 -0
- package/dist/backlog-transitions.js +64 -0
- package/dist/backlog-transitions.js.map +1 -0
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +13 -0
- package/dist/backlog.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +24 -2
- package/dist/claim-check.js.map +1 -1
- package/dist/claude-hooks-assets.d.ts +93 -0
- package/dist/claude-hooks-assets.d.ts.map +1 -0
- package/dist/claude-hooks-assets.js +208 -0
- package/dist/claude-hooks-assets.js.map +1 -0
- package/dist/clean-room-smoke.d.ts +83 -0
- package/dist/clean-room-smoke.d.ts.map +1 -0
- package/dist/clean-room-smoke.js +138 -0
- package/dist/clean-room-smoke.js.map +1 -0
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +36 -6
- package/dist/cmd-usage.js.map +1 -1
- package/dist/codex-hooks-assets.d.ts +25 -7
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +138 -30
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/codex-hooks.d.ts +21 -1
- package/dist/codex-hooks.d.ts.map +1 -1
- package/dist/codex-hooks.js +21 -1
- package/dist/codex-hooks.js.map +1 -1
- package/dist/course-staleness.d.ts +19 -0
- package/dist/course-staleness.d.ts.map +1 -0
- package/dist/course-staleness.js +95 -0
- package/dist/course-staleness.js.map +1 -0
- package/dist/destructive-guard-hook.d.ts +40 -0
- package/dist/destructive-guard-hook.d.ts.map +1 -0
- package/dist/destructive-guard-hook.js +109 -0
- package/dist/destructive-guard-hook.js.map +1 -0
- package/dist/destructive-guard.d.ts +27 -0
- package/dist/destructive-guard.d.ts.map +1 -0
- package/dist/destructive-guard.js +2808 -0
- package/dist/destructive-guard.js.map +1 -0
- package/dist/discrimination-gate.d.ts +28 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +76 -16
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +95 -1
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +193 -27
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +90 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +271 -0
- package/dist/guard.js.map +1 -1
- package/dist/harness-core-location.d.ts +18 -0
- package/dist/harness-core-location.d.ts.map +1 -0
- package/dist/harness-core-location.js +42 -0
- package/dist/harness-core-location.js.map +1 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -3
- package/dist/index.js.map +1 -1
- package/dist/lead-shift-gate.d.ts +95 -0
- package/dist/lead-shift-gate.d.ts.map +1 -0
- package/dist/lead-shift-gate.js +100 -0
- package/dist/lead-shift-gate.js.map +1 -0
- package/dist/ledger-backfill.d.ts +11 -1
- package/dist/ledger-backfill.d.ts.map +1 -1
- package/dist/ledger-backfill.js +19 -0
- package/dist/ledger-backfill.js.map +1 -1
- package/dist/loop-blobs.generated.d.ts +1 -1
- package/dist/loop-blobs.generated.d.ts.map +1 -1
- package/dist/loop-blobs.generated.js +12 -3
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-lint.d.ts.map +1 -1
- package/dist/loop-lint.js +56 -7
- package/dist/loop-lint.js.map +1 -1
- package/dist/loop-plan-graph.d.ts +1 -3
- package/dist/loop-plan-graph.d.ts.map +1 -1
- package/dist/loop-plan-graph.js +70 -1
- package/dist/loop-plan-graph.js.map +1 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +16 -2
- package/dist/loop-trace.js.map +1 -1
- package/dist/managed-hooks.d.ts +5 -6
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +2 -2
- package/dist/managed-hooks.js.map +1 -1
- package/dist/model-recommender.d.ts.map +1 -1
- package/dist/model-recommender.js +14 -3
- package/dist/model-recommender.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +82 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +23 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +10 -0
- package/dist/patterns.js.map +1 -1
- package/dist/publish.d.ts +11 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +16 -2
- package/dist/publish.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -2
- package/dist/registry.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +15 -4
- package/dist/score.js.map +1 -1
- package/dist/session-retro.d.ts +121 -2
- package/dist/session-retro.d.ts.map +1 -1
- package/dist/session-retro.js +454 -11
- package/dist/session-retro.js.map +1 -1
- package/dist/setup.d.ts +28 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +223 -6
- package/dist/setup.js.map +1 -1
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +18 -1
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +34 -1
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +82 -0
- package/dist/skills-verify.js.map +1 -1
- package/dist/stage-line.d.ts +68 -0
- package/dist/stage-line.d.ts.map +1 -0
- package/dist/stage-line.js +129 -0
- package/dist/stage-line.js.map +1 -0
- package/dist/statusline.d.ts +99 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +310 -44
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +26 -0
- package/dist/store-counts.d.ts.map +1 -0
- package/dist/store-counts.js +125 -0
- package/dist/store-counts.js.map +1 -0
- package/dist/store-guard.d.ts +106 -0
- package/dist/store-guard.d.ts.map +1 -0
- package/dist/store-guard.js +294 -0
- package/dist/store-guard.js.map +1 -0
- package/dist/swarm-brief.d.ts +95 -0
- package/dist/swarm-brief.d.ts.map +1 -0
- package/dist/swarm-brief.js +660 -0
- package/dist/swarm-brief.js.map +1 -0
- package/dist/trace-bundle.d.ts +8 -0
- package/dist/trace-bundle.d.ts.map +1 -1
- package/dist/trace-bundle.js +11 -0
- package/dist/trace-bundle.js.map +1 -1
- package/package.json +12 -11
- package/sbom.json +804 -154
- package/src/agents-policy.ts +46 -2
- package/src/amendment-trace.ts +441 -19
- package/src/backlog-public.ts +503 -0
- package/src/backlog-transitions.ts +77 -0
- package/src/backlog.ts +12 -0
- package/src/claim-check.ts +25 -2
- package/src/claude-hooks-assets.ts +227 -0
- package/src/clean-room-smoke.ts +195 -0
- package/src/cmd-usage.ts +29 -5
- package/src/codex-hooks-assets.ts +140 -30
- package/src/codex-hooks.ts +21 -1
- package/src/course-staleness.ts +125 -0
- package/src/destructive-guard-hook.ts +151 -0
- package/src/destructive-guard.ts +3027 -0
- package/src/discrimination-gate.ts +98 -19
- package/src/feature-adr-routing.ts +220 -22
- package/src/guard.ts +318 -0
- package/src/harness-core-location.ts +44 -0
- package/src/index.ts +111 -3
- package/src/lead-shift-gate.ts +145 -0
- package/src/ledger-backfill.ts +20 -1
- package/src/loop-blobs.generated.ts +12 -3
- package/src/loop-lint.ts +52 -7
- package/src/loop-plan-graph.ts +66 -1
- package/src/loop-trace.ts +13 -1
- package/src/managed-hooks.ts +5 -6
- package/src/model-recommender.ts +14 -3
- package/src/operations.ts +75 -0
- package/src/patterns.ts +33 -0
- package/src/publish.ts +27 -2
- package/src/registry.ts +3 -2
- package/src/score.ts +16 -4
- package/src/session-retro.ts +466 -11
- package/src/setup.ts +246 -9
- package/src/sign.ts +18 -1
- package/src/skills-verify.ts +99 -1
- package/src/stage-line.ts +151 -0
- package/src/statusline.ts +396 -47
- package/src/store-counts.ts +154 -0
- package/src/store-guard.ts +388 -0
- package/src/swarm-brief.ts +661 -0
- 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 {
|
|
196
|
-
export type {
|
|
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';
|