leopold-driver 0.1.1 → 0.1.2
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/README.md +19 -5
- package/assets/VERSION +1 -0
- package/assets/extensions/README.md +52 -0
- package/assets/extensions/gstack/extension.json +8 -0
- package/assets/extensions/gstack/manage.sh +68 -0
- package/assets/extensions/leopold/extension.json +8 -0
- package/assets/extensions/leopold/manage.sh +59 -0
- package/assets/extensions/ovmem/README.md +101 -0
- package/assets/extensions/ovmem/extension.json +8 -0
- package/assets/extensions/ovmem/install.sh +330 -0
- package/assets/extensions/ovmem/manage.sh +87 -0
- package/assets/extensions/ovmem/models.json +24 -0
- package/assets/extensions/ovmem/payload/RUNTIME.md +121 -0
- package/assets/extensions/ovmem/payload/ovmem-cleanup.py +148 -0
- package/assets/extensions/ovmem/payload/ovmem.py +421 -0
- package/assets/extensions/serena/README.md +50 -0
- package/assets/extensions/serena/extension.json +8 -0
- package/assets/extensions/serena/manage.sh +119 -0
- package/assets/hooks/guard-irreversible.sh +185 -0
- package/assets/hooks/hooks.json +20 -0
- package/assets/hooks/stop-continuity.sh +132 -0
- package/assets/install.sh +150 -0
- package/assets/scripts/__pycache__/leopold-watch.cpython-312.pyc +0 -0
- package/assets/scripts/leopold-doctor.sh +53 -0
- package/assets/scripts/leopold-menu.sh +132 -0
- package/assets/scripts/leopold-update-check.sh +23 -0
- package/assets/scripts/leopold-update.sh +13 -0
- package/assets/scripts/leopold-watch.py +585 -0
- package/assets/scripts/record-demo.sh +61 -0
- package/assets/scripts/test-guard.sh +76 -0
- package/assets/scripts/test-hooks.sh +121 -0
- package/assets/settings.template.json +23 -0
- package/assets/skills/leopold-brief/SKILL.md +121 -0
- package/assets/skills/leopold-doctor/SKILL.md +23 -0
- package/assets/skills/leopold-run/SKILL.md +171 -0
- package/assets/skills/leopold-status/SKILL.md +34 -0
- package/assets/skills/leopold-stop/SKILL.md +36 -0
- package/assets/skills/leopold-update/SKILL.md +27 -0
- package/assets/skills/leopold-watch/SKILL.md +48 -0
- package/assets/templates/CHARTER.md +32 -0
- package/assets/templates/DECISIONS.md +15 -0
- package/assets/templates/GUARDRAILS.md +38 -0
- package/assets/templates/MISSION.md +22 -0
- package/assets/templates/PLAN.md +9 -0
- package/dist/guard.js +82 -23
- package/dist/harness.js +71 -0
- package/dist/index.js +53 -23
- package/package.json +6 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: leopold-watch
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
description: "Launch a local live web dashboard to watch the current project's Leopold run — status, cost meters (context/subagents/forks), event feed, decisions, and a Stop button. Local-only (127.0.0.1), zero dependencies."
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Bash
|
|
7
|
+
triggers:
|
|
8
|
+
- leopold watch
|
|
9
|
+
- watch the run
|
|
10
|
+
- open the dashboard
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# /leopold-watch
|
|
14
|
+
|
|
15
|
+
Start the local live dashboard for this project's Leopold run and give the user the URL.
|
|
16
|
+
It only **reads** `.leopold/` (state, plan, decisions, events) and serves on loopback;
|
|
17
|
+
nothing leaves the machine. The one action it offers is a Stop button, which touches
|
|
18
|
+
`.leopold/STOP` — the same kill switch as `/leopold-stop`.
|
|
19
|
+
|
|
20
|
+
Run this (idempotent — if it is already up, just report the URL):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
WATCH="$HOME/.claude/leopold/scripts/leopold-watch.py"; [ -f "$WATCH" ] || WATCH="scripts/leopold-watch.py"
|
|
24
|
+
PORT="${LEOPOLD_WATCH_PORT:-4179}"
|
|
25
|
+
if curl -sf "http://127.0.0.1:$PORT/api/state" >/dev/null 2>&1; then
|
|
26
|
+
echo "Dashboard already running -> http://127.0.0.1:$PORT"
|
|
27
|
+
else
|
|
28
|
+
nohup python3 "$WATCH" --project "$(pwd)" --port "$PORT" >/tmp/leopold-watch.log 2>&1 &
|
|
29
|
+
sleep 1
|
|
30
|
+
if curl -sf "http://127.0.0.1:$PORT/api/state" >/dev/null 2>&1; then
|
|
31
|
+
echo "Dashboard started -> http://127.0.0.1:$PORT (pid $!)"
|
|
32
|
+
else
|
|
33
|
+
echo "Failed to start; last log lines:"; tail -3 /tmp/leopold-watch.log
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then tell the user, briefly:
|
|
39
|
+
|
|
40
|
+
- Open **http://127.0.0.1:4179** (or the port shown) in a browser to watch the run live:
|
|
41
|
+
run status, cost meters (context MB, subagents, forks, iterations, failures vs their
|
|
42
|
+
budgets), the live event feed, the decisions log, and a **Stop** button.
|
|
43
|
+
- It shows real data only while a run is active (`/leopold-run`); otherwise it says
|
|
44
|
+
"no active run" and updates the moment one starts.
|
|
45
|
+
- To stop the dashboard itself: `pkill -f leopold-watch.py`. It also runs via `make watch`
|
|
46
|
+
or, from the npm package, `npx leopold-driver watch`.
|
|
47
|
+
|
|
48
|
+
Do not do anything else. Start it, report the URL, stop.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Charter
|
|
2
|
+
|
|
3
|
+
> This is the decider. The run consults it on every fork so it chooses the way
|
|
4
|
+
> you would. Be concrete; concrete rules resolve real decisions, vague ones do
|
|
5
|
+
> not. This is the part of Leopold that "becomes you."
|
|
6
|
+
|
|
7
|
+
## What I optimize for
|
|
8
|
+
> Ranked. Example: 1) shipping working software, 2) simplicity, 3) speed.
|
|
9
|
+
1. ...
|
|
10
|
+
|
|
11
|
+
## Technology and style preferences
|
|
12
|
+
> Languages, frameworks, patterns you favor or avoid.
|
|
13
|
+
- Prefer: ...
|
|
14
|
+
- Avoid: ...
|
|
15
|
+
|
|
16
|
+
## Always
|
|
17
|
+
> Things the run should always do.
|
|
18
|
+
- ...
|
|
19
|
+
|
|
20
|
+
## Never
|
|
21
|
+
> Hard lines the run must never cross.
|
|
22
|
+
- ...
|
|
23
|
+
|
|
24
|
+
## Tie-breakers
|
|
25
|
+
> When principles conflict, how do you choose? Example: "When robustness and
|
|
26
|
+
> speed conflict on user-facing paths, robustness wins; on internal tooling,
|
|
27
|
+
> speed wins."
|
|
28
|
+
- ...
|
|
29
|
+
|
|
30
|
+
## Examples of decisions I would make
|
|
31
|
+
> 2-4 worked examples. These calibrate the run better than abstract rules.
|
|
32
|
+
- Situation: ... -> I would: ... because ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
> Append-only audit trail of decisions the run made on your behalf, newest last.
|
|
4
|
+
> Each non-mechanical decision is one block. The Reversal line is your escape
|
|
5
|
+
> hatch: it tells you how to undo a call you disagree with.
|
|
6
|
+
|
|
7
|
+
<!-- Example:
|
|
8
|
+
## D1 — Cache layer for the MVP (turn 3, 2026-06-17T15:00:00Z)
|
|
9
|
+
Fork: in-memory cache vs Redis
|
|
10
|
+
Class: reversible
|
|
11
|
+
Charter: "no new infrastructure for the MVP"
|
|
12
|
+
Decision: in-memory map with a TTL
|
|
13
|
+
Why: charter rule + principle 5 (explicit over clever)
|
|
14
|
+
Reversal: swap the cache module for a Redis client; interface is unchanged
|
|
15
|
+
-->
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Guardrails
|
|
2
|
+
|
|
3
|
+
> The autonomy boundary for this run. Defaults are the recommended posture.
|
|
4
|
+
|
|
5
|
+
## Action classes
|
|
6
|
+
- Autonomous: reversible work in the repo (edit, build, lint, test, stage).
|
|
7
|
+
- Gated (locked): git commit, git push, PR create/merge, publish, deploy.
|
|
8
|
+
- Forbidden: rm -rf outside scratch, edits outside the project root, editing
|
|
9
|
+
this file / the hooks / Claude Code settings.
|
|
10
|
+
|
|
11
|
+
## Git posture
|
|
12
|
+
> Default: LOCKED. The run stages and reports; the human commits and pushes.
|
|
13
|
+
> To allow commits this session: `touch .leopold/ALLOW_GIT` (push stays locked).
|
|
14
|
+
- Commit: locked
|
|
15
|
+
- Push / PR / publish: locked
|
|
16
|
+
|
|
17
|
+
## Stop conditions
|
|
18
|
+
- max_iterations: 50
|
|
19
|
+
- max_failures: 3 # consecutive failures of the same kind
|
|
20
|
+
- max_no_progress: 6 # turns with the open PLAN set unchanged -> loop, stop
|
|
21
|
+
- max_subagents: 8 # total Task/subagent spawns per run; past it the guard
|
|
22
|
+
# denies more (each subagent re-loads the full context =
|
|
23
|
+
# the #1 cost multiplier). Raise only if you must.
|
|
24
|
+
- max_forks: 0 # forks clone the WHOLE session context (the leak) — forbidden
|
|
25
|
+
# by default; raise only if a sub-task needs the full convo
|
|
26
|
+
- max_context_mb: 5 # stop when the transcript passes this; a long run re-bills
|
|
27
|
+
# its growing context every turn. Resume with a fresh run.
|
|
28
|
+
- token/time budget: none # set if you want a hard ceiling
|
|
29
|
+
|
|
30
|
+
## On finish
|
|
31
|
+
- on_finish: keep # keep | archive
|
|
32
|
+
> keep: brief, DECISIONS, and events stay in place. archive: on a clean finish,
|
|
33
|
+
> DECISIONS.md and events.jsonl move to .leopold/runs/<timestamp>/. Either way,
|
|
34
|
+
> the kill switch and git opt-in tokens are always cleared when a run stops.
|
|
35
|
+
|
|
36
|
+
## Kill switch
|
|
37
|
+
- `/leopold-stop` (clean) or `touch .leopold/STOP` (blunt). Takes effect at the
|
|
38
|
+
next turn boundary; nothing is interrupted mid-turn.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Mission
|
|
2
|
+
|
|
3
|
+
> What we are building and why. The autonomous run executes this; it does not
|
|
4
|
+
> invent it. Fill every section. Delete the guidance quotes when done.
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
> What hurts today, and why it matters now.
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
> The outcome we want. One or two sentences.
|
|
11
|
+
|
|
12
|
+
## Non-goals
|
|
13
|
+
> What we are deliberately NOT doing this run. This prevents scope creep.
|
|
14
|
+
- ...
|
|
15
|
+
|
|
16
|
+
## Definition of done
|
|
17
|
+
> Concrete, checkable. How we will know the mission succeeded.
|
|
18
|
+
- [ ] ...
|
|
19
|
+
|
|
20
|
+
## Constraints
|
|
21
|
+
> Stack, deadlines, things that must not change, environments.
|
|
22
|
+
- ...
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Plan
|
|
2
|
+
|
|
3
|
+
> Ordered, checkbox backlog. Each item should be independently completable and
|
|
4
|
+
> verifiable. The run takes the next unchecked item each turn and marks it [x]
|
|
5
|
+
> when done. Order by dependency, then by value. Keep items small.
|
|
6
|
+
|
|
7
|
+
- [ ] First item
|
|
8
|
+
- [ ] Second item
|
|
9
|
+
- [ ] Third item
|
package/dist/guard.js
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
1
|
// The git lock, in driver form. Used as the worker's canUseTool callback so the
|
|
2
2
|
// same policy as the in-session hook holds when the worker runs under the SDK.
|
|
3
|
-
// It only adds denials; it never loosens anything.
|
|
3
|
+
// It only adds denials; it never loosens anything. Detection mirrors the hardened
|
|
4
|
+
// bash guard (hooks/guard-irreversible.sh) and is covered by test/guard.test.ts.
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const GH_PR = /(^|[^\w-])gh(\s.*)?\s(pr\s+(create|merge)|release\s+create)/i;
|
|
8
|
+
const PUBLISH = /(npm|pnpm|yarn)\s+publish|cargo\s+publish|twine\s+upload|pip\s+.*upload/i;
|
|
9
|
+
const PROTECTED_PATH = /(GUARDRAILS\.md|settings\.json|settings\.local\.json|leopold\/hooks\/|\.leopold\/state\.json)/;
|
|
10
|
+
// git global options that consume the following token as their value.
|
|
11
|
+
const GIT_VALUE_OPTS = new Set([
|
|
12
|
+
"-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env",
|
|
13
|
+
]);
|
|
14
|
+
/** Collapse whitespace so tab/space evasion does not change matching. */
|
|
15
|
+
function norm(s) {
|
|
16
|
+
return s.replace(/[\t\n]+/g, " ").replace(/ +/g, " ").trim();
|
|
17
|
+
}
|
|
18
|
+
/** Resolve the git subcommand, skipping global options + their values, by basename. */
|
|
19
|
+
export function gitSubcommand(cmd) {
|
|
20
|
+
const toks = norm(cmd).split(" ");
|
|
21
|
+
let i = toks.findIndex((t) => t.split("/").pop() === "git");
|
|
22
|
+
if (i === -1)
|
|
23
|
+
return "";
|
|
24
|
+
i += 1;
|
|
25
|
+
while (i < toks.length) {
|
|
26
|
+
const t = toks[i];
|
|
27
|
+
if (GIT_VALUE_OPTS.has(t)) {
|
|
28
|
+
i += 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (t.startsWith("-")) {
|
|
32
|
+
i += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
return t;
|
|
36
|
+
}
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
/** Recursive AND forced rm, in any spelling (--recursive --force, -r -f, -rf, /bin/rm -rf). */
|
|
40
|
+
export function isRecursiveForceRm(cmd) {
|
|
41
|
+
const c = norm(cmd);
|
|
42
|
+
if (!/(^|[^\w-])rm(\s|$)/i.test(c))
|
|
43
|
+
return false;
|
|
44
|
+
const recursive = /(--recursive|(^|\s)-[a-z]*r)/i.test(c);
|
|
45
|
+
const force = /(--force|(^|\s)-[a-z]*f)/i.test(c);
|
|
46
|
+
return recursive && force;
|
|
47
|
+
}
|
|
48
|
+
/** find ... -delete or find ... -exec rm. */
|
|
49
|
+
export function isFindDelete(cmd) {
|
|
50
|
+
const c = norm(cmd);
|
|
51
|
+
if (!/(^|[^\w-])find(\s|$)/i.test(c))
|
|
52
|
+
return false;
|
|
53
|
+
return /\s-delete(\s|$)/i.test(c) || /-exec\s+rm(\s|$)/i.test(c);
|
|
54
|
+
}
|
|
14
55
|
function hasToken(leoDir, name) {
|
|
15
56
|
return fs.existsSync(path.join(leoDir, name));
|
|
16
57
|
}
|
|
@@ -21,26 +62,44 @@ export function makeGuard(leoDir, onBlock) {
|
|
|
21
62
|
return { behavior: "deny", message };
|
|
22
63
|
};
|
|
23
64
|
if (toolName === "Bash") {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
return deny("Leopold guard:
|
|
27
|
-
if (
|
|
28
|
-
return deny("Leopold guard:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
const c = norm(String(input.command ?? ""));
|
|
66
|
+
if (isRecursiveForceRm(c))
|
|
67
|
+
return deny("Leopold guard: recursive+forced rm is forbidden in autonomous mode.");
|
|
68
|
+
if (isFindDelete(c))
|
|
69
|
+
return deny("Leopold guard: 'find -delete' / 'find -exec rm' is forbidden in autonomous mode.");
|
|
70
|
+
switch (gitSubcommand(c)) {
|
|
71
|
+
case "reset":
|
|
72
|
+
if (/(^|\s)--hard(\s|$)/.test(c))
|
|
73
|
+
return deny("Leopold guard: 'git reset --hard' is forbidden in autonomous mode.");
|
|
74
|
+
break;
|
|
75
|
+
case "clean":
|
|
76
|
+
if (/(--force|(^|\s)-[a-z]*f)/i.test(c))
|
|
77
|
+
return deny("Leopold guard: 'git clean -f' is forbidden in autonomous mode.");
|
|
78
|
+
break;
|
|
79
|
+
case "branch":
|
|
80
|
+
if (/(^|\s)-D(\s|$)/.test(c))
|
|
81
|
+
return deny("Leopold guard: 'git branch -D' is forbidden in autonomous mode.");
|
|
82
|
+
break;
|
|
83
|
+
case "push":
|
|
84
|
+
if (/(--force|--force-with-lease|(^|\s)-f(\s|$))/.test(c))
|
|
85
|
+
return deny("Leopold guard: force-push is forbidden in autonomous mode.");
|
|
86
|
+
if (!hasToken(leoDir, "ALLOW_PUSH"))
|
|
87
|
+
return deny("Leopold guard: git push is locked. Report readiness instead.");
|
|
88
|
+
break;
|
|
89
|
+
case "commit":
|
|
90
|
+
if (!hasToken(leoDir, "ALLOW_GIT"))
|
|
91
|
+
return deny("Leopold guard: git commit is locked. Stage and report; the user commits. (touch .leopold/ALLOW_GIT to allow commits this run.)");
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (GH_PR.test(c) && !hasToken(leoDir, "ALLOW_PUSH"))
|
|
36
95
|
return deny("Leopold guard: opening or merging PRs and creating releases is locked.");
|
|
37
|
-
if (PUBLISH.test(
|
|
96
|
+
if (PUBLISH.test(c) && !hasToken(leoDir, "ALLOW_PUBLISH"))
|
|
38
97
|
return deny("Leopold guard: publishing packages is locked in autonomous mode.");
|
|
39
98
|
}
|
|
40
99
|
if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit" || toolName === "NotebookEdit") {
|
|
41
100
|
const p = String(input.file_path ?? input.path ?? "");
|
|
42
101
|
if (PROTECTED_PATH.test(p))
|
|
43
|
-
return deny("Leopold guard: editing guardrails, settings, or
|
|
102
|
+
return deny("Leopold guard: editing guardrails, settings, hooks, or run state is forbidden in autonomous mode.");
|
|
44
103
|
}
|
|
45
104
|
return { behavior: "allow", updatedInput: input };
|
|
46
105
|
};
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Harness commands for the leopold-driver CLI: install / menu / watch / extensions /
|
|
2
|
+
// doctor / update. They run the bundled harness assets (skills, hooks, installer,
|
|
3
|
+
// extensions) that `npm run build` vendors into ../assets, so everything works from the
|
|
4
|
+
// npm package alone — no repo clone, no `make`.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
/** Locate the vendored harness root (assets/ in the package, or the repo root in dev). */
|
|
10
|
+
export function assetRoot() {
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const bundled = join(here, "..", "assets");
|
|
13
|
+
if (existsSync(join(bundled, "install.sh")))
|
|
14
|
+
return bundled;
|
|
15
|
+
let dir = here; // dev: walk up to the repo root (has install.sh + skills/)
|
|
16
|
+
for (let i = 0; i < 6; i++) {
|
|
17
|
+
if (existsSync(join(dir, "install.sh")) && existsSync(join(dir, "skills")))
|
|
18
|
+
return dir;
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir)
|
|
21
|
+
break;
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
return bundled;
|
|
25
|
+
}
|
|
26
|
+
function run(cmd, args) {
|
|
27
|
+
const r = spawnSync(cmd, args, { stdio: "inherit" });
|
|
28
|
+
if (r.error) {
|
|
29
|
+
const e = r.error;
|
|
30
|
+
const hint = e.code === "ENOENT" ? ` (is "${cmd}" installed and on PATH?)` : "";
|
|
31
|
+
console.error(`leopold-driver: could not run ${cmd}${hint}: ${e.message}`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
return r.status ?? 0;
|
|
35
|
+
}
|
|
36
|
+
export function runInstall(args) {
|
|
37
|
+
return run("bash", [join(assetRoot(), "install.sh"), ...args]);
|
|
38
|
+
}
|
|
39
|
+
export function runMenu() {
|
|
40
|
+
return run("bash", [join(assetRoot(), "scripts", "leopold-menu.sh")]);
|
|
41
|
+
}
|
|
42
|
+
export function runWatch(args) {
|
|
43
|
+
const script = join(assetRoot(), "scripts", "leopold-watch.py");
|
|
44
|
+
if (!existsSync(script)) {
|
|
45
|
+
console.error("leopold-driver watch: dashboard script (leopold-watch.py) not found.");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
const py = process.env.LEOPOLD_PYTHON ?? "python3";
|
|
49
|
+
return run(py, [script, "--project", process.cwd(), ...args]);
|
|
50
|
+
}
|
|
51
|
+
export function runExt(name, args) {
|
|
52
|
+
const mgr = join(assetRoot(), "extensions", name, "manage.sh");
|
|
53
|
+
if (!existsSync(mgr)) {
|
|
54
|
+
console.error(`leopold-driver: unknown extension "${name}". Try: serena, gstack, ovmem.`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
return run("bash", [mgr, ...(args.length ? args : ["status"])]);
|
|
58
|
+
}
|
|
59
|
+
export function runDoctor() {
|
|
60
|
+
const extDir = join(assetRoot(), "extensions");
|
|
61
|
+
let rc = 0;
|
|
62
|
+
for (const name of existsSync(extDir) ? readdirSync(extDir) : []) {
|
|
63
|
+
if (!existsSync(join(extDir, name, "manage.sh")))
|
|
64
|
+
continue;
|
|
65
|
+
process.stdout.write(`\n[${name}]\n`);
|
|
66
|
+
const r = run("bash", [join(extDir, name, "manage.sh"), "doctor"]);
|
|
67
|
+
if (r !== 0)
|
|
68
|
+
rc = r;
|
|
69
|
+
}
|
|
70
|
+
return rc;
|
|
71
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// leopold-driver:
|
|
3
|
-
//
|
|
2
|
+
// leopold-driver: Leopold from npm. Install + manage the harness, conduct runs, and
|
|
3
|
+
// watch — without cloning the repo or running `make`. The harness assets are bundled
|
|
4
|
+
// into the package at build time; subcommands run them.
|
|
4
5
|
import { runDriver } from "./loop.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { runInstall, runMenu, runWatch, runExt, runDoctor } from "./harness.js";
|
|
7
|
+
const sub = process.argv[2];
|
|
8
|
+
const rest = process.argv.slice(3);
|
|
9
|
+
function help() {
|
|
10
|
+
process.stdout.write(`leopold-driver — Leopold from npm. Manage the harness, conduct runs, watch.
|
|
8
11
|
|
|
9
12
|
Usage:
|
|
10
|
-
leopold-driver
|
|
13
|
+
leopold-driver install [--with-gstack] install skills + hooks into ~/.claude
|
|
14
|
+
leopold-driver menu toolchain manager (serena / gstack / ovmem)
|
|
15
|
+
leopold-driver watch [--port N] live dashboard (http://127.0.0.1:4179)
|
|
16
|
+
leopold-driver serena [install|doctor] manage an extension (also: gstack, ovmem)
|
|
17
|
+
leopold-driver doctor run every extension's doctor
|
|
18
|
+
leopold-driver update reinstall from this package
|
|
19
|
+
leopold-driver run [--dry-run] conduct the .leopold run (the SDK driver)
|
|
11
20
|
|
|
12
|
-
|
|
21
|
+
Most commands run the bundled harness — no repo clone, no make. 'watch' needs Python 3.
|
|
22
|
+
Newer version: npm i -g leopold-driver@latest.
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
and the conductor. No API key needed. ANTHROPIC_API_KEY is only required in a
|
|
17
|
-
headless environment that has no Claude Code auth configured.
|
|
18
|
-
|
|
19
|
-
Env:
|
|
20
|
-
LEOPOLD_CONDUCTOR_MODEL conductor model (default: your Claude Code default)
|
|
21
|
-
LEOPOLD_WORKER_MODEL worker model (default: your Claude Code default)
|
|
22
|
-
LEOPOLD_MAX_TURNS_PER_ITEM max worker turns per item (default: 40)
|
|
23
|
-
LEOPOLD_WEBHOOK optional URL for JSON POST notifications
|
|
24
|
+
Conducting a run uses your existing Claude Code login (ANTHROPIC_API_KEY only in headless).
|
|
25
|
+
Env: LEOPOLD_CONDUCTOR_MODEL, LEOPOLD_WORKER_MODEL, LEOPOLD_MAX_TURNS_PER_ITEM, LEOPOLD_WEBHOOK
|
|
24
26
|
`);
|
|
25
|
-
process.exit(0);
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
function conduct() {
|
|
29
|
+
runDriver(process.cwd(), process.argv.slice(2)).catch((err) => {
|
|
30
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
31
|
+
console.error("leopold-driver error:", msg);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case "install":
|
|
37
|
+
case "update":
|
|
38
|
+
process.exit(runInstall(rest));
|
|
39
|
+
case "menu":
|
|
40
|
+
process.exit(runMenu());
|
|
41
|
+
case "watch":
|
|
42
|
+
case "watch-web":
|
|
43
|
+
process.exit(runWatch(rest));
|
|
44
|
+
case "serena":
|
|
45
|
+
case "gstack":
|
|
46
|
+
case "ovmem":
|
|
47
|
+
process.exit(runExt(sub, rest));
|
|
48
|
+
case "doctor":
|
|
49
|
+
process.exit(runDoctor());
|
|
50
|
+
case "--help":
|
|
51
|
+
case "-h":
|
|
52
|
+
case "help":
|
|
53
|
+
help();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
default:
|
|
56
|
+
if (sub && sub !== "run" && !sub.startsWith("-")) {
|
|
57
|
+
console.error(`leopold-driver: unknown command "${sub}". Try: leopold-driver --help`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
conduct();
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leopold-driver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Leopold SDK driver: a persistent conductor that orchestrates fresh Claude Code workers per task, decides from your charter, and notifies you. Uses your Claude Code auth. Git stays locked.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"leopold-driver": "dist/index.js"
|
|
7
|
+
"leopold-driver": "dist/index.js",
|
|
8
|
+
"leopold": "dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"main": "dist/index.js",
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
13
|
+
"assets",
|
|
12
14
|
"README.md"
|
|
13
15
|
],
|
|
14
16
|
"engines": {
|
|
@@ -33,8 +35,9 @@
|
|
|
33
35
|
"anthropic"
|
|
34
36
|
],
|
|
35
37
|
"scripts": {
|
|
36
|
-
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-runtime.mjs",
|
|
37
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"test": "node --experimental-strip-types --test test/protocol.test.ts test/guard.test.ts",
|
|
38
41
|
"dev": "tsx src/index.ts",
|
|
39
42
|
"start": "node dist/index.js",
|
|
40
43
|
"prepublishOnly": "npm run build"
|