litgrok-ai 0.2.6 → 0.2.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.7 (pending) — 2026-09-02
4
+
5
+ - Repair payload upgrades across the ownership-manifest transition: installs now
6
+ record a schema-checked `.grok/.litgrok-install-manifest.json` of SHA-256
7
+ payload hashes, classify files matching recorded hashes as installer-owned,
8
+ and atomically replace owned files during upgrades. Uninstall removes owned
9
+ files and the manifest only after payload removal succeeds.
10
+ - Migrate a pristine manifest-free 0.2.5 payload through the bundled seven-file
11
+ legacy hash snapshot, while malformed manifests and modified or unrecognized
12
+ files still fail closed before payload writes.
13
+ - Add regression coverage for historical migration, legacy user edits,
14
+ malformed ownership metadata, and manifest cleanup; enable full-history CI
15
+ checkout for the historical archive fixture and document the ownership
16
+ behavior in both READMEs.
17
+
3
18
  ## 0.2.6 (pending) — 2026-09-02
4
19
 
5
20
  - Harden inode-reuse handling across skill-loop state locks and the
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  </p>
23
23
  <p align="center">
24
24
  <img src="https://img.shields.io/badge/npm-litgrok--ai-cb3837" alt="npm: litgrok-ai" />
25
- <img src="https://img.shields.io/badge/version-0.2.6-2ea44f" alt="version 0.2.6" />
25
+ <img src="https://img.shields.io/badge/version-0.2.7-2ea44f" alt="version 0.2.7" />
26
26
  <img src="https://img.shields.io/badge/Grok%20Build-skills%20%2B%20rules-111827" alt="Grok Build skills and project rules" />
27
27
  <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
28
28
  </p>
@@ -105,7 +105,7 @@ npx --yes litgrok-ai@latest install
105
105
  Pin a version when you need a reproducible install:
106
106
 
107
107
  ```bash
108
- npx --yes litgrok-ai@0.2.6 install
108
+ npx --yes litgrok-ai@0.2.7 install
109
109
  ```
110
110
 
111
111
  Preview without writing files:
@@ -120,7 +120,7 @@ Default install writes the whole payload into the current project's `.grok/` tre
120
120
  npx --yes litgrok-ai@latest install --user
121
121
  ```
122
122
 
123
- `--user` targets `~/.grok/skills`, `~/.grok/rules`, and `~/.grok/hooks`. Dry-run, `NO_COLOR`, and non-interactive runs do not mutate files. Reinstall is idempotent for byte-identical payload files. A different file occupying any packaged path refuses the whole install rather than being overwritten.
123
+ `--user` targets `~/.grok/skills`, `~/.grok/rules`, and `~/.grok/hooks`. Dry-run, `NO_COLOR`, and non-interactive runs do not mutate files. The installer records SHA-256 ownership in `.grok/.litgrok-install-manifest.json`: byte-identical files stay untouched, and files matching a previously recorded payload hash may be replaced during an upgrade. A user-modified or otherwise unrecognized file refuses the whole install rather than being overwritten. A pristine pre-manifest 0.2.5 payload is recognized through the bundled migration snapshot while no manifest exists and receives a new manifest after the upgrade.
124
124
 
125
125
  ### Install output
126
126
 
@@ -146,7 +146,7 @@ npx --yes litgrok-ai@latest uninstall
146
146
  npx --yes litgrok-ai@latest uninstall --user
147
147
  ```
148
148
 
149
- If any installed payload file was changed, uninstall refuses the whole removal. Reinstall by running the matching `install` command again; byte-identical files are left untouched.
149
+ If any installed payload file was changed, uninstall refuses the whole removal. Files recorded as installer-owned are removed too, and the ownership manifest is removed only after the payload removal succeeds. Reinstall by running the matching `install` command again; byte-identical files are left untouched.
150
150
 
151
151
  ## First use
152
152
 
@@ -198,12 +198,12 @@ Review is proposal-only. `autoApply` is always false, transcript text is inert d
198
198
  From an installed project, use the package CLI directly when you want a replayable foreground command:
199
199
 
