@dzhechkov/harness-core 0.8.38 → 0.8.39
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 +223 -123
- package/README.md +97 -2
- package/dist/agentdb-index.d.ts +3 -3
- package/dist/agentdb-index.js +5 -5
- package/dist/agentdb-index.js.map +1 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +4 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog.d.ts +2 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +3 -2
- package/dist/backlog.js.map +1 -1
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +9 -2
- package/dist/brain.js.map +1 -1
- package/dist/bto-optimize.d.ts.map +1 -1
- package/dist/bto-optimize.js +9 -12
- package/dist/bto-optimize.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +50 -14
- package/dist/claim-check.js.map +1 -1
- package/dist/experiment-assign.d.ts +110 -0
- package/dist/experiment-assign.d.ts.map +1 -0
- package/dist/experiment-assign.js +229 -0
- package/dist/experiment-assign.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +7 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +20 -5
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +12 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -1
- package/dist/feature-adr-envelope.js +12 -1
- package/dist/feature-adr-envelope.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +4 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +4 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/ledger-cost-fill.d.ts +58 -0
- package/dist/ledger-cost-fill.d.ts.map +1 -0
- package/dist/ledger-cost-fill.js +78 -0
- package/dist/ledger-cost-fill.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +29 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +20 -0
- package/dist/mutation-gate.js.map +1 -1
- package/dist/name-check.d.ts +20 -1
- package/dist/name-check.d.ts.map +1 -1
- package/dist/name-check.js +42 -1
- package/dist/name-check.js.map +1 -1
- package/dist/no-stubs.d.ts +10 -0
- package/dist/no-stubs.d.ts.map +1 -1
- package/dist/no-stubs.js +13 -9
- package/dist/no-stubs.js.map +1 -1
- package/dist/patterns.d.ts +24 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +49 -9
- package/dist/patterns.js.map +1 -1
- package/dist/publish-source-scope.d.ts +32 -0
- package/dist/publish-source-scope.d.ts.map +1 -0
- package/dist/publish-source-scope.js +53 -0
- package/dist/publish-source-scope.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +32 -21
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +4 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +21 -11
- package/dist/qe-bridge.js.map +1 -1
- package/dist/rake-analyzer.d.ts +10 -3
- package/dist/rake-analyzer.d.ts.map +1 -1
- package/dist/rake-analyzer.js +84 -22
- package/dist/rake-analyzer.js.map +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +8 -4
- package/dist/recap.js.map +1 -1
- package/dist/release.d.ts +2 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +18 -4
- package/dist/release.js.map +1 -1
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +11 -1
- package/dist/reqe.js.map +1 -1
- package/dist/run-records.d.ts +18 -4
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +61 -7
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +8 -1
- package/dist/score.js.map +1 -1
- package/dist/sign.d.ts +23 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +8 -5
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +59 -7
- package/dist/skills-verify.js.map +1 -1
- package/dist/store-guard-prune.d.ts +22 -0
- package/dist/store-guard-prune.d.ts.map +1 -0
- package/dist/store-guard-prune.js +54 -0
- package/dist/store-guard-prune.js.map +1 -0
- package/dist/sweep-failure-classify.d.ts +45 -0
- package/dist/sweep-failure-classify.d.ts.map +1 -0
- package/dist/sweep-failure-classify.js +90 -0
- package/dist/sweep-failure-classify.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +14 -2
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +14 -2
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +372 -122
- package/src/agentdb-index.ts +5 -5
- package/src/amendment-trace.ts +4 -1
- package/src/backlog.ts +3 -2
- package/src/brain.ts +8 -2
- package/src/bto-optimize.ts +10 -8
- package/src/claim-check.ts +48 -12
- package/src/experiment-assign.ts +276 -0
- package/src/feature-adr-checkpoints.ts +20 -5
- package/src/feature-adr-envelope.ts +24 -1
- package/src/feature-adr-routing.ts +4 -0
- package/src/index.ts +17 -3
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +59 -0
- package/src/name-check.ts +43 -1
- package/src/no-stubs.ts +14 -5
- package/src/patterns.ts +49 -8
- package/src/publish-source-scope.ts +53 -0
- package/src/publish.ts +38 -16
- package/src/qe-bridge.ts +22 -11
- package/src/rake-analyzer.ts +75 -20
- package/src/rake-signatures.json +98 -0
- package/src/recap.ts +8 -4
- package/src/release.ts +18 -4
- package/src/reqe.ts +11 -1
- package/src/run-records.ts +78 -10
- package/src/score.ts +8 -1
- package/src/sign.ts +53 -0
- package/src/skills-verify.ts +69 -9
- package/src/store-guard-prune.ts +81 -0
- package/src/sweep-failure-classify.ts +88 -0
- package/src/workflow-run-dispatch.ts +14 -2
package/src/skills-verify.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* the PROPERTY (registration). This module makes the property observable.
|
|
7
7
|
*
|
|
8
8
|
* Two layers:
|
|
9
|
-
* L1 `scanSkillsLayout` — instant, no Claude session:
|
|
10
|
-
*
|
|
9
|
+
* L1 `scanSkillsLayout` — instant, no Claude session: names whose on-disk form passes the
|
|
10
|
+
* static checks, plus known issue shapes.
|
|
11
11
|
* L2 `parseInitFacts` — the authoritative listing, parsed from the `system/init` event of
|
|
12
12
|
* + `classifyRegistration` `claude -p --output-format stream-json --verbose`. No model prose.
|
|
13
13
|
*
|
|
@@ -19,13 +19,15 @@ import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
|
19
19
|
|
|
20
20
|
// ── Layer 1: static layout scan ─────────────────────────────────────
|
|
21
21
|
|
|
22
|
-
/** The
|
|
22
|
+
/** The seven static issue shapes this scanner reports. */
|
|
23
23
|
export type SkillIssueKind =
|
|
24
24
|
| 'no-skill-md' // a skill dir with no SKILL.md at depth 1 → never registers
|
|
25
25
|
| 'buried-skill-md' // a SKILL.md at depth >= 2 → the loader does not scan that deep
|
|
26
26
|
| 'plugin-manifest-trap' // .claude-plugin/plugin.json under .claude/skills → does NOT auto-register
|
|
27
27
|
| 'wildcard-allowed-tools' // `allowed-tools: *` → every tool granted; a FINDING (see below)
|
|
28
|
-
| 'empty-allowed-tools'
|
|
28
|
+
| 'empty-allowed-tools' // `allowed-tools:` with no value → looks restrictive, restricts nothing
|
|
29
|
+
| 'missing-frontmatter-fence' // measured absent from the registry without an opening `---`
|
|
30
|
+
| 'missing-description'; // measured absent from the registry without a non-empty description
|
|
29
31
|
|
|
30
32
|
export interface SkillLayoutFinding {
|
|
31
33
|
readonly dir: string;
|
|
@@ -38,7 +40,10 @@ export interface StaticScan {
|
|
|
38
40
|
readonly projectDir: string;
|
|
39
41
|
readonly skillsRoot: string;
|
|
40
42
|
readonly exists: boolean;
|
|
41
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Bare skill names whose on-disk form passes static layout and measured frontmatter checks.
|
|
45
|
+
* Actual registration is established only by the Layer-2 session listing.
|
|
46
|
+
*/
|
|
42
47
|
readonly registrable: readonly string[];
|
|
43
48
|
/** Load-blocking problems: these make the verdict FAIL. */
|
|
44
49
|
readonly findings: readonly SkillLayoutFinding[];
|
|
@@ -228,6 +233,39 @@ export function parseAllowedTools(markdown: string): AllowedToolsState {
|
|
|
228
233
|
return { state: 'listed', values, wildcard: values.includes('*') };
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
type SkillFrontmatterIssue = 'missing-frontmatter-fence' | 'missing-description';
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read the registration-relevant frontmatter fields, line by line and only before its closing fence.
|
|
240
|
+
* Scanning the whole file would let `description:` in skill documentation suppress a real finding,
|
|
241
|
+
* the same false-positive/false-negative boundary guarded by `parseAllowedTools` above.
|
|
242
|
+
*
|
|
243
|
+
* The two conditions are measured as absence from the registry, not refusal of invocation. Each
|
|
244
|
+
* measurement used a fresh session, so it also does not establish appearance in the same response
|
|
245
|
+
* without a restart.
|
|
246
|
+
*/
|
|
247
|
+
function findSkillFrontmatterIssue(markdown: string): SkillFrontmatterIssue | null {
|
|
248
|
+
const lines = markdown.split(/\r?\n/);
|
|
249
|
+
// NO explicit BOM strip, and that is deliberate: `String.prototype.trim()` already removes U+FEFF
|
|
250
|
+
// (it is <ZWNBSP>, part of the WhiteSpace production), so a BOM-prefixed fence compares equal to
|
|
251
|
+
// a bare one. MEASURED 2026-09-19: adding a strip left the mutant "don't strip" 70/70 green —
|
|
252
|
+
// dead code. The tolerance is pinned by a test instead, so replacing `trim()` with anything
|
|
253
|
+
// stricter goes red rather than silently rejecting every BOM-prefixed skill.
|
|
254
|
+
if (lines[0]?.trim() !== '---') return 'missing-frontmatter-fence';
|
|
255
|
+
|
|
256
|
+
let description: string | null = null;
|
|
257
|
+
for (let i = 1; i < lines.length; i++) {
|
|
258
|
+
const line = lines[i] ?? '';
|
|
259
|
+
if (line.trim() === '---') break;
|
|
260
|
+
const match = /^description:(.*)$/.exec(line);
|
|
261
|
+
if (match) {
|
|
262
|
+
description = match[1] ?? '';
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return description === null || description.trim() === '' ? 'missing-description' : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
231
269
|
/**
|
|
232
270
|
* Privilege findings for one skill directory.
|
|
233
271
|
*
|
|
@@ -426,16 +464,38 @@ export function scanSkillsLayout(projectDir: string): StaticScan {
|
|
|
426
464
|
const isPluginContainer = hasPluginManifest(dir);
|
|
427
465
|
const bucket = isPluginContainer ? advisories : findings;
|
|
428
466
|
|
|
467
|
+
// Read under a guard, exactly like scanSkillPrivileges: an unreadable SKILL.md is another
|
|
468
|
+
// check's business, and this scanner's contract is to REPORT findings, never to throw out of
|
|
469
|
+
// the whole sweep over one bad file.
|
|
470
|
+
let frontmatterIssue: SkillFrontmatterIssue | null = null;
|
|
471
|
+
if (registers) {
|
|
472
|
+
try {
|
|
473
|
+
frontmatterIssue = findSkillFrontmatterIssue(readFileSync(join(dir, 'SKILL.md'), 'utf8'));
|
|
474
|
+
} catch {
|
|
475
|
+
frontmatterIssue = null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (frontmatterIssue) {
|
|
479
|
+
findings.push({
|
|
480
|
+
dir: name,
|
|
481
|
+
kind: frontmatterIssue,
|
|
482
|
+
detail: frontmatterIssue === 'missing-frontmatter-fence'
|
|
483
|
+
? 'SKILL.md has no opening frontmatter fence — measured absent from the registry without it'
|
|
484
|
+
: 'SKILL.md has no non-empty frontmatter description — measured absent from the registry without one',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
429
488
|
// Права проверяются у КАЖДОГО навыка с читаемым SKILL.md, включая одно-навыковый плагин: щедрая
|
|
430
489
|
// выдача не становится безопаснее оттого, что навык лежит в контейнере.
|
|
431
490
|
if (registers) scanSkillPrivileges(dir, name, findings, advisories);
|
|
432
491
|
|
|
433
|
-
if (registers
|
|
434
|
-
|
|
435
|
-
|
|
492
|
+
if (registers) {
|
|
493
|
+
if (!isPluginContainer && !frontmatterIssue) {
|
|
494
|
+
registrable.push(name);
|
|
495
|
+
}
|
|
436
496
|
// A single-skill plugin: it has a depth-1 SKILL.md AND a manifest, so it registers NAMESPACED.
|
|
437
497
|
// Expecting the bare directory name here was a false FAIL (QE5 #2); the container check below
|
|
438
|
-
// accepts either form.
|
|
498
|
+
// accepts either form. A frontmatter finding likewise keeps the bare name out of registrable.
|
|
439
499
|
} else {
|
|
440
500
|
// A dir with no markdown was never meant to be a skill — advisory, not a failure (QE5 #7).
|
|
441
501
|
const intended = looksLikeSkillDir(dir);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { sep } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export type StoreGuardPruneBucket = 'stale-temp' | 'live' | 'gone-outside-tmp' | 'unreadable';
|
|
4
|
+
|
|
5
|
+
export interface StoreGuardPruneEntry {
|
|
6
|
+
readonly file: string;
|
|
7
|
+
readonly project: string | null;
|
|
8
|
+
readonly bytes: number;
|
|
9
|
+
readonly bucket: StoreGuardPruneBucket;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StoreGuardPrunePlan {
|
|
13
|
+
readonly entries: readonly StoreGuardPruneEntry[];
|
|
14
|
+
readonly counts: Readonly<Record<StoreGuardPruneBucket, number>>;
|
|
15
|
+
readonly reclaimableBytes: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StoreGuardPruneDeps {
|
|
19
|
+
readonly exists: (path: string) => boolean;
|
|
20
|
+
readonly tmpDirs: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A temp-root candidate that authorizes deletion must be a REAL directory path, never the filesystem
|
|
25
|
+
* root or an empty string. MEASURED 2026-09-20 (cross-family review, P1): `TMPDIR=/` makes
|
|
26
|
+
* `os.tmpdir()` return the single character `"/"` (node strips a trailing slash only when the path is
|
|
27
|
+
* longer than one character), and `isUnder` then matches EVERY absolute path — so every
|
|
28
|
+
* `gone-outside-tmp` mark, which FR-3 exists to protect, would be classified `stale-temp` and deleted.
|
|
29
|
+
* A degenerate candidate authorizes nothing and is dropped here, in the pure half, where it is testable.
|
|
30
|
+
*/
|
|
31
|
+
function isUsableTmpDir(tmpDir: string): boolean {
|
|
32
|
+
const trimmed = tmpDir.trim();
|
|
33
|
+
return trimmed.length > 1 && trimmed !== sep && trimmed !== '/';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isUnder(project: string, tmpDir: string): boolean {
|
|
37
|
+
const prefix = tmpDir.endsWith(sep) ? tmpDir : `${tmpDir}${sep}`;
|
|
38
|
+
return project === tmpDir || project.startsWith(prefix);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function planStoreGuardPrune(
|
|
42
|
+
input: readonly { readonly file: string; readonly bytes: number; readonly text: string }[],
|
|
43
|
+
deps: StoreGuardPruneDeps,
|
|
44
|
+
): StoreGuardPrunePlan {
|
|
45
|
+
const counts: Record<StoreGuardPruneBucket, number> = {
|
|
46
|
+
'stale-temp': 0,
|
|
47
|
+
live: 0,
|
|
48
|
+
'gone-outside-tmp': 0,
|
|
49
|
+
unreadable: 0,
|
|
50
|
+
};
|
|
51
|
+
let reclaimableBytes = 0;
|
|
52
|
+
|
|
53
|
+
const entries = input.map(({ file, bytes, text }): StoreGuardPruneEntry => {
|
|
54
|
+
let value: unknown;
|
|
55
|
+
try {
|
|
56
|
+
value = JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
counts.unreadable += 1;
|
|
59
|
+
return { file, project: null, bytes, bucket: 'unreadable' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof value !== 'object' || value === null
|
|
63
|
+
|| typeof (value as { project?: unknown }).project !== 'string'
|
|
64
|
+
|| (value as { project: string }).project.length === 0) {
|
|
65
|
+
counts.unreadable += 1;
|
|
66
|
+
return { file, project: null, bytes, bucket: 'unreadable' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const project = (value as { project: string }).project;
|
|
70
|
+
const bucket: StoreGuardPruneBucket = deps.exists(project)
|
|
71
|
+
? 'live'
|
|
72
|
+
: deps.tmpDirs.filter(isUsableTmpDir).some((tmpDir) => isUnder(project, tmpDir))
|
|
73
|
+
? 'stale-temp'
|
|
74
|
+
: 'gone-outside-tmp';
|
|
75
|
+
counts[bucket] += 1;
|
|
76
|
+
if (bucket === 'stale-temp') reclaimableBytes += bytes;
|
|
77
|
+
return { file, project, bytes, bucket };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { entries, counts, reclaimableBytes };
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Различитель красных обхода: ЛОГИЧЕСКИЙ отказ против отказа ПОД НАГРУЗКОЙ.
|
|
3
|
+
*
|
|
4
|
+
* ЗАЧЕМ. Запись 9e48a975 установила класс: набор harness-core детерминирован В ОДИНОЧКУ и
|
|
5
|
+
* краснеет ТОЛЬКО под параллельной нагрузкой (пять последовательных прогонов — ноль красных;
|
|
6
|
+
* те же тесты под фоновым вторым набором — красные поодиночке и в разных сочетаниях).
|
|
7
|
+
* Подпись класса: тест (а) читает собранный `dist`, (б) порождает процессы, (в) несёт явный
|
|
8
|
+
* таймаут, близкий к измеренному времени.
|
|
9
|
+
*
|
|
10
|
+
* ИЗМЕРЕНО 2026-09-19, новый экземпляр того же класса: `scout` прошёл в одном обходе и упал в
|
|
11
|
+
* следующем через 17 минут — `deep-analyzer.test.ts`, `Test timed out in 5000ms`, 192 из 193
|
|
12
|
+
* зелёные; тот же файл в одиночку 5 из 5. Единственное различие между прогонами — загрузка
|
|
13
|
+
* машины (подкачка 2342 → 2671 МБ).
|
|
14
|
+
*
|
|
15
|
+
* Класс диагностирован, но МЕХАНИЗМА не имел: каждый экземпляр стоил ручного разбора. Этот
|
|
16
|
+
* модуль — чистая половина механизма: он извлекает из журнала обхода ИМЕНА упавших файлов и
|
|
17
|
+
* классифицирует результат ПЕРЕПРОГОНА В ОДИНОЧКУ. Запуск живёт в скрипте, здесь его нет.
|
|
18
|
+
*
|
|
19
|
+
* ТРЁХЗНАЧНОСТЬ ОБЯЗАТЕЛЬНА. Перепрогон, который не удалось выполнить, — это НЕ «прошёл»;
|
|
20
|
+
* прибор, который не смог посмотреть, не имеет права на вердикт.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Вердикт перепрогона одного файла в одиночку. */
|
|
24
|
+
export type RerunVerdict = 'does-not-reproduce-alone' | 'reproduces-alone' | 'inconclusive';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Имена упавших тестовых файлов из журнала набора. Понимает ОБА формата, которые даёт обход:
|
|
28
|
+
* vitest ` FAIL test/x.test.ts > describe > it`
|
|
29
|
+
* node:test ` location: '/abs/.../test/x.test.mjs:95:1'`
|
|
30
|
+
*
|
|
31
|
+
* Пути приводятся к виду, относительному каталогу пакета, потому что перепрогон запускается
|
|
32
|
+
* из него. Порядок сохраняется, дубликаты убираются: один файл перепрогоняется один раз.
|
|
33
|
+
*/
|
|
34
|
+
export function parseFailedTestFiles(logText: string, packageDir?: string): string[] {
|
|
35
|
+
const out: string[] = [];
|
|
36
|
+
const seen = new Set<string>();
|
|
37
|
+
const add = (raw: string): void => {
|
|
38
|
+
let p = raw.trim();
|
|
39
|
+
if (packageDir !== undefined && p.startsWith(packageDir)) {
|
|
40
|
+
p = p.slice(packageDir.length).replace(/^\/+/, '');
|
|
41
|
+
}
|
|
42
|
+
// Абсолютный путь к ЧУЖОМУ пакету перепрогонять нельзя: он не относителен нашему каталогу.
|
|
43
|
+
if (p.startsWith('/')) return;
|
|
44
|
+
if (p === '' || seen.has(p)) return;
|
|
45
|
+
seen.add(p);
|
|
46
|
+
out.push(p);
|
|
47
|
+
};
|
|
48
|
+
for (const line of logText.split('\n')) {
|
|
49
|
+
const vitest = /^\s*FAIL\s+(?:\|[^|]*\|\s*)?(\S+\.(?:test|spec)\.[cm]?[jt]s)\b/.exec(line);
|
|
50
|
+
if (vitest?.[1] !== undefined) { add(vitest[1]); continue; }
|
|
51
|
+
const node = /location:\s*'([^']+\.(?:test|spec)\.[cm]?[jt]s):\d+/.exec(line);
|
|
52
|
+
if (node?.[1] !== undefined) { add(node[1]); }
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Вердикт по исходу перепрогона.
|
|
59
|
+
*
|
|
60
|
+
* @param sweepFailed упал ли файл в обходе (иначе классифицировать нечего)
|
|
61
|
+
* @param rerun результат одиночного прогона, или null если запустить не удалось
|
|
62
|
+
*/
|
|
63
|
+
export function classifyRerun(
|
|
64
|
+
sweepFailed: boolean,
|
|
65
|
+
rerun: { readonly exitCode: number; readonly ranAnyTest: boolean } | null,
|
|
66
|
+
): RerunVerdict {
|
|
67
|
+
if (!sweepFailed) return 'inconclusive';
|
|
68
|
+
if (rerun === null) return 'inconclusive';
|
|
69
|
+
// «Ни одного теста не запустилось» — это не зелёный прогон, а несостоявшаяся проба: так
|
|
70
|
+
// выглядит и опечатка в пути, и набор, который не грузится. Читать это как «прошёл» значит
|
|
71
|
+
// объявить файл нагрузочно-зависимым на основании того, что его никто не проверил.
|
|
72
|
+
if (!rerun.ranAnyTest) return 'inconclusive';
|
|
73
|
+
return rerun.exitCode === 0 ? 'does-not-reproduce-alone' : 'reproduces-alone';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Человеческая расшифровка вердикта — одна строка, называющая, что делать дальше. */
|
|
77
|
+
export function describeVerdict(verdict: RerunVerdict): string {
|
|
78
|
+
if (verdict === 'does-not-reproduce-alone') {
|
|
79
|
+
// НЕ «зависит от нагрузки»: у зелёного одиночного прогона есть ДВЕ причины, и различить их
|
|
80
|
+
// перепрогоном нельзя — нагрузка обхода ЛИБО починка, внесённая уже после него. Называть
|
|
81
|
+
// одну из двух значило бы выдать догадку за измерение.
|
|
82
|
+
return 'в одиночку ЗЕЛЁНЫЙ — либо отказ зависел от нагрузки обхода, либо починка легла после него; перепрогон эти две причины не различает';
|
|
83
|
+
}
|
|
84
|
+
if (verdict === 'reproduces-alone') {
|
|
85
|
+
return 'в одиночку ТОЖЕ красный — настоящий дефект, нагрузка ни при чём';
|
|
86
|
+
}
|
|
87
|
+
return 'НЕ УСТАНОВЛЕНО — перепрогон не состоялся; это не «прошёл»';
|
|
88
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* a convention whose reason is lost is the next thing somebody "cleans up".
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { CODEX_EXEC_PROMPT_CEILING_CHARS } from './feature-adr-routing.js';
|
|
14
15
|
import type { Deliverable } from './loop-plan.js';
|
|
15
16
|
import { claudeProbeArgs, claudeReviewArgs, extractClaudeResult, interpretClaudeProbe, type BridgeFamily } from './qe-bridge.js';
|
|
16
17
|
import type { WfRunReason } from './workflow-run.js';
|
|
@@ -149,13 +150,24 @@ export const CODEX_SCOPING_PREFIX =
|
|
|
149
150
|
'Answer directly from this prompt text alone; no commands, no files, no tools.';
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
|
-
* The REAL prompt ceiling for `codex exec
|
|
153
|
+
* The REAL prompt ceiling for `codex exec` — RE-EXPORTED, never re-declared.
|
|
153
154
|
*
|
|
154
155
|
* The folk value 1200 is refuted history: it came from an era when the prompt travelled through a
|
|
155
156
|
* fire-and-forget wrapper. Over-ceiling ⇒ a LOUD `prompt-over-ceiling`, never truncation — a
|
|
156
157
|
* truncated prompt produces a confident answer to a question nobody asked.
|
|
158
|
+
*
|
|
159
|
+
* ONE NUMBER, ONE DEFINITION. Until 2026-09-20 this module declared its own `= 24_000` beside the
|
|
160
|
+
* one in `feature-adr-routing.ts`, and NOTHING compared them: no test imported both, so the two
|
|
161
|
+
* could diverge in silence and `codexExecPlan` would route to Claude at a different threshold than
|
|
162
|
+
* `makeCodexExecDispatcher` rejects at. Measured: `feature-adr-codex-dispatch.test.ts` asserts only
|
|
163
|
+
* `> 4000`, and the mirror guard in `codex-scoped-review.test.ts` ties routing.ts to the four
|
|
164
|
+
* workflow copies but knows nothing about this file.
|
|
165
|
+
*
|
|
166
|
+
* The definition lives in `feature-adr-routing.ts` and not here because THAT module is lifted
|
|
167
|
+
* verbatim into the workflow sandbox by `scripts/gen-loop-blobs.mjs` and therefore may not import
|
|
168
|
+
* anything. The dependency can only point this way.
|
|
157
169
|
*/
|
|
158
|
-
export
|
|
170
|
+
export { CODEX_EXEC_PROMPT_CEILING_CHARS };
|
|
159
171
|
|
|
160
172
|
/**
|
|
161
173
|
* argv for one codex dispatch. The prompt travels as ONE argv element (no shell, no quoting), and
|