litgrok-ai 0.2.4 → 0.2.6
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/.grok/skills/autoconference/references/_canonical-corpus/manifest.json +4 -4
- package/.grok/skills/autoconference/scripts/init_conference.py +28 -2
- package/.grok/skills/autoconference/scripts/verify-canonical-corpus.mjs +3 -3
- package/.grok/skills/autoresearch/references/_canonical-corpus/manifest.json +4 -4
- package/.grok/skills/autoresearch/scripts/init_research.py +28 -2
- package/.grok/skills/autoresearch/scripts/verify-canonical-corpus.mjs +3 -3
- package/.grok/skills/skill-observer/scripts/skill-loop.mjs +151 -43
- package/CHANGELOG.md +23 -0
- package/README.md +91 -8
- package/README_ko-KR.md +91 -8
- package/bin/litgrok.mjs +310 -55
- package/package.json +3 -2
- package/plugin.json +1 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"selection": "25 companion files retained; SKILL.md is a Grok-native adapter"
|
|
7
7
|
},
|
|
8
8
|
"file_count": 25,
|
|
9
|
-
"byte_count":
|
|
10
|
-
"aggregate_sha256": "
|
|
9
|
+
"byte_count": 55711,
|
|
10
|
+
"aggregate_sha256": "406a7ca96354e3b863577cdbcb783122031fd451f967d0a2bfadfcdd1949fd34",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "assets/conference_template.md",
|
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"path": "scripts/init_conference.py",
|
|
104
|
-
"size":
|
|
105
|
-
"sha256": "
|
|
104
|
+
"size": 21865,
|
|
105
|
+
"sha256": "58aaaa91d3f31bdacfccac4c3a736efb62cb89563f7d37281be0e49f84651187"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"path": "templates/code-performance.md",
|
|
@@ -209,6 +209,23 @@ def preflight_capabilities() -> None:
|
|
|
209
209
|
)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def same_directory_identity(left: os.stat_result, right: os.stat_result) -> bool:
|
|
213
|
+
"""Compare path identity, including ctime to reject recreated directories."""
|
|
214
|
+
return (
|
|
215
|
+
left.st_dev,
|
|
216
|
+
left.st_ino,
|
|
217
|
+
left.st_mode,
|
|
218
|
+
left.st_uid,
|
|
219
|
+
left.st_ctime_ns,
|
|
220
|
+
) == (
|
|
221
|
+
right.st_dev,
|
|
222
|
+
right.st_ino,
|
|
223
|
+
right.st_mode,
|
|
224
|
+
right.st_uid,
|
|
225
|
+
right.st_ctime_ns,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
212
229
|
def open_safe_directory(path: Path) -> Tuple[int, Path]:
|
|
213
230
|
"""Create/open path one component at a time without following symlinks."""
|
|
214
231
|
absolute = Path(os.path.abspath(os.fspath(path)))
|
|
@@ -233,7 +250,16 @@ def open_safe_directory(path: Path) -> Tuple[int, Path]:
|
|
|
233
250
|
fail("UNSAFE_OUTPUT_SYMLINK", f"output component is a symlink: {absolute}")
|
|
234
251
|
if not stat.S_ISDIR(info.st_mode):
|
|
235
252
|
fail("UNSAFE_OUTPUT_PATH", f"output component is not a directory: {absolute}")
|
|
236
|
-
next_fd =
|
|
253
|
+
next_fd = None
|
|
254
|
+
try:
|
|
255
|
+
next_fd = os.open(part, flags | nofollow, dir_fd=fd)
|
|
256
|
+
opened = os.fstat(next_fd)
|
|
257
|
+
if not same_directory_identity(info, opened):
|
|
258
|
+
fail("UNSAFE_OUTPUT_SWAP", f"output component identity changed: {absolute}")
|
|
259
|
+
except BaseException:
|
|
260
|
+
if next_fd is not None:
|
|
261
|
+
os.close(next_fd)
|
|
262
|
+
raise
|
|
237
263
|
os.close(fd)
|
|
238
264
|
fd = next_fd
|
|
239
265
|
verify_directory_identity(absolute, fd)
|
|
@@ -251,7 +277,7 @@ def verify_directory_identity(path: Path, fd: int) -> None:
|
|
|
251
277
|
opened = os.fstat(fd)
|
|
252
278
|
if stat.S_ISLNK(named.st_mode) or not stat.S_ISDIR(named.st_mode):
|
|
253
279
|
fail("UNSAFE_OUTPUT_SYMLINK", f"output directory is no longer a real directory: {path}")
|
|
254
|
-
if (named
|
|
280
|
+
if not same_directory_identity(named, opened):
|
|
255
281
|
fail("UNSAFE_OUTPUT_SWAP", f"output directory identity changed: {path}")
|
|
256
282
|
|
|
257
283
|
|
|
@@ -11,8 +11,8 @@ const VERIFIER_RELATIVE = 'scripts/verify-canonical-corpus.mjs';
|
|
|
11
11
|
const EXPECTED = Object.freeze({
|
|
12
12
|
"schemaVersion": "litgrok.autoconference-corpus/v1",
|
|
13
13
|
"fileCount": 25,
|
|
14
|
-
"byteCount":
|
|
15
|
-
"aggregateSha256": "
|
|
14
|
+
"byteCount": 55711,
|
|
15
|
+
"aggregateSha256": "406a7ca96354e3b863577cdbcb783122031fd451f967d0a2bfadfcdd1949fd34",
|
|
16
16
|
"files": {
|
|
17
17
|
"assets/conference_template.md": "c1b0a1bbfb52080384e9bf6d725a91d2165a2105a4f055820660a607c913c7e7",
|
|
18
18
|
"assets/report_template.md": "73a00032dc143f34f67a97a257f76e7a5acd9365d367e0b27d0c72e439a336ab",
|
|
@@ -32,7 +32,7 @@ const EXPECTED = Object.freeze({
|
|
|
32
32
|
"references/modes/survey.md": "93866084c2abe5d7a1c6ef7251e5594e03044545ddda5e80c1ca42ef450769d0",
|
|
33
33
|
"references/results-logging.md": "829714f30af623aa78a4ebc9e13a0ba7135b2c876af3443e1c1274af8857fe96",
|
|
34
34
|
"references/visualization-guide.md": "4f7e705735b948038193c5a9c111e2db85672f5d97bb1c570aad217843868331",
|
|
35
|
-
"scripts/init_conference.py": "
|
|
35
|
+
"scripts/init_conference.py": "58aaaa91d3f31bdacfccac4c3a736efb62cb89563f7d37281be0e49f84651187",
|
|
36
36
|
"templates/code-performance.md": "a90d1425477d9aec002778ba695299cfabb593ccd3dbab82ed0d76038e17a4d5",
|
|
37
37
|
"templates/debate-mode.md": "9c74435dc7eb2e2d24d32b0a80381aadc62890f4674d916022ce6ef220aab966",
|
|
38
38
|
"templates/prompt-optimization.md": "8d78c233529533bb742986769941347eebc460fdb2a2dac63952e65883398e10",
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"selection": "27 companion files retained; SKILL.md is a Grok-native adapter"
|
|
7
7
|
},
|
|
8
8
|
"file_count": 27,
|
|
9
|
-
"byte_count":
|
|
10
|
-
"aggregate_sha256": "
|
|
9
|
+
"byte_count": 38805,
|
|
10
|
+
"aggregate_sha256": "0296ca6e52fb7cb68a37f3e17f148248146955ad9624e37e449e60b56eeeb1d3",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "assets/report_template.md",
|
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"path": "scripts/init_research.py",
|
|
139
|
-
"size":
|
|
140
|
-
"sha256": "
|
|
139
|
+
"size": 14161,
|
|
140
|
+
"sha256": "b675ea22abf0794e32dbbf59b95248f20878bbf3d071fa10d37b5a004a7f880c"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"path": "scripts/style_presets.py",
|
|
@@ -103,6 +103,23 @@ def preflight_capabilities() -> None:
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def same_directory_identity(left: os.stat_result, right: os.stat_result) -> bool:
|
|
107
|
+
"""Compare path identity, including ctime to reject recreated directories."""
|
|
108
|
+
return (
|
|
109
|
+
left.st_dev,
|
|
110
|
+
left.st_ino,
|
|
111
|
+
left.st_mode,
|
|
112
|
+
left.st_uid,
|
|
113
|
+
left.st_ctime_ns,
|
|
114
|
+
) == (
|
|
115
|
+
right.st_dev,
|
|
116
|
+
right.st_ino,
|
|
117
|
+
right.st_mode,
|
|
118
|
+
right.st_uid,
|
|
119
|
+
right.st_ctime_ns,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
106
123
|
def open_safe_directory(path: Path) -> Tuple[int, Path]:
|
|
107
124
|
"""Create/open path one component at a time without following symlinks."""
|
|
108
125
|
absolute = Path(os.path.abspath(os.fspath(path)))
|
|
@@ -127,7 +144,16 @@ def open_safe_directory(path: Path) -> Tuple[int, Path]:
|
|
|
127
144
|
fail("UNSAFE_OUTPUT_SYMLINK", f"output component is a symlink: {absolute}")
|
|
128
145
|
if not stat.S_ISDIR(info.st_mode):
|
|
129
146
|
fail("UNSAFE_OUTPUT_PATH", f"output component is not a directory: {absolute}")
|
|
130
|
-
next_fd =
|
|
147
|
+
next_fd = None
|
|
148
|
+
try:
|
|
149
|
+
next_fd = os.open(part, flags | nofollow, dir_fd=fd)
|
|
150
|
+
opened = os.fstat(next_fd)
|
|
151
|
+
if not same_directory_identity(info, opened):
|
|
152
|
+
fail("UNSAFE_OUTPUT_SWAP", f"output component identity changed: {absolute}")
|
|
153
|
+
except BaseException:
|
|
154
|
+
if next_fd is not None:
|
|
155
|
+
os.close(next_fd)
|
|
156
|
+
raise
|
|
131
157
|
os.close(fd)
|
|
132
158
|
fd = next_fd
|
|
133
159
|
verify_directory_identity(absolute, fd)
|
|
@@ -146,7 +172,7 @@ def verify_directory_identity(path: Path, fd: int) -> None:
|
|
|
146
172
|
opened = os.fstat(fd)
|
|
147
173
|
if stat.S_ISLNK(named.st_mode) or not stat.S_ISDIR(named.st_mode):
|
|
148
174
|
fail("UNSAFE_OUTPUT_SYMLINK", f"output directory is no longer a real directory: {path}")
|
|
149
|
-
if (named
|
|
175
|
+
if not same_directory_identity(named, opened):
|
|
150
176
|
fail("UNSAFE_OUTPUT_SWAP", f"output directory identity changed: {path}")
|
|
151
177
|
|
|
152
178
|
|
|
@@ -11,8 +11,8 @@ const VERIFIER_RELATIVE = 'scripts/verify-canonical-corpus.mjs';
|
|
|
11
11
|
const EXPECTED = Object.freeze({
|
|
12
12
|
"schemaVersion": "litgrok.autoresearch-corpus/v1",
|
|
13
13
|
"fileCount": 27,
|
|
14
|
-
"byteCount":
|
|
15
|
-
"aggregateSha256": "
|
|
14
|
+
"byteCount": 38805,
|
|
15
|
+
"aggregateSha256": "0296ca6e52fb7cb68a37f3e17f148248146955ad9624e37e449e60b56eeeb1d3",
|
|
16
16
|
"files": {
|
|
17
17
|
"assets/report_template.md": "b6b7607488736f24d3bacea8f3ec8563da40ff782cc1d85c064ab53b2488a4f9",
|
|
18
18
|
"assets/research_template.md": "726d175cd2db92dfe58a738117d9c6cc88211e519d3be8fb7cb05b66ba34c050",
|
|
@@ -39,7 +39,7 @@ const EXPECTED = Object.freeze({
|
|
|
39
39
|
"references/modes/ship/type-checklists.md": "6bea500594cad1e23bdeee2df34f6dd079d02b279773b20f914d313cabd630b3",
|
|
40
40
|
"references/results-logging.md": "4671c19df6d389b42091f2f117fd90726cb32f3082b44dbc9a895dc3625b9832",
|
|
41
41
|
"references/visualization-guide.md": "2ca249d74b30b41e7660c8b7ba91a59b0c33a1e74fc18b2ec2f867441a6e4b27",
|
|
42
|
-
"scripts/init_research.py": "
|
|
42
|
+
"scripts/init_research.py": "b675ea22abf0794e32dbbf59b95248f20878bbf3d071fa10d37b5a004a7f880c",
|
|
43
43
|
"scripts/style_presets.py": "b1c7d495016b0737b622684f4381f87990dfaae77934b3acae44e19742ebdfd5"
|
|
44
44
|
}
|
|
45
45
|
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
fstatSync,
|
|
11
11
|
fsyncSync,
|
|
12
12
|
lstatSync,
|
|
13
|
+
linkSync,
|
|
13
14
|
mkdirSync,
|
|
14
15
|
openSync,
|
|
15
16
|
readFileSync,
|
|
@@ -301,10 +302,119 @@ function validateLockFileSecurity(path, identity) {
|
|
|
301
302
|
return status;
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
function sameLockIdentity(left, right) {
|
|
306
|
+
return left.isFile() === right.isFile()
|
|
307
|
+
&& left.isSymbolicLink() === right.isSymbolicLink()
|
|
308
|
+
&& left.dev === right.dev
|
|
309
|
+
&& left.ino === right.ino
|
|
310
|
+
&& left.uid === right.uid
|
|
311
|
+
&& left.mode === right.mode
|
|
312
|
+
&& left.nlink === right.nlink
|
|
313
|
+
&& left.size === right.size
|
|
314
|
+
&& left.ctimeMs === right.ctimeMs
|
|
315
|
+
&& left.birthtimeMs === right.birthtimeMs;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sameLockHandle(left, right) {
|
|
319
|
+
return left.isFile() === right.isFile()
|
|
320
|
+
&& left.isSymbolicLink() === right.isSymbolicLink()
|
|
321
|
+
&& left.dev === right.dev
|
|
322
|
+
&& left.ino === right.ino
|
|
323
|
+
&& left.uid === right.uid
|
|
324
|
+
&& left.mode === right.mode
|
|
325
|
+
&& left.nlink === right.nlink
|
|
326
|
+
&& left.size === right.size;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function sameLockLocation(left, right) {
|
|
330
|
+
return left.isFile() === right.isFile()
|
|
331
|
+
&& left.isSymbolicLink() === right.isSymbolicLink()
|
|
332
|
+
&& left.dev === right.dev
|
|
333
|
+
&& left.ino === right.ino
|
|
334
|
+
&& left.uid === right.uid
|
|
335
|
+
&& left.nlink === right.nlink;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function inspectLockAgainstDescriptor(path, descriptor, identity, secure) {
|
|
339
|
+
const named = secure ? validateLockFileSecurity(path, identity) : lstatSync(path);
|
|
340
|
+
if (!named.isFile() || named.isSymbolicLink() || named.size > 4096 || named.nlink !== 1) fail('STATE_LOCK_INVALID', path);
|
|
341
|
+
const held = fstatSync(descriptor);
|
|
342
|
+
if (held.size > 4096) fail('STATE_LOCK_INVALID', path);
|
|
343
|
+
let reader;
|
|
344
|
+
try {
|
|
345
|
+
reader = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
346
|
+
const opened = fstatSync(reader);
|
|
347
|
+
if (!sameLockLocation(opened, held) || opened.size !== named.size) fail('STATE_LOCK_RACE', path);
|
|
348
|
+
const bytes = readFileSync(reader);
|
|
349
|
+
const current = secure ? validateLockFileSecurity(path, identity) : lstatSync(path);
|
|
350
|
+
if (!sameLockLocation(current, held) || current.size !== opened.size) fail('STATE_LOCK_RACE', path);
|
|
351
|
+
return { bytes, digest: createHash('sha256').update(bytes).digest('hex'), status: current };
|
|
352
|
+
} finally {
|
|
353
|
+
if (reader !== undefined) closeSync(reader);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function restoreMovedLock(path, tombstone) {
|
|
358
|
+
try {
|
|
359
|
+
linkSync(tombstone, path);
|
|
360
|
+
unlinkSync(tombstone);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function removePinnedLock(path, descriptor, identity, expected, expectedBytes, code, {
|
|
367
|
+
secure = false,
|
|
368
|
+
tokenWritten = true,
|
|
369
|
+
bytesWritten = Buffer.byteLength(expectedBytes, 'utf8'),
|
|
370
|
+
faultInjector,
|
|
371
|
+
phase,
|
|
372
|
+
} = {}) {
|
|
373
|
+
const bytes = Buffer.from(expectedBytes, 'utf8');
|
|
374
|
+
const prefix = bytes.subarray(0, bytesWritten);
|
|
375
|
+
const matches = (entry, beforeRename) => {
|
|
376
|
+
const identityMatches = secure ? sameLockHandle(entry.status, expected) : sameLockLocation(entry.status, expected);
|
|
377
|
+
if (!identityMatches) return false;
|
|
378
|
+
if (tokenWritten ? !entry.bytes.equals(bytes) : !entry.bytes.equals(prefix)) return false;
|
|
379
|
+
return !beforeRename || tokenWritten || bytesWritten > 0 || sameLockIdentity(entry.status, expected);
|
|
380
|
+
};
|
|
381
|
+
let checked;
|
|
382
|
+
try {
|
|
383
|
+
checked = inspectLockAgainstDescriptor(path, descriptor, identity, secure);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error?.code === 'ENOENT') return 'vanished';
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
if (!matches(checked, true)) fail(code, path);
|
|
389
|
+
faultInjector?.(phase, { path, descriptor, expectedBytes });
|
|
390
|
+
checked = inspectLockAgainstDescriptor(path, descriptor, identity, secure);
|
|
391
|
+
if (!matches(checked, true)) fail(code, path);
|
|
392
|
+
const tombstone = `${path}.cleanup-${process.pid}-${randomUUID()}`;
|
|
393
|
+
try {
|
|
394
|
+
renameSync(path, tombstone);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error?.code === 'ENOENT') return 'vanished';
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
let moved;
|
|
400
|
+
try {
|
|
401
|
+
moved = inspectLockAgainstDescriptor(tombstone, descriptor, identity, secure);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
restoreMovedLock(path, tombstone);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
if (!matches(moved, false)) {
|
|
407
|
+
restoreMovedLock(path, tombstone);
|
|
408
|
+
fail(code, path);
|
|
409
|
+
}
|
|
410
|
+
unlinkSync(tombstone);
|
|
411
|
+
return 'removed';
|
|
412
|
+
}
|
|
413
|
+
|
|
304
414
|
function readSecureLock(path, identity, options = {}) {
|
|
305
415
|
let expected;
|
|
306
416
|
try {
|
|
307
|
-
expected = validateLockFileSecurity(path, identity);
|
|
417
|
+
expected = options.expectedStatus ?? validateLockFileSecurity(path, identity);
|
|
308
418
|
} catch (error) {
|
|
309
419
|
if (options.missingLock === 'propagate' && error?.code === 'ENOENT') throw error;
|
|
310
420
|
throw error instanceof SkillLoopError ? error : new SkillLoopError('STATE_LOCK_INVALID', path);
|
|
@@ -313,12 +423,15 @@ function readSecureLock(path, identity, options = {}) {
|
|
|
313
423
|
try {
|
|
314
424
|
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
315
425
|
const opened = fstatSync(descriptor);
|
|
316
|
-
if (!opened.isFile() || opened.size > 4096 || opened.nlink !== 1
|
|
317
|
-
|
|
318
|
-
|
|
426
|
+
if (!opened.isFile() || opened.size > 4096 || opened.nlink !== 1 || !sameLockIdentity(opened, expected)) {
|
|
427
|
+
fail('STATE_LOCK_INVALID', path);
|
|
428
|
+
}
|
|
429
|
+
const bytes = readFileSync(descriptor);
|
|
430
|
+
const contents = bytes.toString('utf8').trim();
|
|
431
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
319
432
|
const current = validateLockFileSecurity(path, identity);
|
|
320
|
-
if (current
|
|
321
|
-
return { contents, status: current };
|
|
433
|
+
if (!sameLockIdentity(current, opened)) fail('STATE_LOCK_RACE', path);
|
|
434
|
+
return { contents, digest, status: current };
|
|
322
435
|
} catch (error) {
|
|
323
436
|
if (error instanceof SkillLoopError) throw error;
|
|
324
437
|
if (options.missingLock === 'propagate' && error?.code === 'ENOENT') throw error;
|
|
@@ -344,12 +457,12 @@ function reclaimStaleLock(path, identity, options = {}) {
|
|
|
344
457
|
options.faultInjector?.('reclaim-read', { path });
|
|
345
458
|
let secureRead;
|
|
346
459
|
try {
|
|
347
|
-
secureRead = readSecureLock(path, identity, { missingLock: 'propagate' });
|
|
460
|
+
secureRead = readSecureLock(path, identity, { missingLock: 'propagate', expectedStatus: initial });
|
|
348
461
|
} catch (error) {
|
|
349
462
|
if (error?.code === 'ENOENT') return 'vanished';
|
|
350
463
|
throw error;
|
|
351
464
|
}
|
|
352
|
-
const { contents, status } = secureRead;
|
|
465
|
+
const { contents, digest, status } = secureRead;
|
|
353
466
|
let record;
|
|
354
467
|
if (/^[1-9][0-9]{0,14}$/.test(contents)) {
|
|
355
468
|
if (identity.platform === 'win32') fail('STATE_LOCK_INVALID', 'legacy owner unavailable');
|
|
@@ -381,57 +494,46 @@ function reclaimStaleLock(path, identity, options = {}) {
|
|
|
381
494
|
throw error;
|
|
382
495
|
}
|
|
383
496
|
const moved = readSecureLock(tombstone, identity);
|
|
384
|
-
if (moved.status
|
|
385
|
-
|
|
497
|
+
if (!sameLockHandle(moved.status, status) || moved.contents !== contents || moved.digest !== digest) {
|
|
498
|
+
restoreMovedLock(path, tombstone);
|
|
386
499
|
fail('STATE_LOCK_RACE', path);
|
|
387
500
|
}
|
|
388
501
|
unlinkSync(tombstone);
|
|
389
502
|
return 'reclaimed';
|
|
390
503
|
}
|
|
391
504
|
|
|
392
|
-
function cleanupFailedLockAcquisition(path, descriptor, created, expectedBytes, tokenWritten) {
|
|
393
|
-
try {
|
|
394
|
-
closeSync(descriptor);
|
|
395
|
-
} catch {
|
|
396
|
-
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', 'descriptor');
|
|
397
|
-
}
|
|
398
|
-
let current;
|
|
505
|
+
function cleanupFailedLockAcquisition(path, descriptor, created, expectedBytes, tokenWritten, bytesWritten, identity, options = {}) {
|
|
399
506
|
try {
|
|
400
|
-
current = lstatSync(path);
|
|
401
|
-
} catch {
|
|
402
|
-
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', 'missing');
|
|
403
|
-
}
|
|
404
|
-
if (!current.isFile() || current.isSymbolicLink() || current.dev !== created.dev || current.ino !== created.ino
|
|
405
|
-
|| current.nlink !== 1 || current.size > 4096) fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', 'identity');
|
|
406
|
-
let reader;
|
|
407
|
-
if (tokenWritten) {
|
|
408
507
|
try {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
508
|
+
removePinnedLock(path, descriptor, identity, created, expectedBytes, 'STATE_LOCK_ACQUISITION_CLEANUP_FAILED', {
|
|
509
|
+
tokenWritten,
|
|
510
|
+
bytesWritten,
|
|
511
|
+
faultInjector: options.faultInjector,
|
|
512
|
+
phase: 'before-acquisition-cleanup-rename',
|
|
513
|
+
});
|
|
413
514
|
} catch (error) {
|
|
414
|
-
if (error
|
|
415
|
-
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', '
|
|
416
|
-
}
|
|
417
|
-
|
|
515
|
+
if (error?.code === 'STATE_LOCK_ACQUISITION_CLEANUP_FAILED') throw error;
|
|
516
|
+
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', error?.code ?? 'retargeted');
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
try {
|
|
520
|
+
closeSync(descriptor);
|
|
521
|
+
} catch {
|
|
522
|
+
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', 'descriptor');
|
|
418
523
|
}
|
|
419
524
|
}
|
|
420
|
-
const final = lstatSync(path);
|
|
421
|
-
if (final.dev !== created.dev || final.ino !== created.ino || final.nlink !== 1) {
|
|
422
|
-
fail('STATE_LOCK_ACQUISITION_CLEANUP_FAILED', 'retargeted');
|
|
423
|
-
}
|
|
424
|
-
unlinkSync(path);
|
|
425
525
|
}
|
|
426
526
|
|
|
427
527
|
export function withStateLock(path, operation, attempts = 200, options = {}) {
|
|
428
528
|
safeMkdir(dirname(path));
|
|
429
529
|
const identity = lockIdentity(path);
|
|
430
530
|
let descriptor;
|
|
531
|
+
let ownerBytes;
|
|
431
532
|
for (let index = 0; index < attempts; index += 1) {
|
|
432
533
|
let created;
|
|
433
534
|
let expectedBytes;
|
|
434
535
|
let tokenWritten = false;
|
|
536
|
+
let bytesWritten = 0;
|
|
435
537
|
try {
|
|
436
538
|
descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
437
539
|
created = fstatSync(descriptor);
|
|
@@ -442,12 +544,15 @@ export function withStateLock(path, operation, attempts = 200, options = {}) {
|
|
|
442
544
|
createdAt: new Date().toISOString(),
|
|
443
545
|
nonce: randomUUID(),
|
|
444
546
|
})}\n`;
|
|
547
|
+
ownerBytes = expectedBytes;
|
|
445
548
|
const bytes = Buffer.from(expectedBytes, 'utf8');
|
|
446
549
|
let offset = 0;
|
|
447
550
|
while (offset < bytes.length) {
|
|
551
|
+
options.faultInjector?.('before-token-write', { descriptor, path });
|
|
448
552
|
const written = writeSync(descriptor, bytes, offset, bytes.length - offset);
|
|
449
553
|
if (!Number.isSafeInteger(written) || written < 1) fail('STATE_LOCK_WRITE_FAILED');
|
|
450
554
|
offset += written;
|
|
555
|
+
bytesWritten = offset;
|
|
451
556
|
}
|
|
452
557
|
tokenWritten = true;
|
|
453
558
|
fsyncSync(descriptor);
|
|
@@ -456,7 +561,7 @@ export function withStateLock(path, operation, attempts = 200, options = {}) {
|
|
|
456
561
|
break;
|
|
457
562
|
} catch (error) {
|
|
458
563
|
if (descriptor !== undefined) {
|
|
459
|
-
cleanupFailedLockAcquisition(path, descriptor, created, expectedBytes, tokenWritten);
|
|
564
|
+
cleanupFailedLockAcquisition(path, descriptor, created, expectedBytes, tokenWritten, bytesWritten, identity, options);
|
|
460
565
|
descriptor = undefined;
|
|
461
566
|
}
|
|
462
567
|
if (error?.code !== 'EEXIST') throw error;
|
|
@@ -474,13 +579,16 @@ export function withStateLock(path, operation, attempts = 200, options = {}) {
|
|
|
474
579
|
try {
|
|
475
580
|
return operation();
|
|
476
581
|
} finally {
|
|
477
|
-
closeSync(descriptor);
|
|
478
582
|
try {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
583
|
+
removePinnedLock(path, descriptor, identity, owned, ownerBytes, 'STATE_LOCK_OWNERSHIP_LOST', {
|
|
584
|
+
secure: true,
|
|
585
|
+
faultInjector: options.faultInjector,
|
|
586
|
+
phase: 'before-release-rename',
|
|
587
|
+
});
|
|
482
588
|
} catch (error) {
|
|
483
589
|
if (error?.code !== 'ENOENT') throw error;
|
|
590
|
+
} finally {
|
|
591
|
+
closeSync(descriptor);
|
|
484
592
|
}
|
|
485
593
|
}
|
|
486
594
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.6 (pending) — 2026-09-02
|
|
4
|
+
|
|
5
|
+
- Harden inode-reuse handling across skill-loop state locks and the
|
|
6
|
+
autoconference/autoresearch output scaffolders: lock reclaim, failed-acquisition
|
|
7
|
+
cleanup, and release stay bound to descriptor identity, ctime/birthtime, and
|
|
8
|
+
exact bytes through tombstone checks, while opened output directories are
|
|
9
|
+
revalidated against full stat identity before publication. Add same-inode race
|
|
10
|
+
regressions and regenerate the affected canonical manifests so replacements fail
|
|
11
|
+
closed while authorized rotations remain valid.
|
|
12
|
+
|
|
13
|
+
## 0.2.5 (pending) — 2026-09-01
|
|
14
|
+
|
|
15
|
+
- Add the shared stacked LitFamily ASCII wordmark to the installer and lead both
|
|
16
|
+
READMEs with the version-free wordmark.
|
|
17
|
+
- Add a CI-enforced, manifest-driven version-lockstep guard for all eight release
|
|
18
|
+
sites, counting both plain and escaped-regex pins.
|
|
19
|
+
- Report model-picker and output-style selection as Grok host limits rather than
|
|
20
|
+
shipped surfaces: the installer writes no model or style keys, and
|
|
21
|
+
`grok inspect --json` exposes no `model`, `outputStyles`, or `settings` keys.
|
|
22
|
+
- Add installer-depth coverage for explicit `--yes` consent, non-interactive
|
|
23
|
+
no-write behavior, host-limit receipts, measured install phases, and isolated
|
|
24
|
+
plugin validation.
|
|
25
|
+
|
|
3
26
|
## 0.2.4 (pending) — 2026-08-31
|
|
4
27
|
|
|
5
28
|
- Ship the Grok plugin manifest, eleven Grok-native agents, and complete reference
|
package/README.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
<p align="center"><img src="./cover.png" width="100%" alt="LitGrok — Grok Build skills and project rules, installed with npx" /></p>
|
|
2
2
|
|
|
3
|
+
```text
|
|
4
|
+
██╗ ██╗████████╗
|
|
5
|
+
██║ ██║╚══██╔══╝
|
|
6
|
+
██║ ██║ ██║
|
|
7
|
+
███████╗██║ ██║
|
|
8
|
+
╚══════╝╚═╝ ╚═╝
|
|
9
|
+
██████╗ ██████╗ ██████╗ ██╗ ██╗
|
|
10
|
+
██╔════╝ ██╔══██╗██╔═══██╗██║ ██╔╝
|
|
11
|
+
██║ ███╗██████╔╝██║ ██║█████╔╝
|
|
12
|
+
╚██████╔╝██║ ██║╚██████╔╝██║ ██╗
|
|
13
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
|
14
|
+
```
|
|
15
|
+
|
|
3
16
|
<h1 align="center">LitGrok</h1>
|
|
4
17
|
<p align="center">
|
|
5
18
|
<em>Grok Build skills and project rules for evidence-first lit work, installed with <code>npx</code>.</em>
|
|
@@ -9,7 +22,7 @@
|
|
|
9
22
|
</p>
|
|
10
23
|
<p align="center">
|
|
11
24
|
<img src="https://img.shields.io/badge/npm-litgrok--ai-cb3837" alt="npm: litgrok-ai" />
|
|
12
|
-
<img src="https://img.shields.io/badge/version-0.2.
|
|
25
|
+
<img src="https://img.shields.io/badge/version-0.2.6-2ea44f" alt="version 0.2.6" />
|
|
13
26
|
<img src="https://img.shields.io/badge/Grok%20Build-skills%20%2B%20rules-111827" alt="Grok Build skills and project rules" />
|
|
14
27
|
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
|
|
15
28
|
</p>
|
|
@@ -18,6 +31,37 @@ LitGrok is the Grok Build member of the lit workflow family. The npm package is
|
|
|
18
31
|
|
|
19
32
|
This package does not log in to Grok, start the TUI, or claim live host behavior.
|
|
20
33
|
|
|
34
|
+
### The workflow in one view
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart TD
|
|
38
|
+
R["a request<br/>make it better"] --> DI["<b>deep-interview</b><br/>turn it into a decision-complete brief"]
|
|
39
|
+
DI --> P["<b>lit-plan</b><br/>objective · non-goals<br/>action / output / <b>binary verification</b>"]
|
|
40
|
+
P --> GATE{"user approves?"}
|
|
41
|
+
GATE -->|no| P
|
|
42
|
+
GATE -->|yes| SW["<b>start-work</b><br/>execute one slice"]
|
|
43
|
+
|
|
44
|
+
subgraph LOOP["each slice: RED to GREEN to SURFACE to CLEAN"]
|
|
45
|
+
SW --> RED["failing test first"]
|
|
46
|
+
RED --> GREEN["smallest change that passes"]
|
|
47
|
+
GREEN --> SURF["exercise the <b>real surface</b><br/>not just the test"]
|
|
48
|
+
SURF --> CLEAN["tear down · cleanup receipt"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
CLEAN --> EV{"evidence complete?"}
|
|
52
|
+
EV -->|"tests only"| SW
|
|
53
|
+
EV -->|"artifact + receipt"| RW["<b>review-work</b><br/>scope · evidence · payload<br/>security · real surface"]
|
|
54
|
+
RW -->|findings| SW
|
|
55
|
+
RW -->|clean| HO["<b>lit-handoff</b><br/>resumable packet"]
|
|
56
|
+
|
|
57
|
+
style GATE fill:#fff3cd,stroke:#856404
|
|
58
|
+
style EV fill:#fff3cd,stroke:#856404
|
|
59
|
+
style SURF fill:#d4edda,stroke:#155724
|
|
60
|
+
style RW fill:#d1ecf1,stroke:#0c5460
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **Execution discipline.** A plan earns approval only when every item has binary verification. A slice closes after the real surface produces an artifact and its QA resources are torn down; passing tests alone never closes it.
|
|
64
|
+
|
|
21
65
|
## What it is
|
|
22
66
|
|
|
23
67
|
- 34 Grok-native skill documents at `.grok/skills/<name>/SKILL.md`.
|
|
@@ -31,6 +75,27 @@ This package does not log in to Grok, start the TUI, or claim live host behavior
|
|
|
31
75
|
|
|
32
76
|
Grok Build loads skills from project, user, plugin, and configured paths. Project guidance uses AGENTS-family files and `.grok/rules/*.md`; hooks load from project or user `.grok/hooks/`. LitGrok installs only these documented direct surfaces.
|
|
33
77
|
|
|
78
|
+
### How it connects to Grok Build
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart LR
|
|
82
|
+
subgraph GB["Grok Build"]
|
|
83
|
+
PJ["plugin.json"]; HK["hooks"]; RL["rules"]
|
|
84
|
+
end
|
|
85
|
+
subgraph LG["LitGrok plugin"]
|
|
86
|
+
PLAN["plan gate"]
|
|
87
|
+
HEDGE["deliverable hedge guard"]
|
|
88
|
+
LEDGER[("session ledger")]
|
|
89
|
+
end
|
|
90
|
+
PJ --> LG
|
|
91
|
+
HK --> PLAN --> LEDGER
|
|
92
|
+
HK --> HEDGE
|
|
93
|
+
RL --> LG
|
|
94
|
+
LG --> S["34 skills · 11 agents"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **A narrow host boundary.** `plugin.json` exposes the package, hooks feed the plan gate and hedge guard, and rules provide project guidance while the session ledger records bounded state. Together these surfaces carry LitGrok’s 34 skills and 11 agents into Grok Build.
|
|
98
|
+
|
|
34
99
|
## Install
|
|
35
100
|
|
|
36
101
|
```bash
|
|
@@ -40,7 +105,7 @@ npx --yes litgrok-ai@latest install
|
|
|
40
105
|
Pin a version when you need a reproducible install:
|
|
41
106
|
|
|
42
107
|
```bash
|
|
43
|
-
npx --yes litgrok-ai@0.2.
|
|
108
|
+
npx --yes litgrok-ai@0.2.6 install
|
|
44
109
|
```
|
|
45
110
|
|
|
46
111
|
Preview without writing files:
|
|
@@ -87,6 +152,24 @@ If any installed payload file was changed, uninstall refuses the whole removal.
|
|
|
87
152
|
|
|
88
153
|
After install, trust project hooks, then open or restart Grok Build in that project. Skills and the rule are guidance surfaces; hook commands are the only executable payload files, and they receive documented event JSON on stdin.
|
|
89
154
|
|
|
155
|
+
### What the packed payload guarantees
|
|
156
|
+
|
|
157
|
+
```mermaid
|
|
158
|
+
flowchart LR
|
|
159
|
+
SK["a skill"] --> Q{"does it declare<br/>a capability?"}
|
|
160
|
+
Q -->|"self-contained<br/>procedure"| AL["explicit allowlist entry<br/>with a written reason"]
|
|
161
|
+
Q -->|"needs a corpus"| C["corpus must resolve<br/>inside the <b>packed payload</b>"]
|
|
162
|
+
AL --> G1
|
|
163
|
+
C --> G1["<b>payload-substance</b>"]
|
|
164
|
+
G1 --> G2["<b>cross-product parity</b><br/>one product cannot ship a stub<br/>where the family ships substance"]
|
|
165
|
+
G2 --> G3["<b>referenced-path resolution</b><br/>every path in a SKILL.md<br/>must exist in the tarball"]
|
|
166
|
+
G3 --> OK["installs and works<br/>on a machine that has<br/>nothing else"]
|
|
167
|
+
style C fill:#d4edda,stroke:#155724
|
|
168
|
+
style OK fill:#d4edda,stroke:#155724
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
> **The tarball is the shipping boundary.** A file present in the repository is not necessarily shipped. Each `npm pack` gate checks the allowlist or required corpus, parity, and every referenced path, so a missing dependency is caught before release rather than on a user’s machine.
|
|
172
|
+
|
|
90
173
|
Confirm the files exist:
|
|
91
174
|
|
|
92
175
|
```bash
|
|
@@ -115,12 +198,12 @@ Review is proposal-only. `autoApply` is always false, transcript text is inert d
|
|
|
115
198
|
From an installed project, use the package CLI directly when you want a replayable foreground command:
|
|
116
199
|
|
|
117
200
|
```bash
|
|
118
|
-
npx --yes litgrok-ai@0.2.
|
|
119
|
-
npx --yes litgrok-ai@0.2.
|
|
120
|
-
npx --yes litgrok-ai@0.2.
|
|
121
|
-
npx --yes litgrok-ai@0.2.
|
|
122
|
-
npx --yes litgrok-ai@0.2.
|
|
123
|
-
npx --yes litgrok-ai@0.2.
|
|
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
|
|
124
207
|
```
|
|
125
208
|
|
|
126
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
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
<p align="center"><img src="./cover.png" width="100%" alt="LitGrok — npx로 설치하는 Grok Build skill과 project rule" /></p>
|
|
2
2
|
|
|
3
|
+
```text
|
|
4
|
+
██╗ ██╗████████╗
|
|
5
|
+
██║ ██║╚══██╔══╝
|
|
6
|
+
██║ ██║ ██║
|
|
7
|
+
███████╗██║ ██║
|
|
8
|
+
╚══════╝╚═╝ ╚═╝
|
|
9
|
+
██████╗ ██████╗ ██████╗ ██╗ ██╗
|
|
10
|
+
██╔════╝ ██╔══██╗██╔═══██╗██║ ██╔╝
|
|
11
|
+
██║ ███╗██████╔╝██║ ██║█████╔╝
|
|
12
|
+
╚██████╔╝██║ ██║╚██████╔╝██║ ██╗
|
|
13
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
|
14
|
+
```
|
|
15
|
+
|
|
3
16
|
<h1 align="center">LitGrok</h1>
|
|
4
17
|
<p align="center">
|
|
5
18
|
<em>Grok Build용 skill과 project rule. <code>npx</code>로 설치합니다.</em>
|
|
@@ -9,7 +22,7 @@
|
|
|
9
22
|
</p>
|
|
10
23
|
<p align="center">
|
|
11
24
|
<img src="https://img.shields.io/badge/npm-litgrok--ai-cb3837" alt="npm: litgrok-ai" />
|
|
12
|
-
<img src="https://img.shields.io/badge/version-0.2.
|
|
25
|
+
<img src="https://img.shields.io/badge/version-0.2.6-2ea44f" alt="version 0.2.6" />
|
|
13
26
|
<img src="https://img.shields.io/badge/Grok%20Build-skills%20%2B%20rules-111827" alt="Grok Build skills and project rules" />
|
|
14
27
|
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license" />
|
|
15
28
|
</p>
|
|
@@ -18,6 +31,37 @@ LitGrok은 lit workflow family의 Grok Build 포트입니다. npm 패키지 이
|
|
|
18
31
|
|
|
19
32
|
Grok에 로그인하거나 TUI를 띄우거나, live host 동작을 주장하지 않습니다.
|
|
20
33
|
|
|
34
|
+
### 작업 흐름 한눈에 보기
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart TD
|
|
38
|
+
R["a request<br/>make it better"] --> DI["<b>deep-interview</b><br/>turn it into a decision-complete brief"]
|
|
39
|
+
DI --> P["<b>lit-plan</b><br/>objective · non-goals<br/>action / output / <b>binary verification</b>"]
|
|
40
|
+
P --> GATE{"user approves?"}
|
|
41
|
+
GATE -->|no| P
|
|
42
|
+
GATE -->|yes| SW["<b>start-work</b><br/>execute one slice"]
|
|
43
|
+
|
|
44
|
+
subgraph LOOP["each slice: RED to GREEN to SURFACE to CLEAN"]
|
|
45
|
+
SW --> RED["failing test first"]
|
|
46
|
+
RED --> GREEN["smallest change that passes"]
|
|
47
|
+
GREEN --> SURF["exercise the <b>real surface</b><br/>not just the test"]
|
|
48
|
+
SURF --> CLEAN["tear down · cleanup receipt"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
CLEAN --> EV{"evidence complete?"}
|
|
52
|
+
EV -->|"tests only"| SW
|
|
53
|
+
EV -->|"artifact + receipt"| RW["<b>review-work</b><br/>scope · evidence · payload<br/>security · real surface"]
|
|
54
|
+
RW -->|findings| SW
|
|
55
|
+
RW -->|clean| HO["<b>lit-handoff</b><br/>resumable packet"]
|
|
56
|
+
|
|
57
|
+
style GATE fill:#fff3cd,stroke:#856404
|
|
58
|
+
style EV fill:#fff3cd,stroke:#856404
|
|
59
|
+
style SURF fill:#d4edda,stroke:#155724
|
|
60
|
+
style RW fill:#d1ecf1,stroke:#0c5460
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **실행 규율.** 모든 항목에 binary verification이 있어야 계획을 승인할 수 있습니다. 한 slice는 실제 surface에서 artifact를 만들고 QA 자원을 정리한 뒤에야 닫힙니다. 테스트만 통과한 상태는 완료가 아닙니다.
|
|
64
|
+
|
|
21
65
|
## 무엇을 제공하나요?
|
|
22
66
|
|
|
23
67
|
- `.grok/skills/<name>/SKILL.md`에 Grok-native skill 문서 34개
|
|
@@ -31,6 +75,27 @@ Grok에 로그인하거나 TUI를 띄우거나, live host 동작을 주장하지
|
|
|
31
75
|
|
|
32
76
|
Grok Build는 project, user, plugin, configured path에서 skill을 읽습니다. Project guidance는 AGENTS-family 파일과 `.grok/rules/*.md`이고 hook은 project 또는 user `.grok/hooks/`에서 읽습니다. LitGrok은 이 문서화된 direct surface만 설치합니다.
|
|
33
77
|
|
|
78
|
+
### Grok Build에 연결되는 구조
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart LR
|
|
82
|
+
subgraph GB["Grok Build"]
|
|
83
|
+
PJ["plugin.json"]; HK["hooks"]; RL["rules"]
|
|
84
|
+
end
|
|
85
|
+
subgraph LG["LitGrok plugin"]
|
|
86
|
+
PLAN["plan gate"]
|
|
87
|
+
HEDGE["deliverable hedge guard"]
|
|
88
|
+
LEDGER[("session ledger")]
|
|
89
|
+
end
|
|
90
|
+
PJ --> LG
|
|
91
|
+
HK --> PLAN --> LEDGER
|
|
92
|
+
HK --> HEDGE
|
|
93
|
+
RL --> LG
|
|
94
|
+
LG --> S["34 skills · 11 agents"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **Host 경계는 좁고 명확합니다.** `plugin.json`이 package를 노출하고, hook은 plan gate와 hedge guard로 연결되며, rule은 project guidance가 됩니다. session ledger에는 bounded state를 기록하고, 이 표면들이 34개 skill과 11개 agent를 Grok Build에서 사용할 수 있게 합니다.
|
|
98
|
+
|
|
34
99
|
## 설치
|
|
35
100
|
|
|
36
101
|
```bash
|
|
@@ -40,7 +105,7 @@ npx --yes litgrok-ai@latest install
|
|
|
40
105
|
재현 가능한 설치는 버전을 고정합니다.
|
|
41
106
|
|
|
42
107
|
```bash
|
|
43
|
-
npx --yes litgrok-ai@0.2.
|
|
108
|
+
npx --yes litgrok-ai@0.2.6 install
|
|
44
109
|
```
|
|
45
110
|
|
|
46
111
|
파일을 쓰지 않고 미리보기:
|
|
@@ -87,6 +152,24 @@ npx --yes litgrok-ai@latest uninstall --user
|
|
|
87
152
|
|
|
88
153
|
설치 후 project hook trust를 허용하고, 그 프로젝트에서 Grok Build를 열거나 재시작합니다. Skill과 rule은 guidance surface이고, hook command만 실행 가능한 payload이며 문서화된 event JSON을 stdin으로 받습니다.
|
|
89
154
|
|
|
155
|
+
### 새 머신에서도 payload가 작동하는 이유
|
|
156
|
+
|
|
157
|
+
```mermaid
|
|
158
|
+
flowchart LR
|
|
159
|
+
SK["a skill"] --> Q{"does it declare<br/>a capability?"}
|
|
160
|
+
Q -->|"self-contained<br/>procedure"| AL["explicit allowlist entry<br/>with a written reason"]
|
|
161
|
+
Q -->|"needs a corpus"| C["corpus must resolve<br/>inside the <b>packed payload</b>"]
|
|
162
|
+
AL --> G1
|
|
163
|
+
C --> G1["<b>payload-substance</b>"]
|
|
164
|
+
G1 --> G2["<b>cross-product parity</b><br/>one product cannot ship a stub<br/>where the family ships substance"]
|
|
165
|
+
G2 --> G3["<b>referenced-path resolution</b><br/>every path in a SKILL.md<br/>must exist in the tarball"]
|
|
166
|
+
G3 --> OK["installs and works<br/>on a machine that has<br/>nothing else"]
|
|
167
|
+
style C fill:#d4edda,stroke:#155724
|
|
168
|
+
style OK fill:#d4edda,stroke:#155724
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
> **Shipping의 기준은 tarball입니다.** 저장소 안에 파일이 있다는 것만으로는 package에 포함됐다고 할 수 없습니다. `npm pack`을 기준으로 allowlist/corpus, product 간 substance parity, `SKILL.md`가 가리키는 path까지 확인하므로 빠진 의존성은 사용자 환경에 도달하기 전에 드러납니다.
|
|
172
|
+
|
|
90
173
|
파일이 생겼는지 확인:
|
|
91
174
|
|
|
92
175
|
```bash
|
|
@@ -115,12 +198,12 @@ Review는 proposal만 만듭니다. `autoApply`는 항상 false이고 transcript
|
|
|
115
198
|
설치된 project에서는 다음 package CLI를 foreground command로 실행할 수 있습니다.
|
|
116
199
|
|
|
117
200
|
```bash
|
|
118
|
-
npx --yes litgrok-ai@0.2.
|
|
119
|
-
npx --yes litgrok-ai@0.2.
|
|
120
|
-
npx --yes litgrok-ai@0.2.
|
|
121
|
-
npx --yes litgrok-ai@0.2.
|
|
122
|
-
npx --yes litgrok-ai@0.2.
|
|
123
|
-
npx --yes litgrok-ai@0.2.
|
|
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
|
|
124
207
|
```
|
|
125
208
|
|
|
126
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
|
@@ -7,18 +7,57 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
|
|
8
8
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
9
|
const PAYLOAD_ROOT = join(PACKAGE_ROOT, '.grok');
|
|
10
|
-
const USAGE = 'Usage: litgrok [install|uninstall] [--user|--project] [--dry-run] [--no-color]\n litgrok skill-loop <review|propose|list|apply|reject|rollback|curator> ...\n';
|
|
10
|
+
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
|
+
const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
12
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
13
|
+
const CLEAR_LINE = '\r\u001b[2K';
|
|
14
|
+
|
|
15
|
+
// ANSI Shadow, compressed to 5 rows: the standard 6-row face with its duplicate
|
|
16
|
+
// middle row dropped, keeping the closing row. This matches the approved preview.
|
|
17
|
+
// This table and renderer are ported verbatim from
|
|
18
|
+
// .litclaude/evidence/litfamily-guard-parity-v4/ansi-shadow-authoritative.mjs.
|
|
19
|
+
const GLYPHS = {
|
|
20
|
+
a:[" █████╗ ","██╔══██╗","███████║","██╔══██║","╚═╝ ╚═╝"],
|
|
21
|
+
c:[" ██████╗","██╔════╝","██║ ","╚██████╗"," ╚═════╝"],
|
|
22
|
+
d:["██████╗ ","██╔══██╗","██║ ██║","██████╔╝","╚═════╝ "],
|
|
23
|
+
e:["███████╗","██╔════╝","█████╗ ","███████╗","╚══════╝"],
|
|
24
|
+
g:[" ██████╗ ","██╔════╝ ","██║ ███╗","╚██████╔╝"," ╚═════╝ "],
|
|
25
|
+
h:["██╗ ██╗","██║ ██║","███████║","██║ ██║","╚═╝ ╚═╝"],
|
|
26
|
+
i:["██╗","██║","██║","██║","╚═╝"],
|
|
27
|
+
k:["██╗ ██╗","██║ ██╔╝","█████╔╝ ","██║ ██╗","╚═╝ ╚═╝"],
|
|
28
|
+
l:["██╗ ","██║ ","██║ ","███████╗","╚══════╝"],
|
|
29
|
+
m:["███╗ ███╗","████╗ ████║","██╔████╔██║","██║ ╚═╝ ██║","╚═╝ ╚═╝"],
|
|
30
|
+
n:["███╗ ██╗","████╗ ██║","██╔██╗ ██║","██║ ╚████║","╚═╝ ╚═══╝"],
|
|
31
|
+
o:[" ██████╗ ","██╔═══██╗","██║ ██║","╚██████╔╝"," ╚═════╝ "],
|
|
32
|
+
p:["██████╗ ","██╔══██╗","██████╔╝","██║ ","╚═╝ "],
|
|
33
|
+
r:["██████╗ ","██╔══██╗","██████╔╝","██║ ██║","╚═╝ ╚═╝"],
|
|
34
|
+
s:["███████╗","██╔════╝","███████╗","███████║","╚══════╝"],
|
|
35
|
+
t:["████████╗","╚══██╔══╝"," ██║ "," ██║ "," ╚═╝ "],
|
|
36
|
+
u:["██╗ ██╗","██║ ██║","██║ ██║","╚██████╔╝"," ╚═════╝ "],
|
|
37
|
+
x:["██╗ ██╗","╚██╗██╔╝"," ╚███╔╝ ","██╔╝ ██╗","╚═╝ ╚═╝"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function renderAnsiShadow(word) {
|
|
41
|
+
const rows = ["", "", "", "", ""];
|
|
42
|
+
for (const ch of word) {
|
|
43
|
+
const g = GLYPHS[ch];
|
|
44
|
+
if (!g) throw new Error(`missing glyph for '${ch}' in '${word}'`);
|
|
45
|
+
for (let index = 0; index < 5; index += 1) rows[index] += g[index];
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
11
49
|
|
|
12
50
|
function parseInstall(args) {
|
|
13
51
|
const remaining = [...args];
|
|
14
52
|
let command = 'install';
|
|
15
53
|
if (['install', 'uninstall'].includes(remaining[0])) command = remaining.shift();
|
|
16
|
-
if (remaining.some((argument) => !['--user', '--project', '--dry-run', '--no-color'].includes(argument))) return null;
|
|
54
|
+
if (remaining.some((argument) => !['--user', '--project', '--dry-run', '--no-color', '--yes'].includes(argument))) return null;
|
|
17
55
|
if (remaining.includes('--user') && remaining.includes('--project')) return null;
|
|
18
56
|
return {
|
|
19
57
|
command,
|
|
20
58
|
dryRun: remaining.includes('--dry-run'),
|
|
21
59
|
noColor: remaining.includes('--no-color'),
|
|
60
|
+
yes: remaining.includes('--yes'),
|
|
22
61
|
scope: remaining.includes('--user') ? 'user' : 'project',
|
|
23
62
|
};
|
|
24
63
|
}
|
|
@@ -32,25 +71,168 @@ function packageVersion() {
|
|
|
32
71
|
}
|
|
33
72
|
}
|
|
34
73
|
|
|
74
|
+
function paint(value, code, enabled) {
|
|
75
|
+
return enabled ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function elapsedSince(start) {
|
|
79
|
+
return `${(Number(process.hrtime.bigint() - start) / 1e6).toFixed(1)}ms`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function scopeDescription(options, root) {
|
|
83
|
+
const alternateFlag = options.scope === 'project' ? '--user' : '--project';
|
|
84
|
+
const alternateScope = options.scope === 'project' ? 'user' : 'project';
|
|
85
|
+
return `${options.scope} scope → ${root} (use ${alternateFlag} for ${alternateScope} scope)`;
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
function printInstallFrame(stdout, options, env, fileCount, root) {
|
|
36
|
-
const colors = !options.noColor && !Object.hasOwn(env, 'NO_COLOR') && !env.CI && Boolean(stdout.isTTY);
|
|
37
|
-
const orange = colors ? '
|
|
38
|
-
const yellow = colors ? '
|
|
39
|
-
const reset = colors ? '
|
|
89
|
+
const colors = !options.noColor && !options.dryRun && !Object.hasOwn(env, 'NO_COLOR') && !env.CI && Boolean(stdout.isTTY);
|
|
90
|
+
const orange = colors ? '\u001b[38;2;255;90;31m' : '';
|
|
91
|
+
const yellow = colors ? '\u001b[38;2;250;204;21m' : '';
|
|
92
|
+
const reset = colors ? '\u001b[0m' : '';
|
|
40
93
|
const rule = ` ${'━'.repeat(46)}`;
|
|
94
|
+
const payloadLabel = Number.isInteger(fileCount) ? `${fileCount} files · byte-identical copies only` : 'packaged .grok tree · byte-identical copies only';
|
|
41
95
|
stdout.write(`${rule}\n`);
|
|
42
96
|
stdout.write('\n');
|
|
43
|
-
|
|
97
|
+
const wordmarkRows = [
|
|
98
|
+
...renderAnsiShadow('lit'),
|
|
99
|
+
...renderAnsiShadow('grok').map((row, index) => index === 0 ? `${row} v${packageVersion()}` : row),
|
|
100
|
+
];
|
|
101
|
+
stdout.write(`${wordmarkRows.map((row) => ` ${orange}${row}${reset}`).join('\n')}\n`);
|
|
44
102
|
stdout.write(' Grok Build skills, project rules, and hooks\n');
|
|
45
103
|
stdout.write('\n');
|
|
46
104
|
stdout.write(`${rule}\n`);
|
|
47
105
|
stdout.write(` ╭─ ${options.command.toUpperCase()}\n`);
|
|
48
|
-
stdout.write(` │ ${yellow}01${reset} · Scope ${options
|
|
49
|
-
stdout.write(` │ ${yellow}02${reset} · Payload ${
|
|
106
|
+
stdout.write(` │ ${yellow}01${reset} · Scope ${scopeDescription(options, root)}\n`);
|
|
107
|
+
stdout.write(` │ ${yellow}02${reset} · Payload ${payloadLabel}\n`);
|
|
50
108
|
stdout.write(' ╰─ whole packaged .grok tree, byte-identical copies only\n');
|
|
51
109
|
stdout.write(' ╭─ MODEL ROUTE\n');
|
|
52
110
|
stdout.write(' │ Model selection: host-owned\n');
|
|
53
111
|
stdout.write(' ╰─ Grok Build owns model and effort; the installer writes no model keys\n');
|
|
112
|
+
return colors;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function statusMark(status, colors) {
|
|
116
|
+
if (status === 'ok') return paint('✓', '38;2;80;200;80', colors);
|
|
117
|
+
if (status === 'failed') return paint('✗', '38;2;230;60;60', colors);
|
|
118
|
+
return colors ? paint('○', '38;2;148;163;184', true) : '[skip]';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class InstallProgress {
|
|
122
|
+
constructor(stdout, colors) {
|
|
123
|
+
this.stdout = stdout;
|
|
124
|
+
this.colors = colors;
|
|
125
|
+
this.timer = null;
|
|
126
|
+
this.frameIndex = 0;
|
|
127
|
+
this.active = false;
|
|
128
|
+
this.closed = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
open() {
|
|
132
|
+
this.stdout.write(' ╭─ INSTALL STEPS\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
start(label) {
|
|
136
|
+
this.clearTimer();
|
|
137
|
+
this.active = true;
|
|
138
|
+
this.frameIndex = 0;
|
|
139
|
+
this.label = label;
|
|
140
|
+
if (!this.colors) return;
|
|
141
|
+
this.render();
|
|
142
|
+
this.timer = setInterval(() => {
|
|
143
|
+
this.frameIndex = (this.frameIndex + 1) % BRAILLE_FRAMES.length;
|
|
144
|
+
this.render();
|
|
145
|
+
}, SPINNER_INTERVAL_MS);
|
|
146
|
+
if (typeof this.timer.unref === 'function') this.timer.unref();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
finish(status, label, detail, elapsed) {
|
|
150
|
+
this.clearTimer();
|
|
151
|
+
const suffix = detail ? ` · ${detail}` : '';
|
|
152
|
+
const line = ` │ ${statusMark(status, this.colors)} ${label}${suffix} · ${elapsed}`;
|
|
153
|
+
this.stdout.write(this.colors && this.active ? `${CLEAR_LINE}${line}\n` : `${line}\n`);
|
|
154
|
+
this.active = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
close() {
|
|
158
|
+
this.clearTimer();
|
|
159
|
+
if (this.closed) return;
|
|
160
|
+
this.stdout.write(' ╰─\n');
|
|
161
|
+
this.closed = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
render() {
|
|
165
|
+
const frame = paint(BRAILLE_FRAMES[this.frameIndex] ?? BRAILLE_FRAMES[0], '38;2;250;204;21', this.colors);
|
|
166
|
+
this.stdout.write(`${CLEAR_LINE} │ ${frame} ${this.label}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
clearTimer() {
|
|
170
|
+
if (this.timer === null) return;
|
|
171
|
+
clearInterval(this.timer);
|
|
172
|
+
this.timer = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runStep(progress, label, operation) {
|
|
177
|
+
const started = process.hrtime.bigint();
|
|
178
|
+
progress.start(label);
|
|
179
|
+
try {
|
|
180
|
+
const result = operation();
|
|
181
|
+
progress.finish('ok', label, result.detail ?? '', elapsedSince(started));
|
|
182
|
+
return result.value;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
progress.finish('failed', label, error instanceof Error ? error.message : String(error), elapsedSince(started));
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function skipStep(progress, label, detail) {
|
|
190
|
+
const started = process.hrtime.bigint();
|
|
191
|
+
progress.finish('skipped', label, detail, elapsedSince(started));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function receiptCard(options, root, colors, { status, written, current }) {
|
|
195
|
+
const statusColor = status === 'Ready' || status === 'Already current' || status === 'Removed' ? '38;2;80;200;80' : '38;2;230;60;60';
|
|
196
|
+
const titleColor = status === 'Failed' ? '38;2;230;60;60' : '38;2;255;90;31';
|
|
197
|
+
return [
|
|
198
|
+
'',
|
|
199
|
+
` ${paint('╭─ INSTALL RECEIPT', titleColor, colors)}`,
|
|
200
|
+
` │ Status: ${paint(status, statusColor, colors)}`,
|
|
201
|
+
` │ Scope: ${scopeDescription(options, root)}`,
|
|
202
|
+
` │ Written vs already-current: ${written} written · ${current} already-current`,
|
|
203
|
+
' │ Model route: host-owned; the installer writes no model keys',
|
|
204
|
+
' │ Model picker: host limit — Grok Build owns session selection; no model keys are written',
|
|
205
|
+
' │ Output styles: host limit — Grok Build exposes no output-style selector; the installer writes no style keys',
|
|
206
|
+
' │ Hooks: require trust — /hooks-trust or launch Grok Build with --trust',
|
|
207
|
+
' │ Next: restart Grok Build',
|
|
208
|
+
` ${paint('╰─', titleColor, colors)}`,
|
|
209
|
+
'',
|
|
210
|
+
].join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function failureCard(options, root, colors, step, message) {
|
|
214
|
+
return [
|
|
215
|
+
'',
|
|
216
|
+
` ${paint('╭─ INSTALL FAILED', '38;2;230;60;60', colors)}`,
|
|
217
|
+
` │ Status: ${paint('Failed', '38;2;230;60;60', colors)}`,
|
|
218
|
+
` │ Scope: ${scopeDescription(options, root)}`,
|
|
219
|
+
` │ Step: ${step}`,
|
|
220
|
+
` │ Reason: ${message}`,
|
|
221
|
+
' │ Hooks: require trust — /hooks-trust or launch Grok Build with --trust',
|
|
222
|
+
' │ Next: resolve the issue and rerun the install',
|
|
223
|
+
` ${paint('╰─', '38;2;230;60;60', colors)}`,
|
|
224
|
+
'',
|
|
225
|
+
].join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function errorText(error) {
|
|
229
|
+
return error instanceof Error ? error.message : String(error);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reportFailure(error, { stderr, stdout, options, root, colors, step }) {
|
|
233
|
+
const lines = Array.isArray(error?.stderrLines) ? error.stderrLines : [`litgrok: ${errorText(error)}`];
|
|
234
|
+
for (const line of lines) stderr.write(`${line}\n`);
|
|
235
|
+
stdout.write(failureCard(options, root, colors, error?.step ?? step, errorText(error)));
|
|
54
236
|
}
|
|
55
237
|
|
|
56
238
|
function targetState(source, target) {
|
|
@@ -133,19 +315,40 @@ export async function run(args, context = {}) {
|
|
|
133
315
|
|
|
134
316
|
const home = env.HOME || env.USERPROFILE || homedir();
|
|
135
317
|
const root = join(options.scope === 'project' ? cwd : home, '.grok');
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
318
|
+
const displayRoot = resolve(root);
|
|
319
|
+
const payloadStarted = process.hrtime.bigint();
|
|
320
|
+
let discoveredPayload;
|
|
321
|
+
let payloadError;
|
|
322
|
+
try {
|
|
323
|
+
discoveredPayload = discoverPayloadFiles();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
payloadError = error;
|
|
326
|
+
}
|
|
143
327
|
|
|
144
|
-
function
|
|
145
|
-
|
|
328
|
+
function filesFromPayload(discovered) {
|
|
329
|
+
return discovered.map(({ relativePath, source }) => ({
|
|
330
|
+
relativePath,
|
|
331
|
+
source,
|
|
332
|
+
target: join(root, relativePath),
|
|
333
|
+
}));
|
|
146
334
|
}
|
|
147
335
|
|
|
148
|
-
|
|
336
|
+
function projectHooksIncluded(files) {
|
|
337
|
+
return options.command === 'install' && options.scope === 'project' && files.some(({ relativePath }) => relativePath.split(sep)[0] === 'hooks');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function printHookTrustRequirement(files) {
|
|
341
|
+
if (projectHooksIncluded(files)) stdout.write('Project hooks require trust before they run: use /hooks-trust or launch Grok Build with --trust.\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const files = discoveredPayload ? filesFromPayload(discoveredPayload) : null;
|
|
345
|
+
const payloadElapsed = payloadError ? null : elapsedSince(payloadStarted);
|
|
346
|
+
const colors = printInstallFrame(stdout, options, env, files?.length, displayRoot);
|
|
347
|
+
|
|
348
|
+
if (payloadError) {
|
|
349
|
+
reportFailure(payloadError, { stderr, stdout, options, root: displayRoot, colors, step: 'PAYLOAD' });
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
149
352
|
|
|
150
353
|
const reasons = [];
|
|
151
354
|
if (options.dryRun) reasons.push('DRY RUN');
|
|
@@ -155,52 +358,104 @@ export async function run(args, context = {}) {
|
|
|
155
358
|
if (reasons.length > 0) {
|
|
156
359
|
stdout.write(`${reasons.join(' / ')} — no files written\n`);
|
|
157
360
|
for (const { target } of files) stdout.write(`would ${options.command} ${target}\n`);
|
|
158
|
-
printHookTrustRequirement();
|
|
361
|
+
printHookTrustRequirement(files);
|
|
159
362
|
return 0;
|
|
160
363
|
}
|
|
161
364
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
365
|
+
const progress = new InstallProgress(stdout, colors);
|
|
366
|
+
progress.open();
|
|
367
|
+
let step = 'PAYLOAD';
|
|
368
|
+
try {
|
|
369
|
+
progress.finish('ok', 'PAYLOAD', `${files.length} files discovered`, payloadElapsed);
|
|
370
|
+
|
|
371
|
+
step = 'SAFETY';
|
|
372
|
+
runStep(progress, 'SAFETY', () => {
|
|
373
|
+
const unsafe = unsafeInstallAncestor(root, files);
|
|
374
|
+
if (unsafe) {
|
|
375
|
+
const message = `Refusing ${unsafe.reason} in install path: ${unsafe.path}`;
|
|
376
|
+
const error = new Error(message);
|
|
377
|
+
error.step = 'SAFETY';
|
|
378
|
+
error.stderrLines = [message];
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
return { value: null, detail: 'install path clear' };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
step = 'OWNERSHIP';
|
|
385
|
+
const states = runStep(progress, 'OWNERSHIP', () => {
|
|
386
|
+
const owned = files.map(({ source, target }) => targetState(source, target));
|
|
387
|
+
const counts = {
|
|
388
|
+
missing: owned.filter((state) => state === 'missing').length,
|
|
389
|
+
current: owned.filter((state) => state === 'current').length,
|
|
390
|
+
conflicts: owned.filter((state) => state === 'conflict').length,
|
|
391
|
+
};
|
|
392
|
+
if (counts.conflicts > 0) {
|
|
393
|
+
const stderrLines = [];
|
|
394
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
395
|
+
if (owned[index] === 'conflict') stderrLines.push(`Refusing to ${options.command === 'install' ? 'overwrite' : 'remove'} different existing file: ${files[index].target}`);
|
|
396
|
+
}
|
|
397
|
+
const error = new Error(`${counts.missing} missing · ${counts.current} already-current · ${counts.conflicts} conflicts`);
|
|
398
|
+
error.step = 'OWNERSHIP';
|
|
399
|
+
error.stderrLines = stderrLines;
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
value: { states: owned, counts },
|
|
404
|
+
detail: `${counts.missing} missing · ${counts.current} already-current · ${counts.conflicts} conflicts`,
|
|
405
|
+
};
|
|
406
|
+
});
|
|
167
407
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
408
|
+
step = 'WRITE';
|
|
409
|
+
if (options.command === 'uninstall') {
|
|
410
|
+
const writeStarted = process.hrtime.bigint();
|
|
411
|
+
progress.start('WRITE');
|
|
412
|
+
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;
|
|
417
|
+
}
|
|
418
|
+
progress.finish('ok', 'WRITE', `${removed} removed · ${states.counts.missing} already absent`, elapsedSince(writeStarted));
|
|
419
|
+
const receipt = receiptCard(options, displayRoot, colors, { status: 'Removed', written: 0, current: states.counts.current });
|
|
420
|
+
progress.close();
|
|
421
|
+
stdout.write(receipt);
|
|
422
|
+
return 0;
|
|
172
423
|
}
|
|
173
|
-
return 1;
|
|
174
|
-
}
|
|
175
424
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
425
|
+
if (states.states.every((state) => state === 'current')) {
|
|
426
|
+
skipStep(progress, 'WRITE', `SKIPPED · 0 written · ${states.counts.current} already-current`);
|
|
427
|
+
const receipt = receiptCard(options, displayRoot, colors, { status: 'Already current', written: 0, current: states.counts.current });
|
|
428
|
+
progress.close();
|
|
429
|
+
stdout.write(receipt);
|
|
430
|
+
return 0;
|
|
182
431
|
}
|
|
183
|
-
stdout.write(removed === 0 ? `Already absent: ${files.length} files\n` : `Removed ${removed} files\n`);
|
|
184
|
-
return 0;
|
|
185
|
-
}
|
|
186
432
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
433
|
+
const writeStarted = process.hrtime.bigint();
|
|
434
|
+
progress.start('WRITE');
|
|
435
|
+
let installed = 0;
|
|
436
|
+
try {
|
|
437
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
438
|
+
if (states.states[index] === 'current') continue;
|
|
439
|
+
const { source, target } = files[index];
|
|
440
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
441
|
+
copyFileSync(source, target, constants.COPYFILE_EXCL);
|
|
442
|
+
installed += 1;
|
|
443
|
+
}
|
|
444
|
+
progress.finish('ok', 'WRITE', `${installed} written · ${states.counts.current} already-current`, elapsedSince(writeStarted));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
progress.finish('failed', 'WRITE', errorText(error), elapsedSince(writeStarted));
|
|
447
|
+
if (error && typeof error === 'object') error.step = 'WRITE';
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
const receipt = receiptCard(options, displayRoot, colors, { status: 'Ready', written: installed, current: states.counts.current });
|
|
451
|
+
progress.close();
|
|
452
|
+
stdout.write(receipt);
|
|
190
453
|
return 0;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
progress.close();
|
|
456
|
+
reportFailure(error, { stderr, stdout, options, root: displayRoot, colors, step });
|
|
457
|
+
return 1;
|
|
191
458
|
}
|
|
192
|
-
|
|
193
|
-
let installed = 0;
|
|
194
|
-
for (let index = 0; index < files.length; index += 1) {
|
|
195
|
-
if (states[index] === 'current') continue;
|
|
196
|
-
const { source, target } = files[index];
|
|
197
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
198
|
-
copyFileSync(source, target, constants.COPYFILE_EXCL);
|
|
199
|
-
installed += 1;
|
|
200
|
-
}
|
|
201
|
-
stdout.write(`Installed ${installed} files\n`);
|
|
202
|
-
printHookTrustRequirement();
|
|
203
|
-
return 0;
|
|
204
459
|
}
|
|
205
460
|
|
|
206
461
|
if (process.argv[1] && realpathSync(resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litgrok-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Grok Build skills, project rules, and hooks installer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"litgrok": "bin/litgrok.mjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test test/*.test.mjs"
|
|
11
|
+
"test": "node --test test/*.test.mjs",
|
|
12
|
+
"check:version": "node tools/check-version-lockstep.mjs"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"plugin.json",
|