200
200
  ```bash
201
- npx --yes litgrok-ai@0.2.6 skill-loop review <session-id>
202
- npx --yes litgrok-ai@0.2.6 skill-loop list
203
- npx --yes litgrok-ai@0.2.6 skill-loop apply <proposal-id>
204
- npx --yes litgrok-ai@0.2.6 skill-loop reject <proposal-id>
205
- npx --yes litgrok-ai@0.2.6 skill-loop rollback <ledger-id>
206
- npx --yes litgrok-ai@0.2.6 skill-loop curator
201
+ npx --yes litgrok-ai@0.2.7 skill-loop review <session-id>
202
+ npx --yes litgrok-ai@0.2.7 skill-loop list
203
+ npx --yes litgrok-ai@0.2.7 skill-loop apply <proposal-id>
204
+ npx --yes litgrok-ai@0.2.7 skill-loop reject <proposal-id>
205
+ npx --yes litgrok-ai@0.2.7 skill-loop rollback <ledger-id>
206
+ npx --yes litgrok-ai@0.2.7 skill-loop curator
207
207
  ```
208
208
 
209
209
  `apply` prints the ledger id needed for rollback. The deterministic curator is also user-invoked: by default it runs at most once every seven days, requires at least two idle hours, marks eligible unpinned agent-owned skills stale after 30 idle days, and archives them outside discovery after 90 idle days. It makes a full backup before a transition and never deletes a skill package; `--force` bypasses only the seven-day run interval for an explicit test or maintenance invocation.
package/README_ko-KR.md CHANGED
@@ -22,7 +22,7 @@
22
22
  </p>
23
23
  <p align="center">
24
24
  <img src="https://img.shields.io/badge/npm-litgrok--ai-cb3837" alt="npm: litgrok-ai" />
25
- <img src="https://img.shields.io/badge/version-0.2.6-2ea44f" alt="version 0.2.6" />
25
+ <img src="https://img.shields.io/badge/version-0.2.7-2ea44f" alt="version 0.2.7" />
26
26
  <img src="https://img.shields.io/badge/Grok%20Build-skills%20%2B%20rules-111827" alt="Grok Build skills and project rules" />
27
27
  <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
28
28
  </p>
@@ -105,7 +105,7 @@ npx --yes litgrok-ai@latest install
105
105
  재현 가능한 설치는 버전을 고정합니다.
106
106
 
107
107
  ```bash
108
- npx --yes litgrok-ai@0.2.6 install
108
+ npx --yes litgrok-ai@0.2.7 install
109
109
  ```
110
110
 
111
111
  파일을 쓰지 않고 미리보기:
@@ -120,7 +120,7 @@ npx --yes litgrok-ai@latest install --dry-run
120
120
  npx --yes litgrok-ai@latest install --user
121
121
  ```
122
122
 
123
- `--user`는 `~/.grok/skills`, `~/.grok/rules`, `~/.grok/hooks`를 대상으로 합니다. dry-run, `NO_COLOR`, non-interactive는 파일을 바꾸지 않습니다. byte가 같은 payload 재설치는 멱등입니다. package path 하나라도 다른 파일이 차지하면 전체 설치를 거절하고 덮어쓰지 않습니다.
123
+ `--user`는 `~/.grok/skills`, `~/.grok/rules`, `~/.grok/hooks`를 대상으로 합니다. dry-run, `NO_COLOR`, non-interactive는 파일을 바꾸지 않습니다. installer는 `.grok/.litgrok-install-manifest.json`에 SHA-256 소유권을 기록합니다. byte가 같은 파일은 다시 쓰지 않고, 이전 payload hash와 일치하는 installer 소유 파일은 업그레이드에서 교체할 수 있습니다. 사용자가 바꿨거나 소유권을 확인할 수 없는 파일이 있으면 전체 설치를 거절하고 덮어쓰지 않습니다. manifest가 없을 때 0.2.5의 pristine payload는 내장 migration snapshot으로 인식하며 업그레이드 뒤 새 manifest를 기록합니다.
124
124
 
125
125
  ### 설치 출력
126
126
 
@@ -146,7 +146,7 @@ npx --yes litgrok-ai@latest uninstall
146
146
  npx --yes litgrok-ai@latest uninstall --user
147
147
  ```
148
148
 
