@litfamily/lithermes 1.0.32 → 1.0.33

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.
Files changed (50) hide show
  1. package/README.md +7 -7
  2. package/README_Ko-KR.md +7 -7
  3. package/assets/lithermes-plugin/README.md +0 -12
  4. package/assets/lithermes-plugin/adhd_stack.py +43 -1
  5. package/assets/lithermes-plugin/auto_handoff.py +13 -2
  6. package/assets/lithermes-plugin/bounded_work.py +33 -3
  7. package/assets/lithermes-plugin/core.py +59 -18
  8. package/assets/lithermes-plugin/core_review.py +2 -2
  9. package/assets/lithermes-plugin/lit_mark.py +2 -7
  10. package/assets/lithermes-plugin/payload-version.json +43 -47
  11. package/assets/lithermes-plugin/plugin.yaml +1 -1
  12. package/assets/lithermes-plugin/rules/bundled-rules/baseline-discipline.md +7 -13
  13. package/assets/lithermes-plugin/rules/globmatch.py +1 -1
  14. package/assets/lithermes-plugin/skills/debugging/SKILL.md +1 -1
  15. package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +1 -1
  16. package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +2 -2
  17. package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +1 -1
  18. package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +1 -1
  19. package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +4 -4
  20. package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +1 -1
  21. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nuxtjs.csv +1 -1
  22. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/manifest.json +2 -2
  23. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
  24. package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +10 -10
  25. package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +12 -7
  26. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +81 -27
  27. package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +34 -6
  28. package/assets/lithermes-plugin/skills/lit-burnoff/SKILL.md +1 -1
  29. package/assets/lithermes-plugin/skills/lit-code/SKILL.md +4 -4
  30. package/assets/lithermes-plugin/skills/lit-code/references/rust/README.md +2 -2
  31. package/assets/lithermes-plugin/skills/lit-code/references/rust/async-tokio.md +4 -4
  32. package/assets/lithermes-plugin/skills/lit-code/references/rust/axum-stack.md +1 -1
  33. package/assets/lithermes-plugin/skills/lit-code/references/rust/clap-stack.md +1 -1
  34. package/assets/lithermes-plugin/skills/lit-code/references/rust/libraries.md +3 -3
  35. package/assets/lithermes-plugin/skills/lit-code/references/rust/proptest-insta.md +10 -10
  36. package/assets/lithermes-plugin/skills/lit-code/references/rust/zero-cost-safety.md +1 -1
  37. package/assets/lithermes-plugin/skills/lit-code/references/typescript/README.md +2 -2
  38. package/assets/lithermes-plugin/skills/lit-code/references/typescript/backend-hono.md +1 -1
  39. package/assets/lithermes-plugin/skills/lit-code/references/typescript/tsconfig-strict.md +1 -1
  40. package/assets/lithermes-plugin/skills/lit-code/scripts/typescript/check-no-excuse-rules.ts +2 -2
  41. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  42. package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package.json +2 -1
  43. package/assets/lithermes-plugin/skills/rules/SKILL.md +1 -1
  44. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +2 -2
  45. package/assets/{lithermes-plugin → npm-channel}/auto_update.py +43 -9
  46. package/package.json +1 -1
  47. package/readme-assets/badge-version.svg +1 -1
  48. package/src/lib/canonicalCorpus.js +1 -1
  49. package/src/lib/install.js +22 -0
  50. package/src/lib/skillPayload.js +10 -5
@@ -3,5 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {"render": "remotion render", "still": "remotion still"},
6
- "dependencies": {"@remotion/cli":"4.0.526", "remotion":"4.0.526", "react":"19.3.0", "react-dom":"19.3.0"}
6
+ "dependencies": {"@remotion/cli":"4.0.526", "remotion":"4.0.526", "react":"19.3.0", "react-dom":"19.3.0"},
7
+ "overrides": {"zod": "4.6.6"}
7
8
  }
@@ -169,7 +169,7 @@ priority rules.
169
169
  `paths` and Copilot `applyTo` aliases. A Cursor-style comma-separated scalar
170
170
  (`globs: *.ts,*.tsx`) is split into separate patterns.
171
171
  - **Glob support is a documented SUBSET**, not picomatch. `**`, `*`, `?`,
172
- `[seq]`, `{a,b}` and a leading `!` work. Extglob (`@(a|b)`) and POSIX classes
172
+ `[seq]`, `{a,b}` and a leading (!) work. Extglob (`@(a|b)`) and POSIX classes
173
173
  (`[[:alpha:]]`) do NOT — a rule using them silently matches nothing. The full
174
174
  divergence list lives in `rules/globmatch.py` `DIVERGENCES`; read it before
175
175
  claiming a glob "should have" matched.
@@ -485,7 +485,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
485
485
 
