@wipcomputer/wip-ldm-os 0.4.76 → 0.4.78
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/SKILL.md +1 -1
- package/lib/deploy.mjs +61 -11
- package/package.json +1 -1
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +57 -0
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ license: MIT
|
|
|
9
9
|
compatibility: Requires git, npm, node. Node.js 18+.
|
|
10
10
|
metadata:
|
|
11
11
|
display-name: "LDM OS"
|
|
12
|
-
version: "0.4.
|
|
12
|
+
version: "0.4.78"
|
|
13
13
|
homepage: "https://github.com/wipcomputer/wip-ldm-os"
|
|
14
14
|
author: "Parker Todd Brooks"
|
|
15
15
|
category: infrastructure
|
package/lib/deploy.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
lstatSync, readlinkSync, unlinkSync, chmodSync, readdirSync,
|
|
15
15
|
renameSync, rmSync, statSync, symlinkSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
17
18
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
18
19
|
import { tmpdir } from 'node:os';
|
|
19
20
|
import { detectInterfaces, describeInterfaces, detectToolbox } from './detect.mjs';
|
|
@@ -750,6 +751,45 @@ function installCLI(repoPath, door) {
|
|
|
750
751
|
}
|
|
751
752
|
}
|
|
752
753
|
|
|
754
|
+
// Stable content hash over a directory tree. Used by deployExtension to
|
|
755
|
+
// decide whether to skip a redeploy. Before this, deployExtension skipped
|
|
756
|
+
// when source and deployed had equal versions ... but a prior partial
|
|
757
|
+
// install could have bumped the package.json without finishing the copy,
|
|
758
|
+
// leaving deployed package.json "current" while other files (guard.mjs,
|
|
759
|
+
// core.mjs, etc.) were stale. The 2026-04-20 wip-release 1.9.74 -> 1.9.75
|
|
760
|
+
// rollout hit that (source core.mjs had runNpmPublish, deployed didn't,
|
|
761
|
+
// both package.jsons reported 1.9.75). Skip only when versions match AND
|
|
762
|
+
// content hashes match; otherwise redeploy to heal the drift.
|
|
763
|
+
function computeTreeHash(dir) {
|
|
764
|
+
if (!existsSync(dir)) return null;
|
|
765
|
+
const skipNames = new Set([
|
|
766
|
+
'.git', 'node_modules', 'ai', '_trash', '.worktrees', 'logs', 'test', 'tests', '__tests__',
|
|
767
|
+
]);
|
|
768
|
+
const hash = createHash('sha256');
|
|
769
|
+
function walk(d, rel) {
|
|
770
|
+
let entries;
|
|
771
|
+
try { entries = readdirSync(d, { withFileTypes: true }); }
|
|
772
|
+
catch { return; }
|
|
773
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
774
|
+
for (const e of entries) {
|
|
775
|
+
if (skipNames.has(e.name)) continue;
|
|
776
|
+
const p = join(d, e.name);
|
|
777
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
778
|
+
if (e.isDirectory()) walk(p, r);
|
|
779
|
+
else if (e.isFile()) {
|
|
780
|
+
try {
|
|
781
|
+
hash.update(r);
|
|
782
|
+
hash.update('\0');
|
|
783
|
+
hash.update(readFileSync(p));
|
|
784
|
+
hash.update('\0');
|
|
785
|
+
} catch {}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
walk(dir, '');
|
|
790
|
+
return hash.digest('hex');
|
|
791
|
+
}
|
|
792
|
+
|
|
753
793
|
function deployExtension(repoPath, name) {
|
|
754
794
|
const sourcePkg = readJSON(join(repoPath, 'package.json'));
|
|
755
795
|
const ldmDest = join(LDM_EXTENSIONS, name);
|
|
@@ -759,18 +799,28 @@ function deployExtension(repoPath, name) {
|
|
|
759
799
|
|
|
760
800
|
const cmp = compareSemver(newVersion, currentVersion);
|
|
761
801
|
if (newVersion && currentVersion && cmp <= 0) {
|
|
762
|
-
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
802
|
+
// Versions equal or deployed is newer. Verify content hash before
|
|
803
|
+
// short-circuiting ... a prior partial install could have bumped
|
|
804
|
+
// package.json but not copied the other files, leaving deployed
|
|
805
|
+
// apparently "current" while code is stale.
|
|
806
|
+
const srcHash = computeTreeHash(repoPath);
|
|
807
|
+
const dstHash = computeTreeHash(ldmDest);
|
|
808
|
+
if (srcHash && dstHash && srcHash === dstHash) {
|
|
809
|
+
skip(`LDM: ${name} already at v${currentVersion}${cmp < 0 ? ` (source is older: v${newVersion})` : ''}`);
|
|
810
|
+
// Ensure OC copy exists too
|
|
811
|
+
const ocName = resolveOcPluginName(repoPath, name);
|
|
812
|
+
const ocDest = join(OC_EXTENSIONS, ocName);
|
|
813
|
+
if (!existsSync(ocDest) && !DRY_RUN) {
|
|
814
|
+
mkdirSync(ocDest, { recursive: true });
|
|
815
|
+
cpSync(ldmDest, ocDest, { recursive: true });
|
|
816
|
+
ok(`OpenClaw: synced to ${ocDest}`);
|
|
817
|
+
} else {
|
|
818
|
+
skip(`OpenClaw: ${ocName} already at v${currentVersion}`);
|
|
819
|
+
}
|
|
820
|
+
return true;
|
|
772
821
|
}
|
|
773
|
-
|
|
822
|
+
// Content differs despite matching version; fall through to redeploy.
|
|
823
|
+
ok(`LDM: ${name} v${currentVersion} reports same version but content differs; redeploying`);
|
|
774
824
|
}
|
|
775
825
|
|
|
776
826
|
if (DRY_RUN) {
|
package/package.json
CHANGED
|
@@ -265,6 +265,63 @@ openclaw gateway restart
|
|
|
265
265
|
|
|
266
266
|
Enforced on all 64 repos on 2026-02-20, re-audited 2026-02-27 (18 repos had drifted or were new). No force pushes to main. No direct pushes. No exceptions. The `lesaai` account is not exempt.
|
|
267
267
|
|
|
268
|
+
## Branch Guard: Runtime Enforcement (wip-branch-guard v1.9.80+)
|
|
269
|
+
|
|
270
|
+
The branch guard runs as a PreToolUse hook on every Claude Code tool call and as an OpenClaw plugin for Lēsa. Same rules, same deny messages on both sides. Enforces three layers on top of the standard branch/worktree check.
|
|
271
|
+
|
|
272
|
+
### Layer 1 ... write gate
|
|
273
|
+
|
|
274
|
+
- On main branch: Write/Edit/NotebookEdit denied. Bash writes denied.
|
|
275
|
+
- On a feature branch NOT in a worktree: writes denied.
|
|
276
|
+
- On a feature branch in a worktree: writes allowed (baseline workflow).
|
|
277
|
+
- Shared-state paths (`~/.claude/plans/`, `workspace/*`, `~/.ldm/shared/`, `~/.openclaw/workspace/`, etc.) always allowed regardless of branch.
|
|
278
|
+
|
|
279
|
+
### Layer 2 ... destructive-command block
|
|
280
|
+
|
|
281
|
+
Always denied, any branch: `git clean -f`, `git checkout -- <path>`, `git checkout .`, `git stash drop/pop/clear`, `git reset --hard`, `git restore <path>` (`--staged` is safe). Plus code-execution bypass patterns (`python -c "open().write()"`, `node -e "writeFile()"`).
|
|
282
|
+
|
|
283
|
+
Denied on any branch: `--no-verify`, `git push --force` (use `--force-with-lease`).
|
|
284
|
+
|
|
285
|
+
### Layer 3 ... session-level gates (new 2026-04-20)
|
|
286
|
+
|
|
287
|
+
1. **Onboarding-before-first-write.** Before the first Write/Edit to a repo in a session, the guard requires you to have Read (via the Read tool) `README.md`, `CLAUDE.md`, and any `*RUNBOOK*.md`/`*LANDMINES*.md`/`WORKFLOW*.md` at repo root. State auto-populates from Read calls. 2-hour TTL once onboarded. Override: `LDM_GUARD_SKIP_ONBOARDING=<repo>` or `=1`.
|
|
288
|
+
|
|
289
|
+
2. **Recently-blocked-file tracking.** If the guard denies a file via Write/Edit, retrying via a different tool on the same path re-denies with "equivalent-action bypass" context. Last 20 denials in a 1-hour rolling window. Override: `LDM_GUARD_ACK_BLOCKED_FILE=<path>`.
|
|
290
|
+
|
|
291
|
+
3. **External-PR create guard.** `gh pr create --repo <owner>/<repo>` and `gh api repos/<owner>/<repo>/pulls -X POST` are denied if `<owner>` is not `wipcomputer`. Triggered by the 2026-04-18 PR #89 incident. `gh pr view/list/merge/edit` and `gh api .../issues` pass through. Override: `LDM_GUARD_UPSTREAM_PR_APPROVED=<owner>/<repo>` or `=1`.
|
|
292
|
+
|
|
293
|
+
### Override env vars
|
|
294
|
+
|
|
295
|
+
All overrides are audited. Every use appends to `~/.ldm/state/bypass-audit.jsonl` as `{kind, ts, session_id, tool, path, via, reason}`. Log rotates at 50 MB to dated archives.
|
|
296
|
+
|
|
297
|
+
| Env var | Purpose | Shape |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `LDM_GUARD_SKIP_ONBOARDING` | Skip onboarding gate | `<repo-path>` or `1` |
|
|
300
|
+
| `LDM_GUARD_ACK_BLOCKED_FILE` | Acknowledge a prior block | `<file-path>` or `1` |
|
|
301
|
+
| `LDM_GUARD_UPSTREAM_PR_APPROVED` | Parker-approved external PR | `<owner>/<repo>` or `1` |
|
|
302
|
+
| `LDM_GUARD_APPROVAL_BACKEND` | Backend selector | `env` (default), future: `bridge`, `kaleidoscope-biometric` |
|
|
303
|
+
| `LDM_GUARD_STATE_DIR` | State file location | dir path (test isolation only) |
|
|
304
|
+
|
|
305
|
+
Default stance: don't bypass. Read the block message. It tells you what to do.
|
|
306
|
+
|
|
307
|
+
### Expected first-write ritual
|
|
308
|
+
|
|
309
|
+
For any repo new to your session:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
1. git rev-parse --show-toplevel # confirm the repo
|
|
313
|
+
2. Read README.md # via Read tool
|
|
314
|
+
3. Read CLAUDE.md # if present
|
|
315
|
+
4. Read RUNBOOK / LANDMINES / WORKFLOW # if present at root
|
|
316
|
+
5. Proceed with Write/Edit
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
State persists at `~/.ldm/state/guard-session.json`. Same session + same repo = onboarded for 2h. New session or TTL expires = re-onboard.
|
|
320
|
+
|
|
321
|
+
### Bypass audit trail
|
|
322
|
+
|
|
323
|
+
Every deny and every approved bypass is in `~/.ldm/state/bypass-audit.jsonl`. Append-only JSON Lines. Tail it during post-mortems; rotate archives are `bypass-audit.jsonl.YYYY-MM-DD`.
|
|
324
|
+
|
|
268
325
|
## Worktree Workflow (WIP-specific)
|
|
269
326
|
|
|
270
327
|
Same as the public Dev Guide section, plus:
|