@devwithdavid/ledger 0.1.2 → 0.1.4

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/LEDGER.md CHANGED
@@ -100,7 +100,13 @@ the code.
100
100
  code; all code change goes through dispatched agents. Sole exception: a
101
101
  concrete, in-the-moment, user-approved operation — executed exactly as
102
102
  approved, never inferred or generalized, conferring no standing
103
- authority.
103
+ authority. This exception never covers committing straight to a
104
+ project's default branch (`main`, `master`, or whatever it's configured
105
+ as): a branch + PR stays the default delivery path even for your own
106
+ explicit-exception edit, unless the user's in-the-moment instruction
107
+ specifically names the default branch itself. You do not commit to a
108
+ project's default branch — including this project's own — on your own
109
+ initiative, ever.
104
110
  - **C2 — you never merge a branch or PR, force-push, or close a PR without an
105
111
  explicit user word naming the specific merge/pull request.** One explicit
106
112
  word at a time, in the moment, for that specific PR; there is no standing
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Kartik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -32,39 +32,57 @@ That's the whole install. The package ships pre-built, and the global
32
32
  install puts `ledger` on your PATH — no build step, no separate linking
33
33
  step: the `ledger` binary lands on PATH with the install itself.
34
34
 
35
- **Link the watcher plugin into herdr** — this is what keeps agent status
36
- current automatically as dispatched agents work, without any polling.
37
- `herdr-plugin.toml` ships inside the package, so point the link at the
38
- installed package's root:
35
+ **Finish the setup with `ledger init`** — the single post-install step:
39
36
 
40
37
  ```sh
41
- herdr plugin link "$(npm root -g)/@devwithdavid/ledger"
38
+ ledger init
42
39
  ```
43
40
 