486
486
  ```text
487
487
  $ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
488
- Installed LitHermes 1.0.32
488
+ Installed LitHermes 1.0.33
489
489
  plugin: $ISOLATED/plugins/lithermes
490
490
  model config: updated
491
491
 
@@ -502,7 +502,7 @@ installed payload: PASS
502
502
  enabled config: PASS
503
503
 
504
504
  $ hermes lithermes doctor
505
- [OK] plugin.yaml readable (version 1.0.32)
505
+ [OK] plugin.yaml readable (version 1.0.33)
506
506
  [OK] skills bundled: 40
507
507
  [OK] litgoal durable runtime importable
508
508
  ```
@@ -42,6 +42,8 @@ INSTALL_TIMEOUT_SECONDS = 90.0
42
42
  MAX_RESPONSE_BYTES = 64 * 1024
43
43
  AUTO_UPDATE_GUARD_ENV = "LITHERMES_AUTO_UPDATE_RUNNING"
44
44
  INSTALLER_DISTRIBUTION = "npm"
45
+ NPM_CHANNEL_MARKER_NAME = ".lithermes-npm-channel"
46
+ NPM_CHANNEL_MARKER_TEXT = "lithermes npm channel\n"
45
47
  ATTEMPT_RECORD_NAME = "auto-update-attempt.json"
46
48
  # A broken npm fetch blocked each new session's first reply for about 70 s, so
47
49
  # after a failed install the bridge waits a day before it tries the same
@@ -244,19 +246,51 @@ def _read_install_receipt(home: Path) -> dict[str, Any] | None:
244
246
  return _read_state_json(home, "auto-update-receipt.json")
245
247
 
246
248
 
