@dzhechkov/harness-core 0.8.46 → 0.8.47
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 +175 -163
- package/README.md +84 -2
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +32 -2
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-snapshot.d.ts +14 -1
- package/dist/agentdb-snapshot.d.ts.map +1 -1
- package/dist/agentdb-snapshot.js +100 -4
- package/dist/agentdb-snapshot.js.map +1 -1
- package/dist/apply-leg.d.ts +9 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +26 -4
- package/dist/apply-leg.js.map +1 -1
- package/dist/architecture.d.ts +0 -5
- package/dist/architecture.d.ts.map +1 -1
- package/dist/architecture.js +25 -2
- package/dist/architecture.js.map +1 -1
- package/dist/codex-rollouts.d.ts +35 -7
- package/dist/codex-rollouts.d.ts.map +1 -1
- package/dist/codex-rollouts.js +201 -113
- package/dist/codex-rollouts.js.map +1 -1
- package/dist/cost-ledger.d.ts +42 -3
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +478 -15
- package/dist/cost-ledger.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +16 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +21 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +7 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +48 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-plan.d.ts +4 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +4 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/mutation-gate.d.ts +36 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +109 -19
- package/dist/mutation-gate.js.map +1 -1
- package/dist/npm-homepage.d.ts +64 -0
- package/dist/npm-homepage.d.ts.map +1 -0
- package/dist/npm-homepage.js +109 -0
- package/dist/npm-homepage.js.map +1 -0
- package/dist/operations.d.ts +6 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +150 -24
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts.map +1 -1
- package/dist/pack-inventory.js +6 -1
- package/dist/pack-inventory.js.map +1 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +4 -1
- package/dist/parity.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +43 -1
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +269 -13
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +5 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +18 -1
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +63 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +82 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release-package-audit.d.ts +86 -0
- package/dist/release-package-audit.d.ts.map +1 -0
- package/dist/release-package-audit.js +272 -0
- package/dist/release-package-audit.js.map +1 -0
- package/dist/release.d.ts +8 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +58 -32
- package/dist/release.js.map +1 -1
- package/dist/round.d.ts +10 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +5 -1
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +11 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +124 -20
- package/dist/run-records.js.map +1 -1
- package/dist/stage-usage.d.ts +148 -0
- package/dist/stage-usage.d.ts.map +1 -0
- package/dist/stage-usage.js +261 -0
- package/dist/stage-usage.js.map +1 -0
- package/dist/statusline.d.ts +23 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +167 -1
- package/dist/statusline.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +21 -19
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +206 -95
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +12 -3
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +29 -2
- package/dist/workflow-run.js.map +1 -1
- package/package.json +3 -3
- package/sbom.json +264 -234
- package/src/agentdb-index.ts +31 -2
- package/src/agentdb-snapshot.ts +105 -4
- package/src/apply-leg.ts +26 -4
- package/src/architecture.ts +15 -2
- package/src/codex-rollouts.ts +155 -147
- package/src/cost-ledger.ts +308 -19
- package/src/feature-adr-routing.ts +22 -0
- package/src/guard.ts +52 -0
- package/src/index.ts +15 -3
- package/src/loop-plan.ts +8 -0
- package/src/mutation-gate.ts +153 -19
- package/src/npm-homepage.ts +142 -0
- package/src/operations.ts +177 -22
- package/src/pack-inventory.ts +6 -1
- package/src/parity.ts +4 -1
- package/src/publish-sibling-drift.ts +214 -11
- package/src/publish.ts +20 -1
- package/src/qe-bridge.ts +86 -1
- package/src/release-package-audit.ts +264 -0
- package/src/release.ts +55 -22
- package/src/round.ts +8 -0
- package/src/run-records.ts +101 -22
- package/src/stage-usage.ts +191 -0
- package/src/statusline.ts +149 -1
- package/src/workflow-run-dispatch.ts +148 -89
- package/src/workflow-run.ts +40 -4
- package/dist/ledger-cost-fill.d.ts +0 -58
- package/dist/ledger-cost-fill.d.ts.map +0 -1
- package/dist/ledger-cost-fill.js +0 -78
- package/dist/ledger-cost-fill.js.map +0 -1
- package/dist/retro.d.ts +0 -131
- package/dist/retro.d.ts.map +0 -1
- package/dist/retro.js +0 -207
- package/dist/retro.js.map +0 -1
- package/dist/sbom.d.ts +0 -42
- package/dist/sbom.d.ts.map +0 -1
- package/dist/sbom.js +0 -120
- package/dist/sbom.js.map +0 -1
package/src/agentdb-index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @packageDocumentation
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, mkdirSync, copyFileSync, realpathSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdirSync, copyFileSync, realpathSync, readFileSync, writeFileSync, renameSync, unlinkSync, lstatSync, rmSync, constants as fsConstants } from 'node:fs';
|
|
16
16
|
import { join, dirname, resolve, relative, isAbsolute, basename } from 'node:path';
|
|
17
17
|
import { pathToFileURL } from 'node:url';
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
@@ -1408,6 +1408,31 @@ export async function reindexAgentdbRows(
|
|
|
1408
1408
|
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
1409
1409
|
return { reembedded: 0, error: `opts.backupPath must stay inside ${dbDir}, got: ${opts.backupPath}` };
|
|
1410
1410
|
}
|
|
1411
|
+
// agentdb-backup-no-symlink-escape (backlog c2a85b540a293ea2): the check above resolves only the
|
|
1412
|
+
// PARENT physically and re-joins the basename lexically, so a LAST component that is itself a
|
|
1413
|
+
// symlink out of the store passed it and the snapshot was written through the link (MEASURED on
|
|
1414
|
+
// HEAD: a dangling link got a snapshot created outside, a link to an empty outside file had it
|
|
1415
|
+
// overwritten). The leaf is lstat'ed — never followed — and refused by name. The snapshot writer
|
|
1416
|
+
// repeats this check and writes via a staging dir + rename, so a link planted after this point is
|
|
1417
|
+
// replaced, not followed (agentdb-snapshot.ts, stageThenRename).
|
|
1418
|
+
// Review r1 (MINOR): only ENOENT means "absent" — EACCES/EIO/ENOTDIR are a path whose kind cannot
|
|
1419
|
+
// be determined, refused by name rather than read as free.
|
|
1420
|
+
let leaf: ReturnType<typeof lstatSync> | undefined;
|
|
1421
|
+
try {
|
|
1422
|
+
leaf = lstatSync(candidate);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
1425
|
+
if (code !== 'ENOENT') {
|
|
1426
|
+
return { reembedded: 0, error: `opts.backupPath cannot be inspected (${code ?? 'unknown error'}): ${opts.backupPath}` };
|
|
1427
|
+
}
|
|
1428
|
+
leaf = undefined;
|
|
1429
|
+
}
|
|
1430
|
+
if (leaf?.isSymbolicLink()) {
|
|
1431
|
+
return { reembedded: 0, error: `opts.backupPath is a symbolic link — refusing to follow it: ${opts.backupPath}` };
|
|
1432
|
+
}
|
|
1433
|
+
if (leaf !== undefined && !leaf.isFile()) {
|
|
1434
|
+
return { reembedded: 0, error: `opts.backupPath exists and is not a regular file: ${opts.backupPath}` };
|
|
1435
|
+
}
|
|
1411
1436
|
}
|
|
1412
1437
|
|
|
1413
1438
|
// FR-3: sqlite resolves BEFORE any snapshot is taken — an unavailable dependency must abort with
|
|
@@ -1474,7 +1499,11 @@ export async function reindexAgentdbRows(
|
|
|
1474
1499
|
snapshotMethod = outcome.method;
|
|
1475
1500
|
snapshotNote = outcome.note;
|
|
1476
1501
|
if (existsSync(`${dbFile}.embed-manifest.json`)) {
|
|
1477
|
-
|
|
1502
|
+
// no-symlink-escape (FR-5): remove whatever sits at the sibling (rmSync removes a symlink
|
|
1503
|
+
// itself, never its target), then copy with COPYFILE_EXCL — O_CREAT|O_EXCL refuses a link
|
|
1504
|
+
// planted in between (EEXIST) instead of writing through it.
|
|
1505
|
+
rmSync(`${backupPath}.embed-manifest.json`, { force: true });
|
|
1506
|
+
copyFileSync(`${dbFile}.embed-manifest.json`, `${backupPath}.embed-manifest.json`, fsConstants.COPYFILE_EXCL);
|
|
1478
1507
|
}
|
|
1479
1508
|
}
|
|
1480
1509
|
} catch (snapErr) {
|
package/src/agentdb-snapshot.ts
CHANGED
|
@@ -43,6 +43,18 @@
|
|
|
43
43
|
* bug, a backup that looked complete but carried a wrong-generation `-wal`. It now refuses up front
|
|
44
44
|
* (before `VACUUM INTO` or any copy) when `backupPath` already exists and is non-empty.
|
|
45
45
|
*
|
|
46
|
+
* **agentdb-backup-no-symlink-escape (backlog c2a85b540a293ea2).** A `backupPath` that is itself a
|
|
47
|
+
* symbolic link used to be written THROUGH: `statSync` in AM-4 follows the link (so a link to a
|
|
48
|
+
* non-empty file was refused, by accident), but a dangling link got a fresh snapshot created at its
|
|
49
|
+
* target and a link to an EMPTY file had it overwritten — outside the store (MEASURED on HEAD, /var/tmp
|
|
50
|
+
* probe, features/agentdb-backup-no-symlink-escape/00_complexity_assessment.md). Two layers now:
|
|
51
|
+
* {@link assertBackupLeafWritable} refuses a symlink or non-regular leaf by name before anything is
|
|
52
|
+
* written, and {@link stageThenRename} writes the snapshot (main file and any `-wal`) into a fresh
|
|
53
|
+
* private `mkdtempSync` directory next to the target and moves it into place with `renameSync`.
|
|
54
|
+
* `rename(2)` replaces a directory ENTRY — a link planted at `backupPath` between the check and the
|
|
55
|
+
* write is replaced by the snapshot, never followed. `O_NOFOLLOW` was not an option: the main path
|
|
56
|
+
* writes through sqlite's `VACUUM INTO`, which takes no open flags.
|
|
57
|
+
*
|
|
46
58
|
* {@link restoreSqliteSnapshot} (FR-4) is the paired rollback. MEASURED (same repro): copying the
|
|
47
59
|
* old main file back over `dbFile` WITHOUT removing a `-wal` left over from the aborted operation
|
|
48
60
|
* — reopening the "restored" db returned ZERO rows, not the restored one, because sqlite replayed
|
|
@@ -61,7 +73,8 @@
|
|
|
61
73
|
* @packageDocumentation
|
|
62
74
|
*/
|
|
63
75
|
|
|
64
|
-
import { copyFileSync, existsSync, rmSync, statSync } from 'node:fs';
|
|
76
|
+
import { copyFileSync, existsSync, lstatSync, mkdtempSync, renameSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
77
|
+
import { basename, dirname, join } from 'node:path';
|
|
65
78
|
|
|
66
79
|
/** How a snapshot was actually taken — always reported by the caller, never assumed. */
|
|
67
80
|
export type SnapshotMethod = 'vacuum-into' | 'copy+wal' | 'copy';
|
|
@@ -130,6 +143,77 @@ function clearBackupSidecars(backupPath: string): void {
|
|
|
130
143
|
rmSync(`${backupPath}-shm`, { force: true });
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
/**
|
|
147
|
+
* no-symlink-escape (FR-1/FR-2/FR-4): refuse a `backupPath` whose LAST component is a symbolic link
|
|
148
|
+
* (whatever it points at) or an existing non-regular file (directory, FIFO, socket, device). `lstatSync`
|
|
149
|
+
* never follows the link; only a confirmed `ENOENT` means "free". Any other lstat error propagates —
|
|
150
|
+
* a target whose kind cannot be determined is not written to.
|
|
151
|
+
*/
|
|
152
|
+
function assertBackupLeafWritable(backupPath: string): void {
|
|
153
|
+
let st: ReturnType<typeof lstatSync>;
|
|
154
|
+
try {
|
|
155
|
+
st = lstatSync(backupPath);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
if (st.isSymbolicLink()) {
|
|
161
|
+
throw new Error(`snapshot target is a symbolic link — refusing to follow it: ${backupPath}`);
|
|
162
|
+
}
|
|
163
|
+
if (!st.isFile()) {
|
|
164
|
+
throw new Error(`snapshot target exists and is not a regular file: ${backupPath}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* no-symlink-escape (FR-3, the TOCTOU half): run `write` against a path inside a fresh private staging
|
|
170
|
+
* directory (`mkdtempSync` — atomic, unpredictable name, mode 0700) next to `backupPath`, then move the
|
|
171
|
+
* result into place with `renameSync`: the main file first, then its `-wal` only when the outcome says
|
|
172
|
+
* `'copy+wal'` (AM-1: decided by the explicit method, never by what happens to exist on disk). A symlink
|
|
173
|
+
* planted at the target after {@link assertBackupLeafWritable} is REPLACED by the rename, never
|
|
174
|
+
* followed. The staging directory is always removed; one left by a crashed process is named
|
|
175
|
+
* `.<basename>.staging-XXXXXX`, which the rotation pattern (`<db>.pre-reindex-<ms>.bak…`) never matches.
|
|
176
|
+
*/
|
|
177
|
+
function stageThenRename(backupPath: string, write: (staged: string) => SnapshotOutcome): SnapshotOutcome {
|
|
178
|
+
const staging = mkdtempSync(join(dirname(backupPath), `.${basename(backupPath)}.staging-`));
|
|
179
|
+
try {
|
|
180
|
+
const staged = join(staging, basename(backupPath));
|
|
181
|
+
const outcome = write(staged);
|
|
182
|
+
// Order main -> wal is deliberate: publishing the -wal FIRST would put a foreign WAL next to
|
|
183
|
+
// whatever main file sits at backupPath, which sqlite could apply to it.
|
|
184
|
+
const stagedId = lstatSync(staged); // identity of the file we are about to publish
|
|
185
|
+
renameSync(staged, backupPath);
|
|
186
|
+
if (outcome.method === 'copy+wal') {
|
|
187
|
+
try {
|
|
188
|
+
renameSync(`${staged}-wal`, `${backupPath}-wal`);
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
// Review r1 (MAJOR): a published main file without its -wal is a half snapshot that reads
|
|
191
|
+
// as complete. Withdraw it — only if the entry is still the very file we just renamed
|
|
192
|
+
// (same dev+ino, regular file) — and fail loudly under a named reason.
|
|
193
|
+
let withdrawn = false;
|
|
194
|
+
try {
|
|
195
|
+
const now = lstatSync(backupPath);
|
|
196
|
+
if (now.isFile() && now.dev === stagedId.dev && now.ino === stagedId.ino) {
|
|
197
|
+
unlinkSync(backupPath);
|
|
198
|
+
withdrawn = true;
|
|
199
|
+
}
|
|
200
|
+
} catch { /* withdrawn stays false and is reported below */ }
|
|
201
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
202
|
+
throw new Error(
|
|
203
|
+
`snapshot-wal-publish-failed: could not move the -wal sidecar to ${backupPath}-wal (${why}); ` +
|
|
204
|
+
(withdrawn
|
|
205
|
+
? 'the just-published main file was withdrawn — no half snapshot left'
|
|
206
|
+
: `the main file at ${backupPath} could NOT be confirmed as ours and was left in place`),
|
|
207
|
+
{ cause },
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return outcome;
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(staging, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
133
217
|
function copyWithWal(dbFile: string, backupPath: string, note: string): SnapshotOutcome {
|
|
134
218
|
// AM-2: statSync directly; only a confirmed ENOENT means "no -wal to copy" — any other stat error
|
|
135
219
|
// propagates out of this function (and out of snapshotSqliteDatabase) as a thrown exception.
|
|
@@ -158,7 +242,8 @@ function copyWithWal(dbFile: string, backupPath: string, note: string): Snapshot
|
|
|
158
242
|
*
|
|
159
243
|
* Throws (no snapshot taken, or an incomplete one left in a fully-cleared state) when: `backupPath`
|
|
160
244
|
* already names an existing, non-empty file (AM-4); or a `-wal` stat probe hits a non-ENOENT error
|
|
161
|
-
* (AM-2, inside the fallback path)
|
|
245
|
+
* (AM-2, inside the fallback path); or `backupPath` is a symbolic link or an existing non-regular file
|
|
246
|
+
* (no-symlink-escape). Never silently overwrites, never mis-reports a lesser guarantee
|
|
162
247
|
* as a stronger one.
|
|
163
248
|
*/
|
|
164
249
|
export function snapshotSqliteDatabase(
|
|
@@ -167,8 +252,25 @@ export function snapshotSqliteDatabase(
|
|
|
167
252
|
backupPath: string,
|
|
168
253
|
opts: { strategy?: 'vacuum-into' | 'copy+wal' } = {},
|
|
169
254
|
): SnapshotOutcome {
|
|
255
|
+
assertBackupLeafWritable(backupPath); // no-symlink-escape — a link or non-file leaf is never written through
|
|
170
256
|
assertBackupTargetFree(backupPath); // AM-4 — before any write, on either strategy
|
|
171
257
|
clearBackupSidecars(backupPath); // AM-1 — clean slate before either strategy writes anything
|
|
258
|
+
const outcome = stageThenRename(backupPath, (staged) => writeSnapshot(Database, dbFile, staged, opts));
|
|
259
|
+
// AM-1 — VACUUM INTO's output never has a matching -wal of its own: clear again once it is in place.
|
|
260
|
+
if (outcome.method === 'vacuum-into') clearBackupSidecars(backupPath);
|
|
261
|
+
return outcome;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The two snapshot strategies, writing to `backupPath` — which {@link snapshotSqliteDatabase} always
|
|
266
|
+
* passes as the STAGED path inside its private staging directory, never the final target.
|
|
267
|
+
*/
|
|
268
|
+
function writeSnapshot(
|
|
269
|
+
Database: SnapshotDbCtor,
|
|
270
|
+
dbFile: string,
|
|
271
|
+
backupPath: string,
|
|
272
|
+
opts: { strategy?: 'vacuum-into' | 'copy+wal' },
|
|
273
|
+
): SnapshotOutcome {
|
|
172
274
|
if (opts.strategy === 'copy+wal') {
|
|
173
275
|
return copyWithWal(dbFile, backupPath, 'snapshotStrategy=copy+wal forced by caller');
|
|
174
276
|
}
|
|
@@ -179,8 +281,7 @@ export function snapshotSqliteDatabase(
|
|
|
179
281
|
} finally {
|
|
180
282
|
db.close();
|
|
181
283
|
}
|
|
182
|
-
|
|
183
|
-
return { method: 'vacuum-into' };
|
|
284
|
+
return { method: 'vacuum-into' }; // AM-1 sidecar clear runs in snapshotSqliteDatabase, after the rename
|
|
184
285
|
} catch (err) {
|
|
185
286
|
return copyWithWal(dbFile, backupPath, `VACUUM INTO failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
186
287
|
}
|
package/src/apply-leg.ts
CHANGED
|
@@ -208,8 +208,16 @@ export function probeRecallEngine(socketPath: string, timeoutMs = 1000): Promise
|
|
|
208
208
|
* skipped entirely in own-fallback mode. (c) `answerRecall`'s `embed(prompt)` call is now wrapped so
|
|
209
209
|
* either failure mode degrades to the SAME honest `cosine-fallback` reply shape as every other
|
|
210
210
|
* failure, never a bare protocol `{error}`.
|
|
211
|
+
*
|
|
212
|
+
* Bumped 12→13 (`retro-debt-sentinel-per-session`): the hook reads its admission-debt sentinel from
|
|
213
|
+
* `.dz/retro/<sessionId>/` (entry added retroactively for the record by the next bump).
|
|
214
|
+
*
|
|
215
|
+
* Bumped 13→14 (`daemon-install-root-from-self`, ADR-001, backlog 6dd464d5d6ee917f): the daemon's
|
|
216
|
+
* `INSTALL_ROOT` is two levels above its own directory (it was one short — `<root>/.claude` — so the
|
|
217
|
+
* install-root link never fired and the daemon served, and resolved its deps from, `cwd`); the hook's
|
|
218
|
+
* `reviveDaemon()` spawns it with `cwd: PROJECT` and `DZ_PROJECT_ROOT: PROJECT`.
|
|
211
219
|
*/
|
|
212
|
-
export const APPLY_LEG_VERSION =
|
|
220
|
+
export const APPLY_LEG_VERSION = 14;
|
|
213
221
|
|
|
214
222
|
/**
|
|
215
223
|
* Parse the `dz-apply-leg-version` stamp from a deployed helper file. Unlike
|
|
@@ -595,7 +603,17 @@ function reviveDaemon() {
|
|
|
595
603
|
const { spawn } = require('child_process');
|
|
596
604
|
const daemon = path.join(PROJECT, '.claude', 'helpers', 'dz-embed-daemon.mjs');
|
|
597
605
|
if (!fs.existsSync(daemon)) return;
|
|
598
|
-
|
|
606
|
+
// daemon-install-root-from-self (ADR-001 p.2): the daemon serves exactly the store this hook just
|
|
607
|
+
// checked. \`cwd\` so a self-healed daemon never inherits the caller's directory (the orphan with
|
|
608
|
+
// cwd=…/other from the 2026-09-15 field report); \`DZ_PROJECT_ROOT\` because the daemon ranks it
|
|
609
|
+
// ABOVE its own install root, so a stale value inherited by this hook would otherwise still
|
|
610
|
+
// re-route it (same class as AM-3 of apply-leg-install-root, for the SessionStart command).
|
|
611
|
+
const child = spawn(process.execPath, [daemon], {
|
|
612
|
+
cwd: PROJECT,
|
|
613
|
+
env: { ...process.env, DZ_PROJECT_ROOT: PROJECT },
|
|
614
|
+
detached: true,
|
|
615
|
+
stdio: 'ignore',
|
|
616
|
+
});
|
|
599
617
|
child.unref();
|
|
600
618
|
}, undefined);
|
|
601
619
|
}
|
|
@@ -922,7 +940,7 @@ export function embedDaemonSource(coreDistDir: string | null = null): string {
|
|
|
922
940
|
|
|
923
941
|
import { createServer } from 'node:net';
|
|
924
942
|
import { existsSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, renameSync, statSync } from 'node:fs';
|
|
925
|
-
import { join, dirname } from 'node:path';
|
|
943
|
+
import { join, dirname, resolve } from 'node:path';
|
|
926
944
|
import { createRequire } from 'node:module';
|
|
927
945
|
import { connect } from 'node:net';
|
|
928
946
|
import { createHash } from 'node:crypto';
|
|
@@ -938,7 +956,11 @@ console.log = (...a) => console.error(...a);
|
|
|
938
956
|
// recallHookSource's own PROJECT comment) — DZ_PROJECT_ROOT stays the TOP override for the daemon
|
|
939
957
|
// (a caller that explicitly names a project root always wins), then INSTALL_ROOT (this file's own
|
|
940
958
|
// location, when it owns a \`.dz/\`), then cwd.
|
|
941
|
-
|
|
959
|
+
// daemon-install-root-from-self (ADR-001 p.1): this file lives at <root>/.claude/helpers/, so the
|
|
960
|
+
// install root is TWO levels above its directory — the hook's own rule (path.resolve(__dirname, '..',
|
|
961
|
+
// '..')), written for ESM. The previous dirname(dirname(<file>)) stopped at <root>/.claude, so the
|
|
962
|
+
// install-root link never fired and the daemon always served (and resolved its deps from) cwd.
|
|
963
|
+
const INSTALL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
942
964
|
const PROJECT = process.env['DZ_PROJECT_ROOT'] ?? (existsSync(join(INSTALL_ROOT, '.dz')) ? INSTALL_ROOT : process.cwd());
|
|
943
965
|
|
|
944
966
|
// ADR-001 (hook-recall-hybrid-parity, D1): the SAME candidate-list resolution the hook uses for its
|
package/src/architecture.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* helpers at the bottom do the disk I/O for the CLI and the feature-adr end-of-run auto-update.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
|
|
15
15
|
/** One curated subsystem — the top-layer intent node. */
|
|
@@ -192,13 +192,26 @@ export function renderDriftReport(report: DriftReport): string {
|
|
|
192
192
|
* I/O helper (lazy `require`, never throws) shared by the CLI and the feature-adr end-of-run auto-update
|
|
193
193
|
* (FR-3), so both build the map from the identical scan. Deterministic: output is sorted.
|
|
194
194
|
*/
|
|
195
|
+
function isDirFollowingLinks(path: string): boolean {
|
|
196
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
197
|
+
}
|
|
198
|
+
|
|
195
199
|
export function scanWorkspacePackages(repoRoot: string): ScannedPackage[] {
|
|
196
200
|
try {
|
|
197
201
|
const base = join(repoRoot, 'packages', '@dzhechkov');
|
|
198
202
|
if (!existsSync(base)) return [];
|
|
199
203
|
const out: ScannedPackage[] = [];
|
|
204
|
+
const seenReal = new Set<string>();
|
|
200
205
|
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
201
|
-
|
|
206
|
+
// A symlink to a package directory is a package directory: `dz mutation-gate`'s shadow tree
|
|
207
|
+
// symlinks every sibling, and pnpm links workspace packages the same way. Dirent reports such
|
|
208
|
+
// an entry as a symlink, never as a directory, so follow it; a dangling link is skipped.
|
|
209
|
+
if (!entry.isDirectory() && !(entry.isSymbolicLink() && isDirFollowingLinks(join(base, entry.name)))) continue;
|
|
210
|
+
// An alias link to a directory already scanned is the SAME package — count it once.
|
|
211
|
+
let real: string;
|
|
212
|
+
try { real = realpathSync(join(base, entry.name)); } catch { continue; }
|
|
213
|
+
if (seenReal.has(real)) continue;
|
|
214
|
+
seenReal.add(real);
|
|
202
215
|
const pj = join(base, entry.name, 'package.json');
|
|
203
216
|
if (!existsSync(pj)) continue;
|
|
204
217
|
let json: { name?: string; dependencies?: Record<string, string>; peerDependencies?: Record<string, string>; optionalDependencies?: Record<string, string> };
|