44
- This registers `herdr-plugin.toml`, so herdr invokes `dist/plugin/watcher.js`
45
- whenever a pane's detected agent state changes. It's local and reversible:
46
- `herdr plugin unlink ledger` removes it. herdr always runs whatever's
47
- currently in the installed package's `dist/`, so `npm update -g
48
- @devwithdavid/ledger` picks up new releases (including watcher changes) on
49
- the next event; re-run the link command if you want the registered version
50
- to track a new release.
41
+ Run it once after installing. It does four things, printing a status line
42
+ for each as it goes:
43
+
44
+ 1. **Verifies `herdr` and `treehouse` are on your PATH.** If either is
45
+ missing it exits without doing anything else, with an install pointer
46
+ for each missing tool (herdr: <https://herdr.dev>, treehouse:
47
+ <https://github.com/kunchenguid/treehouse>).
48
+ 2. **Creates the ledger database** at `$LEDGER_HOME/ledger.db` if it
49
+ doesn't exist yet. An existing store is never reset or rewritten.
50
+ 3. **Links the herdr watcher plugin**: runs `herdr plugin link` on the
51
+ installed package's root, where `herdr-plugin.toml` ships. That's what
52
+ keeps agent status current automatically as dispatched agents work,
53
+ without any polling — herdr invokes `dist/plugin/watcher.js` whenever a
54
+ pane's detected agent state changes.
55
+ 4. **Installs the clerk skill** at `~/.agents/skills/ledger/SKILL.md` from
56
+ the copy bundled with this install, symlinking it into
57
+ `~/.claude/skills/ledger` and `~/.pi/agent/skills/ledger` so either tool
58
+ picks it up. See "Starting a clerk session" below for what the skill
59
+ does.
60
+
61
+ It's idempotent — re-running it (for example after `npm update -g
62
+ @devwithdavid/ledger`) is safe: the store is only created if missing,
63
+ re-linking the plugin leaves herdr's registration unchanged, the skill
64
+ file is always re-synced from the bundled copy, and an existing symlink is
65
+ only touched if it doesn't already point at the right place. The plugin
66
+ link is local and reversible: `herdr plugin unlink ledger` removes it.
67
+ herdr always runs whatever's currently in the installed package's `dist/`,
68
+ so `npm update -g @devwithdavid/ledger` picks up new releases (including
69
+ watcher changes) on the next event.
51
70
 
52
71
  ## Starting a clerk session
53
72
 
54
73
  You don't run `ledger` commands yourself day to day — you talk to **the
55
- clerk** (a Claude Code or Pi session), and it runs them on your behalf. A
56
- `ledger` skill is installed at `~/.agents/skills/ledger` (symlinked into
57
- both `~/.claude/skills/` and `~/.pi/agent/skills/`) so either tool can pick
58
- it up — it loads only when you actually ask for ledger-related work
59
- (register a project, dispatch an agent, check status, ...), not on every
60
- unrelated session.
74
+ clerk** (a Claude Code or Pi session), and it runs them on your behalf.
75
+ `ledger init` (step 4 above) installs a `ledger` skill so either Claude
76
+ Code or Pi can pick it up — it loads only when you actually ask for
77
+ ledger-related work (register a project, dispatch an agent, check status,
78
+ ...), not on every unrelated session.
61
79
 
62
80
  The skill itself carries no machine-specific path: it just tells the clerk
63
81
  to run `ledger docs`, which prints `LEDGER.md` by resolving it relative to
64
82
  wherever `ledger` is actually installed (works correctly through the
65
83
  `npm link` symlink too — proven live, see `DECISIONS.md`). That's what
66
- makes the skill portable to a fresh machine as-is: install `ledger` there
67
- per this README, and the skill works with no edits.
84
+ makes the skill portable to a fresh machine as-is: run `ledger init` there
85
+ and the skill works with no edits.
68
86
 
69
87
  ## Quick start (what the clerk actually runs)
70
88
 
@@ -136,3 +154,12 @@ with a high release cadence; some of the CLI/plugin behavior this repo
136
154
  depends on was reverse-engineered live (their docs don't fully match
137
155
  current behavior in places — see `DECISIONS.md`) and may need
138
156
  re-verification after either tool upgrades.
157
+
158
+ ## License
159
+
160
+ MIT — see [`LICENSE`](./LICENSE).
161
+
162
+ Versions 0.1.0-0.1.2 were published to npm before this LICENSE file
163
+ existed, so their tarballs don't contain it: published npm versions are
164
+ immutable. As the sole author, the copyright holder grants those versions
165
+ the same MIT license.
@@ -0,0 +1,192 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { delimiter, dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { getDb, ledgerHome } from "../../db/client.js";
7
+ // This file compiles to dist/cli/commands/init.js, three levels under the
8
+ // package root — resolve the package root relative to *this running code's
9
+ // own location* rather than the cwd (same approach as docs.ts), so it works
10
+ // from any invocation directory, in a dev checkout and in an npm install.
11
+ // The herdr plugin manifest (herdr-plugin.toml) lives at the package root,
12
+ // so that directory is what gets linked.
13
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
14
+ // Step 4's bundled skill template: a hand-authored `~/.agents/skills/ledger`
15
+ // (cross-agent skill tree; see DECISIONS.md "Clerk bootstrapping") existed
16
+ // on one machine only and was never captured as a reproducible setup step —
17
+ // a second machine's clerk session never loaded LEDGER.md as a result and
18
+ // spent a whole session unaware of its own governance gates. This ships the
19
+ // real skill content as a package asset instead, resolved the same way as
20
+ // the herdr plugin root above, so `init` can (re-)install it anywhere.
21
+ const SKILL_TEMPLATE_PATH = join(PACKAGE_ROOT, "skills/ledger/SKILL.md");
22
+ const AGENTS_SKILL_DIR = join(homedir(), ".agents/skills/ledger");
23
+ const AGENTS_SKILL_FILE = join(AGENTS_SKILL_DIR, "SKILL.md");
24
+ // Deliberately outside this repo (same reasoning as the skill itself living
25
+ // outside it) — per-tool skill directories are symlinks into the shared
26
+ // `~/.agents/skills/<name>` tree, so any coding agent that understands that
27
+ // convention picks it up with zero ledger-specific config of its own.
28
+ const SKILL_SYMLINK_TARGETS = [join(homedir(), ".claude/skills/ledger"), join(homedir(), ".pi/agent/skills/ledger")];
29
+ // Install pointers for the pre-check:
30
+ // herdr: derived from the tool's own npm package metadata, verified
31
+ // 2026-09-03 — NOT guessed: npm view herdr homepage -> https://herdr.dev
32
+ // treehouse: npm's 'treehouse' is an unrelated React package (name
33
+ // squat); the required tool is kunchenguid's git-worktree tool
34
+ // (installed binary v2.3.0) — URL per user confirmation 2026-09-04,
35
+ // not npm metadata.
36
+ // (The README's prerequisites section carries the same treehouse URL —
37
+ // the two now agree; see DECISIONS.md for the resolution.)
38
+ const DOCS = {
39
+ herdr: "https://herdr.dev",
40
+ treehouse: "https://github.com/kunchenguid/treehouse",
41
+ };
42
+ const REQUIRED_TOOLS = ["herdr", "treehouse"];
43
+ /**
44
+ * PATH lookup for a required tool: the first $PATH directory that contains
45
+ * an executable FILE named <tool> wins. A plain lookup, deliberately not a
46
+ * version/capability probe — the pre-check's job is to fail fast with a
47
+ * per-tool install pointer, not to audit the tool (item 30).
48
+ */
49
+ function findOnPath(tool) {
50
+ const dirs = (process.env["PATH"] ?? "").split(delimiter).filter((d) => d.length > 0);
51
+ for (const dir of dirs) {
52
+ const candidate = join(dir, tool);
53
+ try {
54
+ accessSync(candidate, constants.X_OK);
55
+ if (statSync(candidate).isFile())
56
+ return candidate;
57
+ }
58
+ catch {
59
+ // not here (or not executable) — keep looking
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Step 4 helper: symlink one per-tool skill path to AGENTS_SKILL_DIR,
66
+ * creating its parent directory if needed. Idempotent: a symlink already
67
+ * pointing at the right place is a silent no-op; a pre-existing real
68
+ * file/directory (or a symlink to somewhere else) is left untouched with a
69
+ * warning rather than clobbered — this must never destroy something a user
70
+ * put there on purpose.
71
+ */
72
+ function linkSkill(linkPath) {
73
+ mkdirSync(dirname(linkPath), { recursive: true });
74
+ if (existsSync(linkPath)) {
75
+ let currentTarget;
76
+ try {
77
+ currentTarget = readlinkSync(linkPath);
78
+ }
79
+ catch {
80
+ // Exists but isn't a symlink at all.
81
+ }
82
+ if (currentTarget === AGENTS_SKILL_DIR) {
83
+ console.log(`✓ ${linkPath} already links to ${AGENTS_SKILL_DIR}`);
84
+ }
85
+ else {
86
+ console.log(`Warning: ${linkPath} already exists and is not a symlink to ${AGENTS_SKILL_DIR} ` +
87
+ `(${currentTarget ?? "a real file/directory"}) — left untouched. Remove it and ` +
88
+ `re-run 'ledger init' to relink.`);
89
+ }
90
+ return;
91
+ }
92
+ symlinkSync(AGENTS_SKILL_DIR, linkPath);
93
+ console.log(`✓ linked ${linkPath} -> ${AGENTS_SKILL_DIR}`);
94
+ }
95
+ /**
96
+ * The single post-install step (item 30, user-directed): the README's
97
+ * install section no longer tells users to hand-run `herdr plugin link
98
+ * <path>` — this command does that, transparently.
99
+ *
100
+ * Steps, in order:
101
+ * 1. Pre-check herdr AND treehouse on PATH before touching anything. If
102
+ * one or both are missing, exit non-zero with a per-tool install
103
+ * pointer (both in one error when both are missing) — no store is
104
+ * created and no link is attempted.
105
+ * 2. Ensure the ledger store, reusing getDb() (it creates $LEDGER_HOME
106
+ * and opens/migrates ledger.db; an existing store is only opened —
107
+ * never reset or rewritten). Prints the path created or found.
108
+ * 3. Link the herdr plugin: `herdr plugin link <package root>`.
109
+ * Verified live against herdr 0.7.5 (2026-09-03): re-linking a path
110
+ * that's already linked exits 0, prints its `plugin_linked` JSON
111
+ * result, and leaves herdr's plugin registry file byte-identical —
112
+ * so this step is idempotent and the command is safe to re-run (e.g.
113
+ * after `npm update -g @devwithdavid/ledger`). A non-zero exit (for
114
+ * instance an already-linked different path) is surfaced: herdr's
115
+ * own output is printed, the command fails, and the user resolves it
116
+ * (`herdr plugin unlink ledger` first).
117
+ * 4. Install/update the clerk skill: write the bundled
118
+ * `skills/ledger/SKILL.md` to `~/.agents/skills/ledger/SKILL.md`
119
+ * (always overwritten from the bundled copy, so re-running `init`
120
+ * after an upgrade re-syncs it), then symlink `~/.claude/skills/ledger`
121
+ * and `~/.pi/agent/skills/ledger` to it if not already correctly
122
+ * linked. Found missing on a second machine (see DECISIONS.md) —
123
+ * the skill's *content* was portable, but nothing ever installed it.
124
+ *
125
+ * Every step prints an explicit status line (✓) naming what was done and
126
+ * the path/URL involved, so the user sees exactly what `ledger init` did.
127
+ */
128
+ export function registerInitCommand(program) {
129
+ program
130
+ .command("init")
131
+ .description("one-time post-install setup: verify herdr and treehouse are on PATH, " +
132
+ "create the ledger store if missing, link the herdr watcher plugin, " +
133
+ "install the clerk skill (idempotent — safe to re-run)")
134
+ .action(() => {
135
+ // 1. Pre-check both required tools up front, before any side effect.
136
+ for (const tool of REQUIRED_TOOLS) {
137
+ if (findOnPath(tool)) {
138
+ console.log(`✓ ${tool} found on PATH`);
139
+ }
140
+ }
141
+ const missing = REQUIRED_TOOLS.filter((tool) => !findOnPath(tool));
142
+ if (missing.length > 0) {
143
+ // All missing tools are reported in one error, per the exact
144
+ // message format "<tool> not found on PATH - install it first:
145
+ // <docs URL>" (item 30). The CLI entry prints this as a single
146
+ // `Error:` line and exits non-zero.
147
+ throw new Error(missing.map((tool) => `${tool} not found on PATH - install it first: ${DOCS[tool]}`).join("; "));
148
+ }
149
+ // 2. Ensure the store — reuse the existing getDb() machinery (it
150
+ // creates the home dir and migrates on open). Never resets or
151
+ // rewrites an existing store.
152
+ const storePath = join(ledgerHome(), "ledger.db");
153
+ const alreadyThere = existsSync(storePath);
154
+ getDb();
155
+ console.log(`✓ ledger store ${alreadyThere ? "found at" : "created at"} ${storePath}`);
156
+ // 3. Link the herdr plugin from the package root.
157
+ const res = spawnSync("herdr", ["plugin", "link", PACKAGE_ROOT], { encoding: "utf8" });
158
+ if (res.error || res.status !== 0) {
159
+ const out = res.stdout ?? "";
160
+ const err = res.stderr ?? "";
161
+ if (out.trim())
162
+ process.stderr.write(`${out.trimEnd()}\n`);
163
+ if (err.trim())
164
+ process.stderr.write(`${err.trimEnd()}\n`);
165
+ const detail = res.error ? res.error.message : `exit ${res.status}`;
166
+ throw new Error(`herdr plugin link failed (${detail})`);
167
+ }
168
+ // Print herdr's own result so a fresh link and a no-op re-link are
169
+ // both visible, then our own status line naming the linked path.
170
+ const out = res.stdout ?? "";
171
+ if (out.trim())
172
+ console.log(out.trimEnd());
173
+ console.log(`✓ herdr plugin linked from ${PACKAGE_ROOT}`);
174
+ // 4. Install/update the clerk skill from the bundled template, then
175
+ // link it into every known per-tool skill tree.
176
+ let skillTemplate;
177
+ try {
178
+ skillTemplate = readFileSync(SKILL_TEMPLATE_PATH, "utf8");
179
+ }
180
+ catch {
181
+ throw new Error(`couldn't read the bundled skill template at ${SKILL_TEMPLATE_PATH} ` +
182
+ `— is this a complete ledger install, not a partial copy?`);
183
+ }
184
+ const skillAlreadyThere = existsSync(AGENTS_SKILL_FILE);
185
+ mkdirSync(AGENTS_SKILL_DIR, { recursive: true });
186
+ writeFileSync(AGENTS_SKILL_FILE, skillTemplate);
187
+ console.log(`✓ clerk skill ${skillAlreadyThere ? "updated at" : "installed at"} ${AGENTS_SKILL_FILE}`);
188
+ for (const linkPath of SKILL_SYMLINK_TARGETS) {
189
+ linkSkill(linkPath);
190
+ }
191
+ });
192
+ }
package/dist/cli/index.js CHANGED
@@ -1,21 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  // Must come first — see the comment in suppress-experimental-warnings.ts.
3
3
  import "./suppress-experimental-warnings.js";
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { Command } from "commander";
5
8
  import { registerAgentCommands } from "./commands/agents.js";
