@dzhechkov/harness-core 0.8.40 → 0.8.42
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 +220 -120
- package/README.md +65 -10
- package/dist/__tests__/golden-baseline.test.js +1 -1
- package/dist/__tests__/golden-baseline.test.js.map +1 -1
- package/dist/agentdb-index.js +3 -3
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +2 -2
- package/dist/agentdb-reindex-marker.js +4 -4
- package/dist/agentdb-reindex-marker.js.map +1 -1
- package/dist/agentdb-snapshot-rotation.d.ts +1 -1
- package/dist/agentdb-snapshot-rotation.js +1 -1
- package/dist/backup-freshness.d.ts +32 -0
- package/dist/backup-freshness.d.ts.map +1 -0
- package/dist/backup-freshness.js +69 -0
- package/dist/backup-freshness.js.map +1 -0
- package/dist/cmd-usage.d.ts +2 -2
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +8 -6
- package/dist/cmd-usage.js.map +1 -1
- package/dist/codex-hooks-assets.d.ts +2 -1
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +15 -2
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/compounding.d.ts +43 -1
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +96 -0
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +0 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +2 -2
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/guard.d.ts +26 -2
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +162 -17
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.js +2 -2
- package/dist/integration-apply.js.map +1 -1
- package/dist/lesson-payoff.js +3 -3
- package/dist/lesson-payoff.js.map +1 -1
- package/dist/named-lock.d.ts +5 -2
- package/dist/named-lock.d.ts.map +1 -1
- package/dist/named-lock.js +30 -10
- package/dist/named-lock.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +24 -0
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts +51 -0
- package/dist/pack-inventory.d.ts.map +1 -0
- package/dist/pack-inventory.js +306 -0
- package/dist/pack-inventory.js.map +1 -0
- package/dist/package-skill-layouts.d.ts +3 -2
- package/dist/package-skill-layouts.d.ts.map +1 -1
- package/dist/package-skill-layouts.js +3 -2
- package/dist/package-skill-layouts.js.map +1 -1
- package/dist/patterns.d.ts +8 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +11 -1
- package/dist/patterns.js.map +1 -1
- package/dist/publish.d.ts +43 -7
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +153 -59
- package/dist/publish.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +24 -20
- package/dist/registry.js.map +1 -1
- package/dist/release-line.d.ts +15 -1
- package/dist/release-line.d.ts.map +1 -1
- package/dist/release-line.js +40 -13
- package/dist/release-line.js.map +1 -1
- package/dist/reqe-verdict.d.ts +55 -0
- package/dist/reqe-verdict.d.ts.map +1 -0
- package/dist/reqe-verdict.js +173 -0
- package/dist/reqe-verdict.js.map +1 -0
- package/dist/reqe.d.ts +35 -14
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +85 -32
- package/dist/reqe.js.map +1 -1
- package/dist/round.d.ts +10 -0
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +7 -0
- package/dist/round.js.map +1 -1
- package/dist/session-retro.d.ts +9 -6
- package/dist/session-retro.d.ts.map +1 -1
- package/dist/session-retro.js +39 -14
- package/dist/session-retro.js.map +1 -1
- package/dist/skill-drift.d.ts +18 -1
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +46 -13
- package/dist/skill-drift.js.map +1 -1
- package/dist/statusline.js +2 -2
- package/dist/statusline.js.map +1 -1
- package/dist/store-guard.js +3 -3
- package/dist/store-guard.js.map +1 -1
- package/dist/store-lock.d.ts +9 -0
- package/dist/store-lock.d.ts.map +1 -1
- package/dist/store-lock.js +22 -3
- package/dist/store-lock.js.map +1 -1
- package/dist/test-receipt.d.ts +66 -0
- package/dist/test-receipt.d.ts.map +1 -0
- package/dist/test-receipt.js +73 -0
- package/dist/test-receipt.js.map +1 -0
- package/dist/tree-sync.d.ts +5 -0
- package/dist/tree-sync.d.ts.map +1 -0
- package/dist/tree-sync.js +13 -0
- package/dist/tree-sync.js.map +1 -0
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +7 -8
- package/dist/workflow-run.js.map +1 -1
- package/package.json +8 -8
- package/sbom.json +369 -119
- package/src/__tests__/golden-baseline.test.ts +1 -1
- package/src/agentdb-index.ts +3 -3
- package/src/agentdb-reindex-marker.ts +4 -4
- package/src/agentdb-snapshot-rotation.ts +1 -1
- package/src/backup-freshness.ts +96 -0
- package/src/cmd-usage.ts +7 -6
- package/src/codex-hooks-assets.ts +15 -2
- package/src/compounding.ts +132 -1
- package/src/discrimination-gate.ts +2 -5
- package/src/guard.ts +166 -20
- package/src/index.ts +25 -6
- package/src/integration-apply.ts +2 -2
- package/src/lesson-payoff.ts +3 -3
- package/src/named-lock.ts +36 -11
- package/src/operations.ts +24 -0
- package/src/pack-inventory.ts +313 -0
- package/src/package-skill-layouts.ts +3 -2
- package/src/patterns.ts +18 -1
- package/src/publish.ts +168 -65
- package/src/registry.ts +23 -17
- package/src/release-line.ts +60 -16
- package/src/reqe-verdict.ts +174 -0
- package/src/reqe.ts +111 -35
- package/src/round.ts +18 -0
- package/src/session-retro.ts +54 -18
- package/src/skill-drift.ts +66 -12
- package/src/statusline.ts +2 -2
- package/src/store-guard.ts +3 -3
- package/src/store-lock.ts +20 -3
- package/src/test-receipt.ts +106 -0
- package/src/tree-sync.ts +17 -0
- package/src/workflow-run.ts +7 -8
package/src/publish.ts
CHANGED
|
@@ -22,7 +22,7 @@ type ExecSyncOptionsWithStringEncoding = NonNullable<Parameters<typeof execSync>
|
|
|
22
22
|
import { createHash } from 'node:crypto';
|
|
23
23
|
|
|
24
24
|
import { claimCheck } from './claim-check.js';
|
|
25
|
-
import { rewriteReleaseLine, isReleaseLineToken } from './release-line.js';
|
|
25
|
+
import { findReleaseLine, rewriteReleaseLine, isReleaseLineToken, shortPackageName } from './release-line.js';
|
|
26
26
|
import { packedTarballName } from './packed-install-smoke.js';
|
|
27
27
|
|
|
28
28
|
export type ProbeOutcome = {
|
|
@@ -38,12 +38,29 @@ export type ProbeOutcome = {
|
|
|
38
38
|
export const REGISTRY_PROBE_BUDGET = 90;
|
|
39
39
|
export const REGISTRY_PROBE_INTERVAL_MS = 10_000;
|
|
40
40
|
|
|
41
|
+
export type RegistryProbe =
|
|
42
|
+
| { kind: 'published'; version: string; scripted?: true }
|
|
43
|
+
| { kind: 'never-published'; scripted?: true }
|
|
44
|
+
| { kind: 'unknown'; reason: string; scripted?: true };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scripted registry seam for tests that spawn the REAL bin and therefore cannot inject `exec`
|
|
48
|
+
* (first-publish-not-offline AM-A; same shape as `WF_RUN_DISPATCH_SCRIPT_ENV`). Value: path to a JSON
|
|
49
|
+
* file `{ "<name>": "<x.y.z>" | "E404" | "<npm code>" }`. When set, no `npm view` runs at all and EVERY
|
|
50
|
+
* row carries `probeOverride: true` — a scripted plan can never read as a verified one.
|
|
51
|
+
*/
|
|
52
|
+
export const PUBLISH_PROBE_SCRIPT_ENV = 'DZ_PUBLISH_PROBE_SCRIPT';
|
|
53
|
+
|
|
41
54
|
/** Result for a single package publish attempt. */
|
|
42
55
|
export interface PublishResult {
|
|
43
56
|
readonly name: string;
|
|
44
57
|
readonly oldVersion: string;
|
|
45
58
|
readonly newVersion: string;
|
|
46
59
|
readonly status: 'published' | 'skipped' | 'error';
|
|
60
|
+
readonly firstPublish?: boolean;
|
|
61
|
+
readonly probe?: RegistryProbe['kind'];
|
|
62
|
+
/** Present (true) only when `DZ_PUBLISH_PROBE_SCRIPT` answered instead of the registry. */
|
|
63
|
+
readonly probeOverride?: boolean;
|
|
47
64
|
readonly error?: string | undefined;
|
|
48
65
|
/** Live publish only: how many registry probes were needed to confirm the exact new version. */
|
|
49
66
|
readonly registryProbes?: number | undefined;
|
|
@@ -130,6 +147,13 @@ export interface MirrorEpilogue {
|
|
|
130
147
|
readonly warning?: string | undefined;
|
|
131
148
|
}
|
|
132
149
|
|
|
150
|
+
export interface ReleaseLineReport {
|
|
151
|
+
readonly path: string; // 'README.md' | 'packages/@dzhechkov/harness-cli/README.md'
|
|
152
|
+
readonly rewritten: readonly { readonly name: string; readonly from: string; readonly to: string }[];
|
|
153
|
+
readonly kept: readonly { readonly name: string; readonly version: string }[];
|
|
154
|
+
readonly wrapped: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
/** Full publish report. */
|
|
134
158
|
export interface PublishReport {
|
|
135
159
|
readonly packages: readonly PublishResult[];
|
|
@@ -139,6 +163,7 @@ export interface PublishReport {
|
|
|
139
163
|
readonly dryRun: boolean;
|
|
140
164
|
/** Repo-relative README paths whose first joint core/CLI release line was rewritten. */
|
|
141
165
|
readonly releaseLineSynced: readonly string[];
|
|
166
|
+
readonly releaseLineReport: readonly ReleaseLineReport[];
|
|
142
167
|
/** Post-publication sync failures are warnings: registry-confirmed packages cannot be unpublished. */
|
|
143
168
|
readonly warnings?: readonly string[] | undefined;
|
|
144
169
|
/** CLI-owned command-hook outcome after a live publish; no transport logic lives in core. */
|
|
@@ -176,26 +201,54 @@ export function compareVersions(a: string, b: string): number {
|
|
|
176
201
|
return a0 - b0 || a1 - b1 || a2 - b2;
|
|
177
202
|
}
|
|
178
203
|
|
|
179
|
-
/**
|
|
180
|
-
* The version already published to npm for `name`, or `undefined` if the package
|
|
181
|
-
* has never been published (or npm is unreachable). Used to bump from
|
|
182
|
-
* max(local, published) so a locally-reverted version can't collide (audit #10).
|
|
183
|
-
*/
|
|
184
204
|
type PublishExec = (command: string, options: ExecSyncOptionsWithStringEncoding) => string;
|
|
185
205
|
|
|
186
|
-
|
|
206
|
+
/** Only explicit npm absence signals establish that a package has never been published. */
|
|
207
|
+
export function classifyRegistryProbe(stderr: string, message: string): RegistryProbe {
|
|
208
|
+
const detail = `${stderr}\n${message}`;
|
|
209
|
+
if (/\bcode E404\b/.test(detail) || /\b404 Not Found\b/.test(detail) || /is not in this registry/.test(detail)) {
|
|
210
|
+
return { kind: 'never-published' };
|
|
211
|
+
}
|
|
212
|
+
const reason = /npm error code (\S+)/.exec(detail)?.[1]
|
|
213
|
+
?? stderr.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0)
|
|
214
|
+
?? message;
|
|
215
|
+
return { kind: 'unknown', reason };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function scriptedProbe(name: string): RegistryProbe | undefined {
|
|
219
|
+
const scriptPath = process.env[PUBLISH_PROBE_SCRIPT_ENV];
|
|
220
|
+
if (typeof scriptPath !== 'string' || scriptPath === '') return undefined;
|
|
221
|
+
let table: Record<string, unknown>;
|
|
187
222
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
table = JSON.parse(readFileSync(scriptPath, 'utf-8')) as Record<string, unknown>;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { kind: 'unknown', reason: `scripted probe unreadable: ${err instanceof Error ? err.message : String(err)}`, scripted: true };
|
|
226
|
+
}
|
|
227
|
+
const answer = table[name];
|
|
228
|
+
if (typeof answer !== 'string') return { kind: 'unknown', reason: `scripted probe has no entry for ${name}`, scripted: true };
|
|
229
|
+
if (/^\d+\.\d+\.\d+/.test(answer)) return { kind: 'published', version: answer, scripted: true };
|
|
230
|
+
if (answer === 'E404') return { kind: 'never-published', scripted: true };
|
|
231
|
+
return { kind: 'unknown', reason: answer, scripted: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function probeRegistry(name: string, exec: PublishExec = execSync): RegistryProbe {
|
|
235
|
+
const scripted = scriptedProbe(name);
|
|
236
|
+
if (scripted !== undefined) return scripted;
|
|
237
|
+
try {
|
|
238
|
+
const out = exec(`npm view ${name} version --prefer-online`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 20000 }).trim();
|
|
239
|
+
return /^\d+\.\d+\.\d+/.test(out)
|
|
240
|
+
? { kind: 'published', version: out }
|
|
241
|
+
: { kind: 'unknown', reason: 'unparsable npm view output' };
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const error = err as { stderr?: unknown; message?: unknown } | null | undefined;
|
|
244
|
+
return classifyRegistryProbe(String(error?.stderr ?? ''), String(error?.message ?? ''));
|
|
192
245
|
}
|
|
193
246
|
}
|
|
194
247
|
|
|
195
248
|
/** The higher of the local version and the npm-published version (audit #10). */
|
|
196
249
|
function maxPublished(name: string, localVersion: string, exec: PublishExec = execSync): string {
|
|
197
|
-
const
|
|
198
|
-
return
|
|
250
|
+
const probe = probeRegistry(name, exec);
|
|
251
|
+
return probe.kind === 'published' && compareVersions(probe.version, localVersion) > 0 ? probe.version : localVersion;
|
|
199
252
|
}
|
|
200
253
|
|
|
201
254
|
// ── workspace-floor preflight (feature workspace-dep-protocol, Codex P1) ─────
|
|
@@ -829,6 +882,8 @@ export function publishPackages(
|
|
|
829
882
|
opts: {
|
|
830
883
|
dryRun?: boolean | undefined;
|
|
831
884
|
filter?: string[] | undefined;
|
|
885
|
+
/** Exact CLI-selected batch; an empty list means no targets. Discovery remains unfiltered. */
|
|
886
|
+
targetNames?: readonly string[] | undefined;
|
|
832
887
|
bumpOnly?: boolean | undefined;
|
|
833
888
|
/**
|
|
834
889
|
* Path to the Ed25519 signing key, OUTSIDE the repository. A pack that carries a
|
|
@@ -860,8 +915,8 @@ export function publishPackages(
|
|
|
860
915
|
/**
|
|
861
916
|
* Floor probe injection for the workspace-floor preflight (see
|
|
862
917
|
* `findUnpublishedWorkspaceFloors`). Default: a real `npm view` probe, which runs only on LIVE
|
|
863
|
-
* publishes
|
|
864
|
-
*
|
|
918
|
+
* publishes. Injecting a probe also arms this floor preflight under dry-run. The package's
|
|
919
|
+
* own registry classification runs in both live and dry-run modes independently.
|
|
865
920
|
*/
|
|
866
921
|
probeFloor?: ((name: string, version: string) => boolean) | undefined;
|
|
867
922
|
/** Subprocess injection for tests; the default is Node's synchronous executor. */
|
|
@@ -907,9 +962,13 @@ export function publishPackages(
|
|
|
907
962
|
|
|
908
963
|
const packages = discoverPackages(monorepoRoot);
|
|
909
964
|
const results: PublishResult[] = [];
|
|
910
|
-
const
|
|
965
|
+
const registryProbes = new Map<string, RegistryProbe>();
|
|
966
|
+
const matching = opts.filter && opts.filter.length > 0
|
|
911
967
|
? packages.filter((p) => opts.filter!.some((f) => matchesPublishFilter(p, f, monorepoRoot)))
|
|
912
968
|
: packages;
|
|
969
|
+
const filtered = opts.targetNames === undefined
|
|
970
|
+
? matching
|
|
971
|
+
: matching.filter((p) => opts.targetNames!.includes(p.name));
|
|
913
972
|
// Publish dependencies before dependents so pnpm rewrites workspace:* to the
|
|
914
973
|
// freshly-bumped version, never a stale one (the harness-cli@0.3.122 breakage).
|
|
915
974
|
const ordered = orderByDependencies(filtered);
|
|
@@ -1014,11 +1073,29 @@ export function publishPackages(
|
|
|
1014
1073
|
failedInBatch.add(pkg.name);
|
|
1015
1074
|
continue;
|
|
1016
1075
|
}
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1076
|
+
// Both modes establish registry state before planning: absence keeps the disk version,
|
|
1077
|
+
// uncertainty refuses a bump, and an existing release bumps from max(local, published).
|
|
1078
|
+
const registryProbe = probeRegistry(pkg.name, exec);
|
|
1079
|
+
registryProbes.set(pkg.name, registryProbe);
|
|
1080
|
+
if (registryProbe.kind === 'unknown') {
|
|
1081
|
+
results.push({
|
|
1082
|
+
name: pkg.name, oldVersion, newVersion: oldVersion,
|
|
1083
|
+
status: opts.dryRun ? 'skipped' : 'error',
|
|
1084
|
+
probe: 'unknown',
|
|
1085
|
+
error: opts.dryRun
|
|
1086
|
+
? `NOT ESTABLISHED (registry unreachable: ${registryProbe.reason})`
|
|
1087
|
+
: `registry unreachable: ${registryProbe.reason} — cannot tell "never published" from "offline"; not bumping blind`,
|
|
1088
|
+
});
|
|
1089
|
+
if (!opts.dryRun) failedInBatch.add(pkg.name);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const plan = {
|
|
1093
|
+
base: registryProbe.kind === 'published' && compareVersions(registryProbe.version, oldVersion) > 0
|
|
1094
|
+
? registryProbe.version : oldVersion,
|
|
1095
|
+
firstPublish: registryProbe.kind === 'never-published',
|
|
1096
|
+
probe: registryProbe.kind,
|
|
1097
|
+
};
|
|
1098
|
+
const newVersion = plan.firstPublish ? oldVersion : bumpPatch(plan.base);
|
|
1022
1099
|
|
|
1023
1100
|
// Preflight: refuse to publish a pack whose `files` whitelist would silently
|
|
1024
1101
|
// drop a skill dir from the tarball (the bug that shipped skills-meta without
|
|
@@ -1113,7 +1190,7 @@ export function publishPackages(
|
|
|
1113
1190
|
if (opts.dryRun) {
|
|
1114
1191
|
// NOT a statement that the package would publish cleanly — only that the gates checked ABOVE
|
|
1115
1192
|
// this line passed. Everything below it (build, re-sign, pack, the package's own
|
|
1116
|
-
// `prepublishOnly`, the registry
|
|
1193
|
+
// `prepublishOnly`, the registry's publish receipt) is untouched by a dry run and named as such.
|
|
1117
1194
|
const NOT_VERIFIED_BY_DRY_RUN = Object.freeze([
|
|
1118
1195
|
'prepublishOnly пакета (его собственный гейт публикации)',
|
|
1119
1196
|
'сборка dist из исходников',
|
|
@@ -1441,63 +1518,89 @@ export function publishPackages(
|
|
|
1441
1518
|
}
|
|
1442
1519
|
|
|
1443
1520
|
const releaseLineSynced: string[] = [];
|
|
1521
|
+
const releaseLineReport: ReleaseLineReport[] = [];
|
|
1444
1522
|
const warnings: string[] = [];
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
(result)
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
try {
|
|
1456
|
-
const parsed = JSON.parse(readFileSync(pathJoin(pkg.dir, 'package.json'), 'utf8')) as { version?: unknown };
|
|
1457
|
-
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
1458
|
-
} catch {
|
|
1459
|
-
return null;
|
|
1523
|
+
const versions: Record<string, string> = {};
|
|
1524
|
+
for (const result of results.filter((result) => result.status === 'published')) {
|
|
1525
|
+
versions[shortPackageName(result.name)] = result.newVersion;
|
|
1526
|
+
}
|
|
1527
|
+
// Dry-run marks would-publish packages skipped, but records successful planning in landedInBatch.
|
|
1528
|
+
const planned: Record<string, string> = opts.dryRun === true ? {} : versions;
|
|
1529
|
+
if (opts.dryRun === true) {
|
|
1530
|
+
for (const result of results) {
|
|
1531
|
+
if (landedInBatch.has(result.name) && result.error === undefined) {
|
|
1532
|
+
planned[shortPackageName(result.name)] = result.newVersion;
|
|
1460
1533
|
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const syncReleaseLine = Object.keys(versions).length > 0 && opts.dryRun !== true && opts.bumpOnly !== true;
|
|
1537
|
+
if (Object.keys(planned).length > 0 && opts.bumpOnly !== true) {
|
|
1538
|
+
const readmes = [
|
|
1539
|
+
{ path: 'README.md', absolute: pathJoin(monorepoRoot, 'README.md') },
|
|
1540
|
+
{
|
|
1541
|
+
path: 'packages/@dzhechkov/harness-cli/README.md',
|
|
1542
|
+
absolute: pathJoin(monorepoRoot, 'packages', '@dzhechkov', 'harness-cli', 'README.md'),
|
|
1543
|
+
},
|
|
1544
|
+
] as const;
|
|
1545
|
+
for (const readme of readmes) {
|
|
1546
|
+
try {
|
|
1547
|
+
const original = readFileSync(readme.absolute, 'utf8');
|
|
1548
|
+
const found = findReleaseLine(original);
|
|
1549
|
+
if (found === null) {
|
|
1550
|
+
// A repo without a joint release line is the NORMAL case for every consumer monorepo —
|
|
1551
|
+
// only a run that was actually about to WRITE has something to report here. The report-only
|
|
1552
|
+
// pass (dry-run) records an empty plan and says nothing, which is what keeps `dz publish`'s
|
|
1553
|
+
// output free of the word the FR-11 proxy watches for (cli.test.ts, "independent of the
|
|
1554
|
+
// release feature"). MEASURED 2026-09-22: widening the gate without this made every dry-run
|
|
1555
|
+
// in a line-less repo print a release-line warning.
|
|
1556
|
+
if (syncReleaseLine) warnings.push(`release-line sync skipped ${readme.path}: release line not found`);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
const rewritten: { name: string; from: string; to: string }[] = [];
|
|
1560
|
+
const kept: { name: string; version: string }[] = [];
|
|
1561
|
+
for (const token of found.tokens) {
|
|
1562
|
+
if (Object.hasOwn(planned, token.name) && planned[token.name] !== token.version) {
|
|
1563
|
+
rewritten.push({ name: token.name, from: token.version, to: planned[token.name]! });
|
|
1564
|
+
} else {
|
|
1565
|
+
kept.push({ name: token.name, version: token.version });
|
|
1481
1566
|
}
|
|
1482
|
-
if (updated === original) continue;
|
|
1483
|
-
const tmp = readme.absolute + '.sync-tmp';
|
|
1484
|
-
writeFileSync(tmp, updated);
|
|
1485
|
-
renameSync(tmp, readme.absolute);
|
|
1486
|
-
releaseLineSynced.push(readme.path);
|
|
1487
|
-
} catch (error) {
|
|
1488
|
-
warnings.push(`release-line sync failed ${readme.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1489
1567
|
}
|
|
1568
|
+
releaseLineReport.push({ path: readme.path, rewritten, kept, wrapped: found.wrapped });
|
|
1569
|
+
const updated = rewriteReleaseLine(original, planned);
|
|
1570
|
+
if (!syncReleaseLine || updated === null || updated === original) continue;
|
|
1571
|
+
const tmp = readme.absolute + '.sync-tmp';
|
|
1572
|
+
writeFileSync(tmp, updated);
|
|
1573
|
+
renameSync(tmp, readme.absolute);
|
|
1574
|
+
releaseLineSynced.push(readme.path);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
// Same rule as the not-found branch above: only the pass that was actually going to WRITE
|
|
1577
|
+
// reports. A README that does not exist at all is the normal state of a consumer monorepo,
|
|
1578
|
+
// and a report-only pass must not turn that into a warning naming the release line
|
|
1579
|
+
// (FR-11 proxy, harness-cli/test/cli.test.ts — MEASURED 2026-09-22: ENOENT on both READMEs
|
|
1580
|
+
// of a tmp fixture made every dry-run print two release-line warnings).
|
|
1581
|
+
if (syncReleaseLine) warnings.push(`release-line sync failed ${readme.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1490
1582
|
}
|
|
1491
1583
|
}
|
|
1492
1584
|
}
|
|
1493
1585
|
|
|
1494
1586
|
return {
|
|
1495
|
-
|
|
1587
|
+
// Decorate every final path, including packed transport and gate failures. A dependency
|
|
1588
|
+
// blocked before its own probe has unknown registry state; keep the deps-first short circuit.
|
|
1589
|
+
packages: results.map((result) => {
|
|
1590
|
+
const registryProbe = registryProbes.get(result.name);
|
|
1591
|
+
return {
|
|
1592
|
+
...result,
|
|
1593
|
+
probe: registryProbe?.kind ?? 'unknown',
|
|
1594
|
+
...(registryProbe?.kind === 'never-published' ? { firstPublish: true } : {}),
|
|
1595
|
+
...(registryProbe?.scripted === true ? { probeOverride: true } : {}),
|
|
1596
|
+
};
|
|
1597
|
+
}),
|
|
1496
1598
|
published: results.filter((r) => r.status === 'published').length,
|
|
1497
1599
|
skipped: results.filter((r) => r.status === 'skipped').length,
|
|
1498
1600
|
errors: results.filter((r) => r.status === 'error').length,
|
|
1499
1601
|
dryRun: opts.dryRun === true,
|
|
1500
1602
|
releaseLineSynced,
|
|
1603
|
+
releaseLineReport,
|
|
1501
1604
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
1502
1605
|
};
|
|
1503
1606
|
}
|
package/src/registry.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, type Dir
|
|
|
11
11
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
|
+
import { PACKAGE_SKILL_LAYOUTS } from './package-skill-layouts.js';
|
|
14
15
|
import { stems } from './stem.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -81,11 +82,13 @@ function readSkillDiscoveryConfig(cwd: string): { skillScopes: string[]; skillDi
|
|
|
81
82
|
/** List `skills-*` pack directories across all base dirs, de-duplicated by pack name (first wins). */
|
|
82
83
|
/**
|
|
83
84
|
* Does this directory actually carry skills? Answers by LOOKING, so a pack is catalogued for what it
|
|
84
|
-
* contains rather than for how it is named.
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* the
|
|
88
|
-
*
|
|
85
|
+
* contains rather than for how it is named. Probes PACKAGE_SKILL_LAYOUTS — the ONE table that says
|
|
86
|
+
* where a package keeps its skills (backlog 86b787b8: the same three layouts used to be spelled out
|
|
87
|
+
* by hand here and in `buildRegistry`, so the next layout would land in one place and be forgotten
|
|
88
|
+
* in the other). Bounded on purpose: never a full-tree walk, because an unbounded scan over
|
|
89
|
+
* `node_modules` would cost more than the catalogue it builds. For the flat layout the package
|
|
90
|
+
* itself may also be a skill (a bare `SKILL.md`). `hasSkillMd` skips a `templates` child so a
|
|
91
|
+
* template stamp is not counted twice — it is reached only through its own allowed layout.
|
|
89
92
|
*/
|
|
90
93
|
function packCarriesSkills(dir: string): boolean {
|
|
91
94
|
const hasSkillMd = (d: string): boolean => {
|
|
@@ -98,13 +101,11 @@ function packCarriesSkills(dir: string): boolean {
|
|
|
98
101
|
return false;
|
|
99
102
|
};
|
|
100
103
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (hasSkillMd(join(dir, 'templates', '.claude', 'skills'))) return true;
|
|
107
|
-
return hasSkillMd(dir);
|
|
104
|
+
return PACKAGE_SKILL_LAYOUTS.some(({ rel }) => {
|
|
105
|
+
const root = rel === '.' ? dir : join(dir, rel);
|
|
106
|
+
if (rel === '.' && existsSync(join(root, 'SKILL.md'))) return true;
|
|
107
|
+
return hasSkillMd(root);
|
|
108
|
+
});
|
|
108
109
|
} catch {
|
|
109
110
|
return false;
|
|
110
111
|
}
|
|
@@ -482,17 +483,22 @@ export function buildRegistry(cwd: string): Registry {
|
|
|
482
483
|
const entries: RegistryEntry[] = [];
|
|
483
484
|
const packs = discoverSkillCarryingDirs(cwd);
|
|
484
485
|
|
|
485
|
-
//
|
|
486
|
+
// The catalogue collects ALL allowed layouts, and reading only the first made 41 real skills
|
|
486
487
|
// invisible (MEASURED 2026-09-01): the pack root (`skills-*` packs), a `skills/` subdirectory
|
|
487
488
|
// (health-advisor and friends), and `templates/.claude/skills/` for packs that roll their skills
|
|
488
|
-
// out into the user's project.
|
|
489
|
-
|
|
489
|
+
// out into the user's project. A template pack ships the skills it will roll out; from the
|
|
490
|
+
// catalogue's point of view those skills EXIST — `trip-planner` and `presentation-storyteller`
|
|
491
|
+
// are installable answers to a task — so hiding them would make the advisor deny a real
|
|
492
|
+
// capability. The layouts themselves now come from PACKAGE_SKILL_LAYOUTS so the knowledge lives
|
|
493
|
+
// in ONE place; resolvePackageSkillRoots deliberately selects only the FIRST non-empty root
|
|
494
|
+
// (installation semantics, plan AM-4) and therefore cannot serve this enumeration — calling it
|
|
495
|
+
// here would silently drop skills from a pack that fills two layouts (backlog 86b787b8).
|
|
490
496
|
const seenSkillIds = new Set<string>();
|
|
491
497
|
|
|
492
498
|
for (const { pack, dir: packDir } of packs) {
|
|
493
499
|
const found: { name: string; root: string }[] = [];
|
|
494
|
-
for (const
|
|
495
|
-
const root =
|
|
500
|
+
for (const { rel } of PACKAGE_SKILL_LAYOUTS) {
|
|
501
|
+
const root = rel === '.' ? packDir : join(packDir, rel);
|
|
496
502
|
if (!existsSync(root)) continue;
|
|
497
503
|
try {
|
|
498
504
|
for (const e of readdirSync(root, { withFileTypes: true })) {
|
package/src/release-line.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export const RELEASE_LINE_RE = /`harness-core v(\d+\.\d+\.\d+)` · `harness-cli v(\d+\.\d+\.\d+)`/;
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface ReleaseLineToken {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly version: string;
|
|
6
|
+
readonly start: number;
|
|
7
|
+
readonly end: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ReleaseLineParse {
|
|
11
|
+
readonly tokens: readonly ReleaseLineToken[];
|
|
12
|
+
readonly chainEnd: number;
|
|
13
|
+
readonly wrapped: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ReleaseLineMatch extends ReleaseLineParse {
|
|
4
17
|
readonly index: number;
|
|
5
18
|
readonly line: string;
|
|
6
19
|
readonly core: string;
|
|
@@ -11,25 +24,43 @@ export function findReleaseLine(text: string): ReleaseLineMatch | null {
|
|
|
11
24
|
const lines = text.split('\n');
|
|
12
25
|
for (let index = 0; index < lines.length; index++) {
|
|
13
26
|
const line = lines[index]!;
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
return { index, line, core:
|
|
27
|
+
const parsed = parseReleaseLine(line);
|
|
28
|
+
if (parsed !== null) {
|
|
29
|
+
return { index, line, core: parsed.tokens[0]!.version, cli: parsed.tokens[1]!.version, ...parsed };
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
32
|
return null;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
export function rewriteReleaseLine(text: string,
|
|
35
|
+
export function rewriteReleaseLine(text: string, versions: Readonly<Record<string, string>>): string | null;
|
|
36
|
+
export function rewriteReleaseLine(text: string, core: string, cli: string): string | null;
|
|
37
|
+
export function rewriteReleaseLine(
|
|
38
|
+
text: string,
|
|
39
|
+
versionsOrCore: Readonly<Record<string, string>> | string,
|
|
40
|
+
cli?: string,
|
|
41
|
+
): string | null {
|
|
42
|
+
const versions: Readonly<Record<string, string>> = typeof arguments[1] === 'string'
|
|
43
|
+
? { 'harness-core': versionsOrCore as string, 'harness-cli': cli! }
|
|
44
|
+
: versionsOrCore as Readonly<Record<string, string>>;
|
|
23
45
|
const found = findReleaseLine(text);
|
|
24
46
|
if (found === null) return null;
|
|
47
|
+
let line = found.line;
|
|
48
|
+
// Tokens are ordered by start; work from the right so length changes preserve earlier offsets.
|
|
49
|
+
for (let index = found.tokens.length - 1; index >= 0; index--) {
|
|
50
|
+
const token = found.tokens[index]!;
|
|
51
|
+
if (!Object.hasOwn(versions, token.name)) continue;
|
|
52
|
+
const versionStart = token.end - 1 - token.version.length;
|
|
53
|
+
line = line.slice(0, versionStart) + versions[token.name]! + line.slice(token.end - 1);
|
|
54
|
+
}
|
|
25
55
|
const lines = text.split('\n');
|
|
26
|
-
lines[found.index] =
|
|
27
|
-
RELEASE_LINE_RE,
|
|
28
|
-
`\`harness-core v${core}\` · \`harness-cli v${cli}\``,
|
|
29
|
-
);
|
|
56
|
+
lines[found.index] = line;
|
|
30
57
|
return lines.join('\n');
|
|
31
58
|
}
|
|
32
59
|
|
|
60
|
+
export function shortPackageName(name: string): string {
|
|
61
|
+
return name.replace(/^@[^/]+\//, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* A generic RELEASE-LINE token: a backtick-quoted `<pkg-short-name> vX` pair, anywhere on a line —
|
|
35
66
|
* the shape `RELEASE_LINE_RE` names for the joint `harness-core`/`harness-cli` pair, generalised to
|
|
@@ -40,6 +71,24 @@ export function rewriteReleaseLine(text: string, core: string, cli: string): str
|
|
|
40
71
|
*/
|
|
41
72
|
export const GENERIC_RELEASE_TOKEN_RE = /`[a-z][a-z0-9-]*\s+v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.]+)?`/;
|
|
42
73
|
|
|
74
|
+
export function parseReleaseLine(line: string): ReleaseLineParse | null {
|
|
75
|
+
const joint = RELEASE_LINE_RE.exec(line);
|
|
76
|
+
if (joint === null) return null;
|
|
77
|
+
// Keep the original sticky chain boundary: prose ends the release line's permission.
|
|
78
|
+
const chainTail = new RegExp(`(?:\\s*·\\s*${GENERIC_RELEASE_TOKEN_RE.source})*`, 'y');
|
|
79
|
+
chainTail.lastIndex = joint.index + joint[0].length;
|
|
80
|
+
const tail = chainTail.exec(line);
|
|
81
|
+
const chainEnd = joint.index + joint[0].length + (tail?.[0].length ?? 0);
|
|
82
|
+
const tokens: ReleaseLineToken[] = [];
|
|
83
|
+
const chain = line.slice(joint.index, chainEnd);
|
|
84
|
+
for (const match of chain.matchAll(new RegExp(GENERIC_RELEASE_TOKEN_RE.source, 'g'))) {
|
|
85
|
+
const [name, version] = match[0].slice(1, -1).split(/\s+v/);
|
|
86
|
+
const start = joint.index + match.index;
|
|
87
|
+
tokens.push({ name: name!, version: version!, start, end: start + match[0].length });
|
|
88
|
+
}
|
|
89
|
+
return { tokens, chainEnd, wrapped: /\s*·\s*$/.test(line.slice(chainEnd)) };
|
|
90
|
+
}
|
|
91
|
+
|
|
43
92
|
/**
|
|
44
93
|
* Is the OLD-VERSION occurrence at `[start, end)` in `line` sitting inside a `` `<name> vX` ``
|
|
45
94
|
* backtick token? A POSITIVE override for `planReadmeVersionSync`'s citation heuristic: a token
|
|
@@ -53,11 +102,6 @@ export function isReleaseLineToken(line: string, start: number, end: number): bo
|
|
|
53
102
|
// Codex r3 HIGH (lead): the permission is the joint pair PLUS the CONTIGUOUS ` · `<name> vX``
|
|
54
103
|
// chain that follows it — not the whole line. `` `harness-core vX` · `harness-cli vY` — historically
|
|
55
104
|
// `memory vZ` `` moves the first two and keeps the third (prose broke the chain).
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const chainTail = new RegExp(`(?:\\s*·\\s*${GENERIC_RELEASE_TOKEN_RE.source})*`, 'y');
|
|
59
|
-
chainTail.lastIndex = joint.index + joint[0].length;
|
|
60
|
-
const tail = chainTail.exec(line);
|
|
61
|
-
const chainEnd = joint.index + joint[0].length + (tail?.[0].length ?? 0);
|
|
62
|
-
return joint.index <= start && end <= chainEnd;
|
|
105
|
+
const p = parseReleaseLine(line);
|
|
106
|
+
return p !== null && p.tokens[0]!.start <= start && end <= p.chainEnd;
|
|
63
107
|
}
|