@damiangilgonzalez/harness 1.3.15 → 1.3.16
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/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/install-apply.js +7 -1
- package/targets/session-memory-core.mjs +18 -4
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/install-apply.js
CHANGED
|
@@ -354,6 +354,11 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
|
|
|
354
354
|
if (target) {
|
|
355
355
|
const paths = targetPaths(target, undefined, cwd);
|
|
356
356
|
const rel = (abs) => relative(cwd, abs).split(sep).join('/');
|
|
357
|
+
// `.claude/worktrees/` is where Claude Code puts the checkout of each isolated branch. Nothing
|
|
358
|
+
// ignored it, so every file of every worktree showed up as untracked: noise in `git status`, a
|
|
359
|
+
// `git add .` away from being committed, and the memory hook fingerprinted them all on each turn
|
|
360
|
+
// (54 s on a project with 964 of them, against a 5 s timeout).
|
|
361
|
+
if (target === 'claude') wanted.push(CLAUDE_WORKTREES);
|
|
357
362
|
for (const id of Object.keys(readState(paths.stateFile).skills || {})) wanted.push(rel(paths.skillDir(id)));
|
|
358
363
|
// The per-file ledger is derived AND platform-shaped (symlink here, real copy on
|
|
359
364
|
// Windows), so committing it puts two incompatible inventories of one thing in one
|
|
@@ -530,7 +535,7 @@ export async function uninstall({ skillIds = [], agentIds = [], target, home, cw
|
|
|
530
535
|
export function removeTargetInstall({ target, home, cwd = process.cwd() }) {
|
|
531
536
|
const paths = targetPaths(target, home, cwd);
|
|
532
537
|
const state = readState(paths.stateFile);
|
|
533
|
-
const ignored = [paths.stateFile];
|
|
538
|
+
const ignored = [paths.stateFile, ...(target === 'claude' ? [join(cwd, CLAUDE_WORKTREES)] : [])];
|
|
534
539
|
const ownedIgnoreEntries = new Set(state.ignoreEntriesAdded || []);
|
|
535
540
|
const owned = [
|
|
536
541
|
...Object.values(state.skills || {}).flatMap((entry) => entry.files || []),
|
|
@@ -659,6 +664,7 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
|
|
|
659
664
|
// own knowledge — kept unless `withDocs` is set. Returns the paths touched.
|
|
660
665
|
// Note: backups live under `.rsc/backups/`, which this removes — so purge does not
|
|
661
666
|
// snapshot (a pre-purge backup would delete itself). It is the deliberate escape hatch.
|
|
667
|
+
const CLAUDE_WORKTREES = '.claude/worktrees/';
|
|
662
668
|
const RSC_IGNORE_HEADER = '# rsc local state (hooks, seals, logs) and managed skill links — machine-local';
|
|
663
669
|
|
|
664
670
|
// Merge consecutive rsc blocks into the first: drop a repeated header and the blank seam before it
|
|
@@ -46,9 +46,10 @@ function cleanId(value, prefix = 'session') {
|
|
|
46
46
|
return `${prefix}-${hash}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function git(cwd, args) {
|
|
49
|
+
function git(cwd, args, input) {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
const stdin = input === undefined ? 'ignore' : 'pipe';
|
|
52
|
+
return { ok: true, out: execFileSync('git', args, { windowsHide: true, cwd, input, encoding: 'utf8', stdio: [stdin, 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 }).trim() };
|
|
52
53
|
} catch {
|
|
53
54
|
return { ok: false, out: '' };
|
|
54
55
|
}
|
|
@@ -162,7 +163,15 @@ function parseStatus(output) {
|
|
|
162
163
|
return files;
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// One `git` for every file, not one per file. Spawning a process costs ~25 ms on Windows, and this
|
|
167
|
+
// runs on every hook event: a few hundred dirty files were enough to blow the 5 s hook timeout.
|
|
168
|
+
// `--stdin-paths` stops at the first path it cannot read (a deleted file), so on failure each file
|
|
169
|
+
// is hashed on its own, which keeps the old per-file `null` for the unreadable ones.
|
|
165
170
|
function fingerprintFiles(worktree, files) {
|
|
171
|
+
if (!files.length) return {};
|
|
172
|
+
const batch = git(worktree, ['hash-object', '--no-filters', '--stdin-paths'], `${files.join('\n')}\n`);
|
|
173
|
+
const hashes = batch.ok ? batch.out.split('\n') : [];
|
|
174
|
+
if (hashes.length === files.length) return Object.fromEntries(files.map((path, i) => [path, hashes[i]]));
|
|
166
175
|
return Object.fromEntries(files.map((path) => {
|
|
167
176
|
const result = git(worktree, ['hash-object', '--no-filters', '--', path]);
|
|
168
177
|
return [path, result.ok ? result.out : null];
|
|
@@ -305,7 +314,8 @@ export function capture(input = {}) {
|
|
|
305
314
|
const recordPath = join(store.root, 'sessions', `${storageId}.json`);
|
|
306
315
|
let anchor = readJson(anchorPath);
|
|
307
316
|
let existing = readJson(recordPath);
|
|
308
|
-
|
|
317
|
+
// The first snapshot only seeds a new anchor; an existing one already has its baseline.
|
|
318
|
+
const firstSnapshot = anchor ? null : snapshot(here, null);
|
|
309
319
|
if (!anchor) {
|
|
310
320
|
anchor = {
|
|
311
321
|
sessionId,
|
|
@@ -318,7 +328,11 @@ export function capture(input = {}) {
|
|
|
318
328
|
}
|
|
319
329
|
if (input.event === 'start' && !existing) return { record: null, path: null, notice: null, compactionHint: false };
|
|
320
330
|
|
|
321
|
-
|
|
331
|
+
// Against its own HEAD as baseline there is nothing committed to diff, so a fresh anchor's
|
|
332
|
+
// snapshot already is the repo state: reuse it instead of walking the tree a second time.
|
|
333
|
+
const repo = firstSnapshot && anchor.baselineHead === firstSnapshot.head
|
|
334
|
+
? firstSnapshot
|
|
335
|
+
: snapshot(here, anchor.baselineHead);
|
|
322
336
|
// Count what CHANGED, not which tool changed it. `editDelta` is +1 per Edit/Write tool event, and
|
|
323
337
|
// on its own it was blind to everything done through the shell — a heredoc, `sed -i`, a script, a
|
|
324
338
|
// formatter — which is how automated sessions work. The session that found this had touched dozens
|