6
9
  import { registerCatchupCommand } from "./commands/catchup.js";
7
10
  import { registerClerkCommands } from "./commands/clerk.js";
8
11
  import { registerDocsCommand } from "./commands/docs.js";
9
12
  import { registerEventCommands } from "./commands/events.js";
13
+ import { registerInitCommand } from "./commands/init.js";
10
14
  import { registerProjectCommands } from "./commands/projects.js";
11
15
  import { registerRoadmapCommands } from "./commands/roadmap.js";
12
16
  import { touchClerkHeartbeat } from "../db/client.js";
17
+ // The version is derived from the package's own package.json at runtime —
18
+ // package.json is the ONLY source of it. This entry compiles to
19
+ // dist/cli/index.js, two levels below the package root, so resolve the
20
+ // file relative to *this running code's own location* rather than the cwd
21
+ // (same approach as docs.ts): that works from any invocation directory, in
22
+ // a dev checkout, and in an npm install (the npm tarball carries
23
+ // package.json at its root — verified in the 0.1.2 tarball). Publishing
24
+ // bumps package.json, and since this reads package.json, the two can never
25
+ // desync — a literal here is a second copy and is forbidden.
26
+ // Graceful degradation: if the file can't be read or parsed (a corrupt
27
+ // install), report "unknown" instead of throwing — a version query must
28
+ // never crash the CLI (item 30, DECISIONS.md).
29
+ function packageVersion() {
30
+ const pkgJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
31
+ try {
32
+ const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
33
+ return parsed.version ?? "unknown";
34
+ }
35
+ catch {
36
+ return "unknown";
37
+ }
38
+ }
13
39
  const program = new Command();