249
+ def _plugin_dir(home: Path, manifest: dict[str, Any] | None) -> Path:
250
+ """Prefer the receipt's plugin path when it is a real directory, not a link."""
251
+
252
+ raw = manifest.get("pluginPath") if isinstance(manifest, dict) else None
253
+ if isinstance(raw, str) and raw.strip():
254
+ candidate = Path(raw).expanduser()
255
+ try:
256
+ info = candidate.lstat()
257
+ except OSError:
258
+ info = None
259
+ if info is not None and stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode):
260
+ return candidate
261
+ return home / "plugins" / "lithermes"
262
+
263
+
264
+ def _channel_marker_matches(root: Path) -> bool:
265
+ marker = root / NPM_CHANNEL_MARKER_NAME
266
+ expected = NPM_CHANNEL_MARKER_TEXT.encode("utf-8")
267
+ try:
268
+ info = marker.lstat()
269
+ if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode):
270
+ return False
271
+ fd = os.open(marker, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
272
+ try:
273
+ data = os.read(fd, len(expected) + 1)
274
+ finally:
275
+ os.close(fd)
276
+ except OSError:
277
+ return False
278
+ return data == expected
279
+
280
+
247
281
  def _is_npm_channel(home: Path) -> bool:
248
- """Return true only for a manifest written by the npm installer.
249
-
250
- Hermes catalog/git installs copy the plugin tree directly and do not create the
251
- installer's ownership receipt. The explicit distribution marker makes a stale
252
- receipt fail closed too: a catalog payload must never use it as permission to
253
- fetch and replace itself. Older receipts without the marker are intentionally
254
- treated as non-npm until the user runs the installer once, which preserves the
255
- catalog safety boundary over an ambiguous channel.
282
+ """Return true only for an npm receipt that also carries the channel marker.
283
+
284
+ The receipt lives outside the plugin directory and survives a catalog replace,
285
+ so ``distribution: npm`` alone is not permission to fetch and replace the
286
+ tree. The marker is written only by the npm installer, into the plugin
287
+ directory, and a catalog copy never contains it. A symlink is not the marker.
256
288
  """
257
289
 
258
290
  manifest = _read_state_json(home, "install-manifest.json")
259
- return isinstance(manifest, dict) and manifest.get("distribution") == INSTALLER_DISTRIBUTION
291
+ if not (isinstance(manifest, dict) and manifest.get("distribution") == INSTALLER_DISTRIBUTION):
292
+ return False
293
+ return _channel_marker_matches(_plugin_dir(home, manifest))
260
294
 
261
295
 
262
296
  def _utc_stamp(seconds: float | None = None) -> str:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/lithermes",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "description": "Hermes Agent plugin that plans, builds, checks and hands off your work when you add one word, lit, to a request.",
5
5
  "keywords": [
6
6
  "hermes-agent",
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.32"><title>release: 1.0.32</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.32</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.33"><title>release: 1.0.33</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.33</text></g></svg>
@@ -14,7 +14,7 @@ const EXPECTED = Object.freeze({
14
14
  tree: "9188410be0af35f2421ba300d91a0d7a7341caf0",
15
15
  fileCount: 167,
16
16
  totalBytes: 2596360,
17
- digest: "b0d1a085de8856e7edeba24127d26875fe5473b80c0d95f6578addfdc801e445",
17
+ digest: "c921ed87f41b5aaef14ef480c791f1c36bbd3ec616eaccc32f5e1246fcc547e6",
18
18
  directories: ["design", "designpowers", "perfection", "ui-ux-db"],
19
19
  attributes: "corpus/** -text\nlegal/** -text\n",
20
20
  legal: [
@@ -32,6 +32,9 @@ const { installSkins } = require("./skins");
32
32
 
33
33
  const packageRoot = path.resolve(__dirname, "..", "..");
34
34
  const assetRoot = path.join(packageRoot, "assets", "lithermes-plugin");
35
+ const channelRoot = path.join(packageRoot, "assets", "npm-channel");
36
+ const NPM_CHANNEL_MARKER_NAME = ".lithermes-npm-channel";
37
+ const NPM_CHANNEL_MARKER_TEXT = "lithermes npm channel\n";
35
38
  const LEGACY_VENDOR_DIRS = new Map([
36
39
  ["vendor/022_handoff", "vendor/handoff"],
37
40
  ["vendor/045_scientific-visualization", "vendor/scientific-visualization"],
@@ -547,6 +550,24 @@ function previewModelRoute(flags = {}) {
547
550
  };
548
551
  }
549
552
 
553
+ function stageNpmChannel(dest) {
554
+ const source = path.join(channelRoot, "auto_update.py");
555
+ const sourceStat = fs.lstatSync(source);
556
+ if (!sourceStat.isFile()) {
557
+ throw new LitHermesError("npm channel updater is missing.", 5);
558
+ }
559
+ const target = path.join(dest, "auto_update.py");
560
+ fs.copyFileSync(source, target);
561
+ fs.chmodSync(target, 0o644);
562
+ const marker = path.join(dest, NPM_CHANNEL_MARKER_NAME);
563
+ fs.writeFileSync(marker, NPM_CHANNEL_MARKER_TEXT, { encoding: "utf8", mode: 0o644 });
564
+ fs.chmodSync(marker, 0o644);
565
+ return [
566
+ { path: "auto_update.py", sha256: sha256(target) },
567
+ { path: NPM_CHANNEL_MARKER_NAME, sha256: sha256(marker) },
568
+ ];
569
+ }
570
+
550
571
  function installLitHermes(flags = {}) {
551
572
  const dryRun = Boolean(flags["dry-run"]);
552
573
  const homeInfo = dryRun
@@ -655,6 +676,7 @@ function installLitHermes(flags = {}) {
655
676
  assertPluginParent(hermesHome);
656
677
  fs.rmSync(dest, { recursive: true, force: true });
657
678
  const files = copyTree(assetRoot, dest);
679
+ files.push(...stageNpmChannel(dest));
658
680
  onProgress("Writing Hermes config");
659
681
  checkState();
660
682
  let afterConfig = beforeConfig;
@@ -221,7 +221,7 @@ function inspectSkillPayload(pluginPath, manifest, verifySourceHash = false, opt
221
221
  const failure = checkEntry(pluginPath, entryPath, expectedHash);
222
222
  if (failure) failures.push(failure);
223
223
  }
224
- const allowedUnlisted = new Set(["payload-version.json"]);
224
+ const allowedUnlisted = new Set(["payload-version.json", "auto_update.py", ".lithermes-npm-channel"]);
225
225
  const allowedDirectories = new Set();
226
226
  for (const entryPath of inspected.entries.keys()) {
227
227
  const segments = entryPath.split("/");
@@ -237,20 +237,25 @@ function inspectSkillPayload(pluginPath, manifest, verifySourceHash = false, opt
237
237
  const directory = path.posix.dirname(entryPath);
238
238
  hostCacheDirectories.add(directory === "." ? "__pycache__/" : `${directory}/__pycache__/`);
239
239
  }
240
- const isHostBytecode = (entryPath) => {
241
- const source = hostBytecodeSource(entryPath);
242
- if (!source || !inspected.entries.has(source)) return false;
240
+ const isRegularFile = (entryPath) => {
243
241
  try {
244
242
  return fs.lstatSync(path.join(pluginPath, ...entryPath.split("/"))).isFile();
245
243
  } catch {
246
244
  return false;
247
245
  }
248
246
  };
247
+ const isHostBytecode = (entryPath) => {
248
+ const source = hostBytecodeSource(entryPath);
249
+ if (!source || !(inspected.entries.has(source) || source === "auto_update.py")) return false;
250
+ return isRegularFile(entryPath);
251
+ };
252
+ const isNpmChannelFile = (entryPath) =>
253
+ (entryPath === "auto_update.py" || entryPath === ".lithermes-npm-channel") && isRegularFile(entryPath);
249
254
  const traversal = listPayloadEntries(pluginPath, options.readDirectory);
250
255
  for (const entryPath of traversal.entries) {
251
256
  if (
252
257
  inspected.entries.has(entryPath)
253
- || allowedUnlisted.has(entryPath)
258
+ || (allowedUnlisted.has(entryPath) && (entryPath === "payload-version.json" || isNpmChannelFile(entryPath)))
254
259
  || allowedDirectories.has(entryPath)
255
260
  || hostCacheDirectories.has(entryPath)
256
261
  || isHostBytecode(entryPath)