149
- 설치된 payload 파일 하나라도 바뀌었다면 uninstall은 전체 삭제를 거절합니다. 재설치는 같은 `install` 명령을 다시 실행합니다. byte가 같은 파일은 다시 쓰지 않습니다.
149
+ 설치된 payload 파일 하나라도 바뀌었다면 uninstall은 전체 삭제를 거절합니다. installer 소유로 기록된 파일도 삭제하며, payload 삭제가 성공한 뒤에만 ownership manifest를 삭제합니다. 재설치는 같은 `install` 명령을 다시 실행합니다. byte가 같은 파일은 다시 쓰지 않습니다.
150
150
 
151
151
  ## 처음 사용하기
152
152
 
@@ -198,12 +198,12 @@ Review는 proposal만 만듭니다. `autoApply`는 항상 false이고 transcript
198
198
  설치된 project에서는 다음 package CLI를 foreground command로 실행할 수 있습니다.
199
199
 
200
200
  ```bash
201
- npx --yes litgrok-ai@0.2.6 skill-loop review <session-id>
202
- npx --yes litgrok-ai@0.2.6 skill-loop list
203
- npx --yes litgrok-ai@0.2.6 skill-loop apply <proposal-id>
204
- npx --yes litgrok-ai@0.2.6 skill-loop reject <proposal-id>
205
- npx --yes litgrok-ai@0.2.6 skill-loop rollback <ledger-id>
206
- npx --yes litgrok-ai@0.2.6 skill-loop curator
201
+ npx --yes litgrok-ai@0.2.7 skill-loop review <session-id>
202
+ npx --yes litgrok-ai@0.2.7 skill-loop list
203
+ npx --yes litgrok-ai@0.2.7 skill-loop apply <proposal-id>
204
+ npx --yes litgrok-ai@0.2.7 skill-loop reject <proposal-id>
205
+ npx --yes litgrok-ai@0.2.7 skill-loop rollback <ledger-id>
206
+ npx --yes litgrok-ai@0.2.7 skill-loop curator
207
207
  ```
208
208
 
209
209
  `apply`는 rollback에 필요한 ledger id를 출력합니다. Deterministic curator도 user-invoked입니다. 기본적으로 7일에 한 번만 실행되고, 마지막 activity 뒤 최소 2시간이 지나야 하며, pin되지 않은 agent-owned skill을 30일 idle 뒤 stale로 표시하고 90일 idle 뒤 discovery 밖으로 archive합니다. Transition 전에는 전체 backup을 만들고 skill package를 삭제하지 않습니다. `--force`는 명시적인 test 또는 maintenance 실행에서 7일 interval만 건너뜁니다.