14
40
  program
15
41
  .name("ledger")
16
42
  .description("Durable state store for a personal agent-orchestration workflow " +
17
43
  "(projects, roadmap, dispatched agents, events).")
18
- .version("0.1.0");
44
+ .version(packageVersion());
19
45
  registerProjectCommands(program);
20
46
  registerRoadmapCommands(program);
21
47
  registerAgentCommands(program);
@@ -23,6 +49,7 @@ registerEventCommands(program);
23
49
  registerClerkCommands(program);
24
50
  registerCatchupCommand(program);
25
51
  registerDocsCommand(program);
52
+ registerInitCommand(program);
26
53
  program.exitOverride();
27
54
  try {
28
55
  await program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "@devwithdavid/ledger",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Personal agent-orchestration ledger: SQLite state store, CLI, and herdr watcher plugin.",
5
+ "license": "MIT",
6
+ "repository": "https://yggdrasil.thekartiks.com/chewbakartik/ledger.git",
5
7
  "type": "module",
6
8
  "bin": {
7
9
  "ledger": "dist/cli/index.js"
8
10
  },
9
11
  "files": [
12
+ "LICENSE",
10
13
  "dist",
11
14
  "LEDGER.md",
12
15
  "README.md",
13
- "herdr-plugin.toml"
16
+ "herdr-plugin.toml",
17
+ "skills"
14
18
  ],
15
19
  "scripts": {
16
20
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: ledger
3
+ description: Act as the first clerk for `ledger`, a personal SQLite-backed agent-orchestration tool (herdr + treehouse). Use when asked to register a ledger project, break work into a roadmap, dispatch a coding agent via ledger, check on dispatched agents, run a ledger catch-up, or otherwise manage state via the `ledger` CLI.
4
+ ---
5
+
6
+ Run `ledger docs` and read its full output before doing anything else in
7
+ this role — that prints the complete clerk/agent reference (CLI surface,
8
+ what the first clerk is responsible for, what a dispatched agent is told).
9
+
10
+ This skill is intentionally just a pointer, not a copy of that content, so
11
+ it can't drift out of sync with the real CLI, and carries no machine-
12
+ specific path — `ledger docs` resolves its own reference doc relative to
13
+ wherever the `ledger` package is actually installed on this machine.
14
+
15
+ If `ledger` isn't found on PATH, either it isn't installed here yet
16
+ (`npm install -g @devwithdavid/ledger`, then `ledger init` — see the
17
+ project's README for prerequisites), or it's installed but npm's global
18
+ bin directory isn't on PATH (`npm config get prefix`, then confirm
19
+ `<that prefix>/bin` is in `$PATH` — common after switching Node
20
+ versions/managers).