package/bin/litgrok.mjs CHANGED
@@ -1,12 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { constants, copyFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync } from 'node:fs';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { constants, copyFileSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
5
  import { homedir } from 'node:os';
5
- import { dirname, join, relative, resolve, sep } from 'node:path';
6
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
9
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
9
10
  const PAYLOAD_ROOT = join(PACKAGE_ROOT, '.grok');
11
+ const INSTALL_MANIFEST_NAME = '.litgrok-install-manifest.json';
12
+ const INSTALL_MANIFEST_SCHEMA = 'litgrok.install-manifest/v1';
13
+ const PACKAGE_NAME = 'litgrok-ai';
14
+
15
+ // 0.2.5 was the last release before ownership manifests existed. These are the
16
+ // only payload hashes changed by the current security release, so a pristine
17
+ // 0.2.5 install can migrate while no manifest exists without treating its
18
+ // seven security-fix files as user conflicts.
19
+ const PRE_MANIFEST_PAYLOAD_HASHES = Object.freeze({
20
+ 'skills/autoconference/references/_canonical-corpus/manifest.json': '4554ee4041b73577a844abc27a97db91e941b8412f1888209203251dc1fee500',
21
+ 'skills/autoconference/scripts/init_conference.py': 'cb83623c1338b7e3a5be7a7beac7a17b8ae362fc24fbc01d1caff634db290dfc',
22
+ 'skills/autoconference/scripts/verify-canonical-corpus.mjs': '4a4afb7dc0b58519a4c521c987cb8998495afc09b89fb87cafb0f1db0047c28d',
23
+ 'skills/autoresearch/references/_canonical-corpus/manifest.json': '9667f852081d4877c579d14e63c0759ebbe863e4170a9c3d5e6487cdb1f0c323',
24
+ 'skills/autoresearch/scripts/init_research.py': 'a5434711928eb4d0487d933df5e1b374f8553140004c6ab8e38fe577ae074d05',
25
+ 'skills/autoresearch/scripts/verify-canonical-corpus.mjs': '80a85ed4ba0c168bd7f7abef5754c4089a629975a827b6ddb3f01cd813602274',
26
+ 'skills/skill-observer/scripts/skill-loop.mjs': '3f3f5265928c0d71175b0f23f71b836d1dc05e7efba407bfdc2d263c6aaed0ce',
27
+ });
10
28
  const USAGE = 'Usage: litgrok [install|uninstall] [--user|--project] [--dry-run] [--no-color] [--yes]\n litgrok skill-loop <review|propose|list|apply|reject|rollback|curator> ...\n';
11
29
  const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12
30
  const SPINNER_INTERVAL_MS = 80;
@@ -235,17 +253,160 @@ function reportFailure(error, { stderr, stdout, options, root, colors, step }) {
235
253
  stdout.write(failureCard(options, root, colors, error?.step ?? step, errorText(error)));
236
254
  }
237
255
 
238
- function targetState(source, target) {
256
+ function normalizedRelativePath(relativePath) {
257
+ return relativePath.split(sep).join('/');
258
+ }
259
+
260
+ function sha256File(filePath) {
261
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
262
+ }
263
+
264
+ function ownershipManifestFailure(reason) {
265
+ const message = `Refusing to use ${INSTALL_MANIFEST_NAME}: ${reason}`;
266
+ const error = new Error(message);
267
+ error.step = 'OWNERSHIP';
268
+ error.stderrLines = [message];
269
+ return error;
270
+ }
271
+
272
+ function ownershipManifestPath(root) {
273
+ return join(root, INSTALL_MANIFEST_NAME);
274
+ }
275
+
276
+ function readOwnershipManifest(root) {
277
+ const target = ownershipManifestPath(root);
278
+ let stat;
279
+ try {
280
+ stat = lstatSync(target);
281
+ } catch (error) {
282
+ if (error?.code === 'ENOENT') return null;
283
+ throw error;
284
+ }
285
+ if (stat.isSymbolicLink() || !stat.isFile()) throw ownershipManifestFailure('path is not a regular file');
286
+
287
+ let parsed;
288
+ try {
289
+ parsed = JSON.parse(readFileSync(target, 'utf8'));
290
+ } catch {
291
+ throw ownershipManifestFailure('file is not valid JSON');
292
+ }
293
+ if (
294
+ !parsed ||
295
+ parsed.schema !== INSTALL_MANIFEST_SCHEMA ||
296
+ parsed.package !== PACKAGE_NAME ||
297
+ typeof parsed.version !== 'string' ||
298
+ parsed.version.length === 0 ||
299
+ !parsed.files ||
300
+ Array.isArray(parsed.files) ||
301
+ typeof parsed.files !== 'object'
302
+ ) {
303
+ throw ownershipManifestFailure('schema is invalid');
304
+ }
305
+
306
+ const hashes = new Map();
307
+ for (const [relativePath, hash] of Object.entries(parsed.files)) {
308
+ const segments = relativePath.split('/');
309
+ if (
310
+ relativePath.length === 0 ||
311
+ relativePath.startsWith('/') ||
312
+ relativePath.includes('\\') ||
313
+ segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')
314
+ ) {
315
+ throw ownershipManifestFailure(`path is invalid: ${relativePath}`);
316
+ }
317
+ if (typeof hash !== 'string' || !/^[0-9a-f]{64}$/u.test(hash)) {
318
+ throw ownershipManifestFailure(`hash is invalid: ${relativePath}`);
319
+ }
320
+ hashes.set(relativePath, hash);
321
+ }
322
+ return { path: target, hashes };
323
+ }
324
+
325
+ function targetState(source, target, relativePath, manifest) {
239
326
  try {
240
327
  const targetStat = lstatSync(target);
241
328
  if (!targetStat.isFile()) return 'conflict';
242
- return readFileSync(source).equals(readFileSync(target)) ? 'current' : 'conflict';
329
+ const targetHash = sha256File(target);
330
+ if (targetHash === sha256File(source)) return 'current';
331
+ const previousHash = manifest?.hashes.get(normalizedRelativePath(relativePath)) ??
332
+ (manifest === null ? PRE_MANIFEST_PAYLOAD_HASHES[normalizedRelativePath(relativePath)] : undefined);
333
+ return targetHash === previousHash ? 'owned' : 'conflict';
243
334
  } catch (error) {
244
335
  if (error?.code === 'ENOENT') return 'missing';
245
336
  throw error;
246
337
  }
247
338
  }
248
339
 
340
+ function manifestContent(files) {
341
+ const hashes = Object.fromEntries(
342
+ files
343
+ .map(({ relativePath, source }) => [normalizedRelativePath(relativePath), sha256File(source)])
344
+ .sort(([left], [right]) => left.localeCompare(right)),
345
+ );
346
+ return `${JSON.stringify({
347
+ schema: INSTALL_MANIFEST_SCHEMA,
348
+ package: PACKAGE_NAME,
349
+ version: packageVersion(),
350
+ files: hashes,
351
+ }, null, 2)}\n`;
352
+ }
353
+
354
+ function writeOwnershipManifest(root, files, previousManifest) {
355
+ const target = ownershipManifestPath(root);
356
+ const content = manifestContent(files);
357
+ let existing;
358
+ try {
359
+ const stat = lstatSync(target);
360
+ if (stat.isSymbolicLink() || !stat.isFile()) throw ownershipManifestFailure('path is not a regular file');
361
+ existing = readFileSync(target, 'utf8');
362
+ } catch (error) {
363
+ if (error?.code !== 'ENOENT') throw error;
364
+ }
365
+ if (existing === content) return false;
366
+ if (previousManifest === null && existing !== undefined) {
367
+ throw ownershipManifestFailure('path appeared during installation');
368
+ }
369
+
370
+ const temporary = join(root, `.${basename(target)}.${randomUUID()}.tmp`);
371
+ try {
372
+ writeFileSync(temporary, content, { flag: 'wx', mode: 0o600 });
373
+ renameSync(temporary, target);
374
+ } finally {
375
+ try {
376
+ unlinkSync(temporary);
377
+ } catch (error) {
378
+ if (error?.code !== 'ENOENT') throw error;
379
+ }
380
+ }
381
+ return true;
382
+ }
383
+
384
+ function replaceOwnedFile(source, target) {
385
+ const temporary = join(dirname(target), `.${basename(target)}.${randomUUID()}.tmp`);
386
+ try {
387
+ copyFileSync(source, temporary, constants.COPYFILE_EXCL);
388
+ renameSync(temporary, target);
389
+ } finally {
390
+ try {
391
+ unlinkSync(temporary);
392
+ } catch (error) {
393
+ if (error?.code !== 'ENOENT') throw error;
394
+ }
395
+ }
396
+ }
397
+
398
+ function removeOwnershipManifest(root, previousManifest) {
399
+ if (previousManifest === null) return;
400
+ const target = ownershipManifestPath(root);
401
+ try {
402
+ const stat = lstatSync(target);
403
+ if (stat.isSymbolicLink() || !stat.isFile()) throw ownershipManifestFailure('path is not a regular file');
404
+ unlinkSync(target);
405
+ } catch (error) {
406
+ if (error?.code !== 'ENOENT') throw error;
407
+ }
408
+ }
409
+
249
410
  function discoverPayloadFiles(root = PAYLOAD_ROOT) {
250
411
  const files = [];
251
412
 
@@ -383,10 +544,12 @@ export async function run(args, context = {}) {
383
544
 
384
545
  step = 'OWNERSHIP';
385
546
  const states = runStep(progress, 'OWNERSHIP', () => {
386
- const owned = files.map(({ source, target }) => targetState(source, target));
547
+ const manifest = readOwnershipManifest(root);
548
+ const owned = files.map(({ relativePath, source, target }) => targetState(source, target, relativePath, manifest));
387
549
  const counts = {
388
550
  missing: owned.filter((state) => state === 'missing').length,
389
551
  current: owned.filter((state) => state === 'current').length,
552
+ owned: owned.filter((state) => state === 'owned').length,
390
553
  conflicts: owned.filter((state) => state === 'conflict').length,
391
554
  };
392
555
  if (counts.conflicts > 0) {
@@ -394,14 +557,14 @@ export async function run(args, context = {}) {
394
557
  for (let index = 0; index < files.length; index += 1) {
395
558
  if (owned[index] === 'conflict') stderrLines.push(`Refusing to ${options.command === 'install' ? 'overwrite' : 'remove'} different existing file: ${files[index].target}`);
396
559
  }
397
- const error = new Error(`${counts.missing} missing · ${counts.current} already-current · ${counts.conflicts} conflicts`);
560
+ const error = new Error(`${counts.missing} missing · ${counts.current} already-current · ${counts.owned} owned · ${counts.conflicts} conflicts`);
398
561
  error.step = 'OWNERSHIP';
399
562
  error.stderrLines = stderrLines;
400
563
  throw error;
401
564
  }
402
565
  return {
403
- value: { states: owned, counts },
404
- detail: `${counts.missing} missing · ${counts.current} already-current · ${counts.conflicts} conflicts`,
566
+ value: { states: owned, counts, manifest },
567
+ detail: `${counts.missing} missing · ${counts.current} already-current · ${counts.owned} owned · ${counts.conflicts} conflicts`,
405
568
  };
406
569
  });
407
570
 
@@ -410,12 +573,19 @@ export async function run(args, context = {}) {
410
573
  const writeStarted = process.hrtime.bigint();
411
574
  progress.start('WRITE');
412
575
  let removed = 0;
413
- for (let index = 0; index < files.length; index += 1) {
414
- if (states.states[index] !== 'current') continue;
415
- unlinkSync(files[index].target);
416
- removed += 1;
576
+ try {
577
+ for (let index = 0; index < files.length; index += 1) {
578
+ if (!['current', 'owned'].includes(states.states[index])) continue;
579
+ unlinkSync(files[index].target);
580
+ removed += 1;
581
+ }
582
+ removeOwnershipManifest(root, states.manifest);
583
+ progress.finish('ok', 'WRITE', `${removed} removed · ${states.counts.missing} already absent`, elapsedSince(writeStarted));
584
+ } catch (error) {
585
+ progress.finish('failed', 'WRITE', errorText(error), elapsedSince(writeStarted));
586
+ if (error && typeof error === 'object') error.step = 'WRITE';
587
+ throw error;
417
588
  }
418
- progress.finish('ok', 'WRITE', `${removed} removed · ${states.counts.missing} already absent`, elapsedSince(writeStarted));
419
589
  const receipt = receiptCard(options, displayRoot, colors, { status: 'Removed', written: 0, current: states.counts.current });
420
590
  progress.close();
421
591
  stdout.write(receipt);
@@ -423,6 +593,12 @@ export async function run(args, context = {}) {
423
593
  }
424
594
 
425
595
  if (states.states.every((state) => state === 'current')) {
596
+ try {
597
+ writeOwnershipManifest(root, files, states.manifest);
598
+ } catch (error) {
599
+ if (error && typeof error === 'object') error.step = 'WRITE';
600
+ throw error;
601
+ }
426
602
  skipStep(progress, 'WRITE', `SKIPPED · 0 written · ${states.counts.current} already-current`);
427
603
  const receipt = receiptCard(options, displayRoot, colors, { status: 'Already current', written: 0, current: states.counts.current });
428
604
  progress.close();
@@ -438,10 +614,12 @@ export async function run(args, context = {}) {
438
614
  if (states.states[index] === 'current') continue;
439
615
  const { source, target } = files[index];
440
616
  mkdirSync(dirname(target), { recursive: true });
441
- copyFileSync(source, target, constants.COPYFILE_EXCL);
617
+ if (states.states[index] === 'owned') replaceOwnedFile(source, target);
618
+ else copyFileSync(source, target, constants.COPYFILE_EXCL);
442
619
  installed += 1;
443
620
  }
444
- progress.finish('ok', 'WRITE', `${installed} written · ${states.counts.current} already-current`, elapsedSince(writeStarted));
621
+ writeOwnershipManifest(root, files, states.manifest);
622
+ progress.finish('ok', 'WRITE', `${installed} written · ${states.counts.current} already-current · ${states.counts.owned} owned`, elapsedSince(writeStarted));
445
623
  } catch (error) {
446
624
  progress.finish('failed', 'WRITE', errorText(error), elapsedSince(writeStarted));
447
625
  if (error && typeof error === 'object') error.step = 'WRITE';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litgrok-ai",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Grok Build skills, project rules, and hooks installer.",
5
5
  "type": "module",
6
6
  "bin": {
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litgrok",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Grok Build skills, project rules, and bounded lifecycle hooks for evidence-first lit work.",
5
5
  "author": {
6
6
  "name": "LitGrok contributors"