director-kit 0.0.0-stage → 0.2.1
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/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/bin/director-kit.mjs +156 -0
- package/bin/session-start.mjs +22 -0
- package/commands/audit.md +4 -0
- package/commands/board.md +4 -0
- package/commands/checkpoint.md +4 -0
- package/hooks/hooks.json +11 -0
- package/lib/audit.mjs +71 -0
- package/lib/board.mjs +101 -0
- package/lib/checkpoint.mjs +61 -0
- package/lib/config.mjs +42 -0
- package/lib/demo.mjs +37 -0
- package/lib/scan.mjs +125 -0
- package/package.json +14 -4
- package/skills/checkpoint-protocol/SKILL.md +37 -0
- package/skills/memory-floors/SKILL.md +25 -0
- package/skills/project-audit/SKILL.md +25 -0
- package/templates/CHECKPOINT.md +9 -0
- package/templates/MEMORY.md +9 -0
- package/templates/SOUL.md +25 -0
- package/templates/VISION.md +15 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "director-kit",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Vladislav Popov",
|
|
5
|
+
"url": "https://github.com/devladpopov"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "director-kit: CHECKPOINT protocol, memory floors and a cross-project board for Claude Code."
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "director-kit",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"description": "CHECKPOINT protocol, memory floors and a cross-project audit for Claude Code."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "director-kit",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "CHECKPOINT protocol, memory floors and a cross-project audit for Claude Code. Sessions resume where they stopped and never redo finished steps.",
|
|
5
|
+
"author": { "name": "Vladislav Popov", "url": "https://github.com/devladpopov" },
|
|
6
|
+
"homepage": "https://github.com/devladpopov/director-kit",
|
|
7
|
+
"repository": "https://github.com/devladpopov/director-kit",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["checkpoint", "memory", "audit", "dashboard"]
|
|
10
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladislav Popov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# director-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**One person, 214 projects, 3,790 commits in 30 days: the whole operating system is open.**
|
|
4
|
+
|
|
5
|
+
<sup>Counted by `director-kit board` on the author's machine on 23 Oct 2026; worktrees share history, so each commit is counted once. Run it on yours.</sup>
|
|
6
|
+
|
|
7
|
+
<!-- refresh both numbers and the date from a live `director-kit board` on release day, never type them by hand -->
|
|
8
|
+
|
|
9
|
+
director-kit is the core of how I run every project with Claude Code: one board over all of them, and a protocol that makes every session resume exactly where the last one stopped and never redo finished work.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx director-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Five minutes later you see a board of everything in your projects folder: what waits on you, what is active, what stalled, what is not backed up. It works on day zero, from git history alone. Nothing leaves your machine.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
70-second walkthrough on sample data ([mp4](docs/screencast/director-kit.mp4)). Still screenshot: [docs/board.png](docs/board.png).
|
|
20
|
+
|
|
21
|
+
## What is inside
|
|
22
|
+
|
|
23
|
+
| Part | What it does |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **Board** | `~/.director-kit/board.html`, one static file. Columns: waiting on you, active, stalled, paused, done, dormant. Every card: task, next step, blocker, days silent, commits, risks. |
|
|
26
|
+
| **Audit** | `director-kit audit`: markdown report. Waiting on you first, then stalled, then repos without a remote, unpushed or uncommitted work, memory indexes over their limit, stale backups. |
|
|
27
|
+
| **CHECKPOINT protocol** | `CHECKPOINT.md` in each project: STATUS, TASK, LAST_ACTION, NEXT, DO_NOT_REDO, VERIFY_BEFORE_ACT. A SessionStart hook shows it to Claude before it acts. After compaction or a crash the session continues instead of starting over. |
|
|
28
|
+
| **Memory floors** | Index under 6000 chars, topic files, archive. Never delete, move. Test memory before and after compressing it. |
|
|
29
|
+
| **Backup watch** | `director-kit backup add <folder>`: the audit flags any backup whose newest file is older than the limit (26 h by default). |
|
|
30
|
+
| **Templates** | `SOUL.md` (how the assistant behaves, what needs your yes), `VISION.md` (goal, criteria, constraints), `MEMORY.md`, `CHECKPOINT.md`. |
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Board and audit (Node 18+, git):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
npx director-kit # asks for your projects folder, builds the board, opens it
|
|
38
|
+
npx director-kit audit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Claude Code plugin (protocol, hook, `/director-kit:audit`, `/director-kit:board`, `/director-kit:checkpoint`):
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
claude plugin marketplace add devladpopov/director-kit
|
|
45
|
+
claude plugin install director-kit@director-kit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How status is decided
|
|
49
|
+
|
|
50
|
+
A project is any subfolder with `.git` or `CHECKPOINT.md`.
|
|
51
|
+
|
|
52
|
+
- With a CHECKPOINT: `STATUS` decides. `NEXT: await user input` puts it in **waiting on you**. In progress but silent longer than 7 days: **stalled**.
|
|
53
|
+
- Without one: commits or edits in the last 7 days: **active**; 7 to 30 days: **stalled**; older: **dormant**.
|
|
54
|
+
- Worktrees share history, so a commit is counted once.
|
|
55
|
+
|
|
56
|
+
Limits live in `~/.director-kit/config.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"root": "/home/me/Projects",
|
|
61
|
+
"activeDays": 7,
|
|
62
|
+
"dormantDays": 30,
|
|
63
|
+
"memoryIndexLimit": 6000,
|
|
64
|
+
"backups": [{ "name": "photos to NAS", "path": "/mnt/nas/photos", "maxAgeHours": 26 }]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
director-kit [root] first run: pick folder, build board, open it
|
|
72
|
+
director-kit board rebuild the board
|
|
73
|
+
director-kit audit markdown audit, saved to ~/.director-kit/audits/
|
|
74
|
+
director-kit checkpoint [dir] create CHECKPOINT.md from the template (never overwrites)
|
|
75
|
+
director-kit doctor check node, git, claude
|
|
76
|
+
director-kit demo board with sample data (~/.director-kit/demo/, for screenshots)
|
|
77
|
+
director-kit backup add <path> [--name=nas] [--max-age=26] watch a backup folder
|
|
78
|
+
director-kit backup list | rm <name> OK / STALE per folder
|
|
79
|
+
--no-open --json --private (no absolute paths) --root=<dir>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Privacy
|
|
83
|
+
|
|
84
|
+
The board is a local file. No telemetry, no network calls, no dependencies. `--private` strips absolute paths if you want to share a screenshot.
|
|
85
|
+
|
|
86
|
+
## Want it set up for you?
|
|
87
|
+
|
|
88
|
+
The full system also runs every project from Telegram topics with Claude, auto-resume and a director that audits all topics every 8 hours. That part is a done-for-you install: {INSTALL_LINK}.
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// director-kit: a board over all your projects in one command.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { loadConfig, saveConfig, guessRoot, isDir, HOME, CONFIG_PATH } from '../lib/config.mjs';
|
|
9
|
+
import { scanRoot } from '../lib/scan.mjs';
|
|
10
|
+
import { buildBoard, writeBoard } from '../lib/board.mjs';
|
|
11
|
+
import { renderAudit, backupFindings } from '../lib/audit.mjs';
|
|
12
|
+
import { checkpointTemplate } from '../lib/checkpoint.mjs';
|
|
13
|
+
import { demoScan } from '../lib/demo.mjs';
|
|
14
|
+
|
|
15
|
+
const KIT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
18
|
+
const pos = argv.filter((a) => !a.startsWith('--'));
|
|
19
|
+
const COMMANDS = ['init', 'board', 'audit', 'checkpoint', 'doctor', 'demo', 'backup', 'help'];
|
|
20
|
+
const cmd = pos[0] && (COMMANDS.includes(pos[0]) || !isDir(pos[0])) ? pos.shift() : 'init';
|
|
21
|
+
|
|
22
|
+
const HELP = `director-kit: one board over all your projects
|
|
23
|
+
|
|
24
|
+
director-kit [root] first run: pick projects folder, build board, open it
|
|
25
|
+
director-kit board rebuild board (~/.director-kit/board.html)
|
|
26
|
+
director-kit audit markdown audit: waiting on you, stalled, not backed up
|
|
27
|
+
director-kit checkpoint [dir] create CHECKPOINT.md from template (does not overwrite)
|
|
28
|
+
director-kit doctor check node, git, claude
|
|
29
|
+
director-kit demo board with sample data (~/.director-kit/demo/, for screenshots)
|
|
30
|
+
director-kit backup add <path> [--name=photos] [--max-age=26]
|
|
31
|
+
director-kit backup list | rm <name|path> folders the audit watches
|
|
32
|
+
|
|
33
|
+
--no-open do not open the browser --json print board json
|
|
34
|
+
--private no absolute paths in board --root=<dir> projects folder
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const has = (bin) => { try { execFileSync(bin, ['--version'], { stdio: 'ignore', timeout: 10000, shell: process.platform === 'win32' }); return true; } catch { return false; } };
|
|
38
|
+
const flagVal = (k) => { const a = argv.find((x) => x.startsWith(`--${k}=`)); return a ? a.split('=').slice(1).join('=') : null; };
|
|
39
|
+
|
|
40
|
+
function open(file) {
|
|
41
|
+
if (flags.has('--no-open') || process.env.CI) return;
|
|
42
|
+
const [c, a] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', file]] : process.platform === 'darwin' ? ['open', [file]] : ['xdg-open', [file]];
|
|
43
|
+
try { spawn(c, a, { stdio: 'ignore', detached: true }).unref(); } catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function build(cfg, scan, outDir = HOME) {
|
|
47
|
+
const board = buildBoard(scan || await scanRoot(cfg.root, cfg), { privatePaths: flags.has('--private') });
|
|
48
|
+
const out = writeBoard(board, outDir);
|
|
49
|
+
if (flags.has('--json')) console.log(JSON.stringify(board, null, 2));
|
|
50
|
+
const S = board.summary;
|
|
51
|
+
console.log(`${S.total} projects, ${S.commits30d} commits in 30 days | waiting on you ${S.waiting}, active ${S.active}, stalled ${S.stalled}, done ${S.done}, dormant ${S.dormant}, at risk ${S.atRisk}`);
|
|
52
|
+
console.log(`board: ${out.html}`);
|
|
53
|
+
return { board, out };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function ask(q, dflt) {
|
|
57
|
+
if (!process.stdin.isTTY || flags.has('--yes')) return dflt;
|
|
58
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
const a = (await rl.question(`${q} [${dflt}] `)).trim();
|
|
60
|
+
rl.close();
|
|
61
|
+
return a || dflt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function init() {
|
|
65
|
+
const cfg = loadConfig();
|
|
66
|
+
let root = flagVal('root') || pos[0] || cfg.root;
|
|
67
|
+
if (!root) root = await ask('Projects folder:', guessRoot());
|
|
68
|
+
root = path.resolve(root);
|
|
69
|
+
if (!isDir(root)) { console.error(`not a folder: ${root}`); process.exit(1); }
|
|
70
|
+
cfg.root = root;
|
|
71
|
+
const saved = saveConfig(cfg);
|
|
72
|
+
console.log(`config: ${saved}`);
|
|
73
|
+
const { out, board } = await build(cfg);
|
|
74
|
+
if (!board.summary.total) console.log(`no projects found in ${root}: a project is a subfolder with .git or CHECKPOINT.md`);
|
|
75
|
+
open(out.html);
|
|
76
|
+
if (has('claude')) {
|
|
77
|
+
console.log('\nClaude Code found. Add the protocol (CHECKPOINT, memory floors, /audit) to every session:');
|
|
78
|
+
console.log(' claude plugin marketplace add devladpopov/director-kit');
|
|
79
|
+
console.log(' claude plugin install director-kit@director-kit');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cmds = {
|
|
84
|
+
init,
|
|
85
|
+
async board() { const cfg = needRoot(); open((await build(cfg)).out.html); },
|
|
86
|
+
async audit() {
|
|
87
|
+
const cfg = needRoot();
|
|
88
|
+
const { board } = await build(cfg);
|
|
89
|
+
const { markdown } = renderAudit(board, cfg);
|
|
90
|
+
const dir = path.join(HOME, 'audits');
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
const f = path.join(dir, `audit-${new Date().toISOString().slice(0, 10)}.md`);
|
|
93
|
+
fs.writeFileSync(f, markdown);
|
|
94
|
+
console.log('\n' + markdown + `\nsaved: ${f}`);
|
|
95
|
+
},
|
|
96
|
+
checkpoint() {
|
|
97
|
+
const dir = path.resolve(pos[0] || process.cwd());
|
|
98
|
+
const f = path.join(dir, 'CHECKPOINT.md');
|
|
99
|
+
if (fs.existsSync(f)) { console.log(`exists, not touched: ${f}`); return; }
|
|
100
|
+
fs.writeFileSync(f, checkpointTemplate());
|
|
101
|
+
console.log(`created: ${f}`);
|
|
102
|
+
},
|
|
103
|
+
doctor() {
|
|
104
|
+
const v = process.versions.node.split('.').map(Number);
|
|
105
|
+
console.log(`node ${process.versions.node} ${v[0] >= 18 ? 'OK' : 'FAIL: need 18+'}`);
|
|
106
|
+
console.log(`git ${has('git') ? 'OK' : 'missing: projects without CHECKPOINT.md will not be detected'}`);
|
|
107
|
+
console.log(`claude ${has('claude') ? 'OK' : 'not found (optional: the board works without it)'}`);
|
|
108
|
+
const cfg = loadConfig();
|
|
109
|
+
console.log(`config ${fs.existsSync(CONFIG_PATH) ? CONFIG_PATH + ' root=' + cfg.root : 'none yet: run director-kit'}`);
|
|
110
|
+
console.log(`kit ${KIT}`);
|
|
111
|
+
},
|
|
112
|
+
// Separate folder: the demo must never overwrite the real board.
|
|
113
|
+
async demo() { const cfg = loadConfig(); open((await build(cfg, demoScan(), path.join(HOME, 'demo'))).out.html); },
|
|
114
|
+
backup,
|
|
115
|
+
help() { console.log(HELP); },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Backup watch: the audit flags a folder whose newest file is older than maxAgeHours.
|
|
119
|
+
function backup() {
|
|
120
|
+
const [sub = 'list', target] = pos;
|
|
121
|
+
const cfg = loadConfig();
|
|
122
|
+
const list = cfg.backups || [];
|
|
123
|
+
if (sub === 'add') {
|
|
124
|
+
if (!target) { console.error('usage: director-kit backup add <path> [--name=...] [--max-age=hours]'); process.exit(1); }
|
|
125
|
+
const p = path.resolve(target);
|
|
126
|
+
if (!fs.existsSync(p)) console.log(`note: ${p} does not exist yet, the audit will report it as STALE until it does`);
|
|
127
|
+
const maxAgeHours = Number(flagVal('max-age') || 26);
|
|
128
|
+
if (!(maxAgeHours > 0)) { console.error('--max-age must be a number of hours'); process.exit(1); }
|
|
129
|
+
const entry = { name: flagVal('name') || path.basename(p), path: p, maxAgeHours };
|
|
130
|
+
const i = list.findIndex((b) => path.resolve(b.path) === p);
|
|
131
|
+
if (i >= 0) list[i] = entry; else list.push(entry);
|
|
132
|
+
cfg.backups = list;
|
|
133
|
+
saveConfig(cfg);
|
|
134
|
+
console.log(`${i >= 0 ? 'updated' : 'added'}: ${entry.name} (${p}), limit ${maxAgeHours}h`);
|
|
135
|
+
} else if (sub === 'rm' || sub === 'remove') {
|
|
136
|
+
const keep = list.filter((b) => b.name !== target && path.resolve(b.path) !== path.resolve(target || ''));
|
|
137
|
+
if (!target || keep.length === list.length) { console.error(`no backup named or at: ${target}`); process.exit(1); }
|
|
138
|
+
cfg.backups = keep;
|
|
139
|
+
saveConfig(cfg);
|
|
140
|
+
console.log(`removed: ${target}`);
|
|
141
|
+
} else if (sub === 'list') {
|
|
142
|
+
if (!list.length) { console.log('no backups watched yet: director-kit backup add <path>'); return; }
|
|
143
|
+
for (const b of backupFindings(list)) console.log(`${b.ok ? 'OK ' : 'STALE'} ${b.name}: ${b.ageHours == null ? 'nothing found' : b.ageHours + 'h old'} (limit ${b.maxAgeHours}h) ${b.path}`);
|
|
144
|
+
} else { console.error(`unknown: backup ${sub}`); process.exit(1); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function needRoot() {
|
|
148
|
+
const cfg = loadConfig();
|
|
149
|
+
if (flagVal('root')) cfg.root = path.resolve(flagVal('root'));
|
|
150
|
+
if (!cfg.root) { console.error('no projects folder yet: run "director-kit" first'); process.exit(1); }
|
|
151
|
+
return cfg;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (flags.has('--help') || cmd === '-h') cmds.help();
|
|
155
|
+
else if (!cmds[cmd]) { console.error(`unknown command: ${cmd}\n`); cmds.help(); process.exit(1); }
|
|
156
|
+
else await cmds[cmd]();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook: puts the project's CHECKPOINT.md in front of the model before it does anything.
|
|
3
|
+
// stdout of a SessionStart hook is added to the session context.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readCheckpoint, statusOf } from '../lib/checkpoint.mjs';
|
|
6
|
+
|
|
7
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
8
|
+
const cp = readCheckpoint(dir);
|
|
9
|
+
|
|
10
|
+
if (!cp) {
|
|
11
|
+
console.log('[director-kit] No CHECKPOINT.md in this project. After the first significant task, write one (skill: checkpoint-protocol).');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const f = cp.fields;
|
|
16
|
+
const st = statusOf(f.STATUS);
|
|
17
|
+
const L = [`[director-kit] CHECKPOINT.md (${path.basename(dir)}), read before acting:`];
|
|
18
|
+
for (const k of ['STATUS', 'TASK', 'DATE', 'LAST_ACTION', 'NEXT', 'BLOCKER', 'VERIFY_BEFORE_ACT']) if (f[k]) L.push(`${k}: ${f[k]}`);
|
|
19
|
+
if (cp.lists.DO_NOT_REDO?.length) L.push('DO_NOT_REDO:', ...cp.lists.DO_NOT_REDO.map((x) => `- ${x}`));
|
|
20
|
+
if (st === 'done') L.push('The task above is COMPLETED. Do not repeat it; wait for a new instruction.');
|
|
21
|
+
else L.push('Continue from NEXT. Before any deploy, migration or delete, run VERIFY_BEFORE_ACT; skip steps that are already done.');
|
|
22
|
+
console.log(L.join('\n'));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rebuild the director-kit board over all projects and open it
|
|
3
|
+
---
|
|
4
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/bin/director-kit.mjs" board` (fallback: `npx -y director-kit board`). Reply with the one summary line it prints and the path to board.html. Nothing else.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Write or refresh CHECKPOINT.md for the current project from what was actually done
|
|
3
|
+
---
|
|
4
|
+
Use the checkpoint-protocol skill. If CHECKPOINT.md exists, rewrite it to the current state (new LAST_ACTION, NEXT, DO_NOT_REDO). If it does not, create it. Fill every field from real evidence in this session or the repo (git log, files); no placeholders, no secrets. Show the result.
|
package/hooks/hooks.json
ADDED
package/lib/audit.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Audit: one markdown report over all projects. What needs you, what is stuck,
|
|
2
|
+
// what is not backed up, which memory indexes outgrew their floor.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const INDEX_FILES = ['CLAUDE.md', 'MEMORY.md', 'AGENTS.md'];
|
|
7
|
+
|
|
8
|
+
export function memoryFindings(projects, limit) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const p of projects) {
|
|
11
|
+
if (!p.path) continue;
|
|
12
|
+
for (const f of INDEX_FILES) {
|
|
13
|
+
try {
|
|
14
|
+
const n = fs.readFileSync(path.join(p.path, f), 'utf8').length;
|
|
15
|
+
if (n > limit) out.push({ project: p.name, file: f, chars: n });
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Newest file under a folder, depth-limited so a huge backup tree stays cheap.
|
|
23
|
+
export function newestFile(dir, depth = 3) {
|
|
24
|
+
let best = null;
|
|
25
|
+
const walk = (d, left) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
const p = path.join(d, e.name);
|
|
30
|
+
if (e.isDirectory()) { if (left > 0) walk(p, left - 1); continue; }
|
|
31
|
+
try { const m = fs.statSync(p).mtimeMs; if (!best || m > best.mtime) best = { path: p, mtime: m }; } catch {}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
try { if (fs.statSync(dir).isFile()) return { path: dir, mtime: fs.statSync(dir).mtimeMs }; } catch { return null; }
|
|
35
|
+
walk(dir, depth);
|
|
36
|
+
return best;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function backupFindings(backups, now = Date.now()) {
|
|
40
|
+
return (backups || []).map((b) => {
|
|
41
|
+
const f = newestFile(b.path);
|
|
42
|
+
const ageH = f ? (now - f.mtime) / 36e5 : null;
|
|
43
|
+
const max = b.maxAgeHours || 26;
|
|
44
|
+
return { name: b.name || b.path, path: b.path, ageHours: ageH == null ? null : Math.round(ageH), maxAgeHours: max, ok: ageH != null && ageH <= max };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function renderAudit(board, cfg, now = Date.now()) {
|
|
49
|
+
const P = board.projects;
|
|
50
|
+
const S = board.summary;
|
|
51
|
+
const L = [];
|
|
52
|
+
const line = (p, extra) => `- **${p.name}**${extra ? `: ${extra}` : ''}`;
|
|
53
|
+
L.push(`# Audit ${new Date(now).toISOString().slice(0, 16).replace('T', ' ')} UTC`, '');
|
|
54
|
+
L.push(`${S.total} projects, ${S.commits30d} commits in 30 days. Waiting on you ${S.waiting}, active ${S.active}, stalled ${S.stalled}, done ${S.done}, dormant ${S.dormant}.`, '');
|
|
55
|
+
|
|
56
|
+
const sect = (title, items) => { if (items.length) L.push(`## ${title} (${items.length})`, ...items, ''); };
|
|
57
|
+
sect('Waiting on you', P.filter((p) => p.status === 'waiting').map((p) => line(p, p.blocker || p.next || p.task)));
|
|
58
|
+
sect('Stalled', P.filter((p) => p.status === 'stalled').sort((a, b) => b.idleDays - a.idleDays)
|
|
59
|
+
.map((p) => line(p, `silent ${p.idleDays}d. ${p.next ? 'Next was: ' + p.next : p.task || ''}`.trim())));
|
|
60
|
+
sect('Not safe (no remote, unpushed or uncommitted work)', P.filter((p) => p.risks.length && p.status !== 'dormant').map((p) => line(p, p.risks.join(', '))));
|
|
61
|
+
sect('Active without a CHECKPOINT.md', P.filter((p) => !p.hasCheckpoint && ['active', 'stalled'].includes(p.status)).map((p) => line(p, `run: director-kit checkpoint "${p.path || p.name}"`)));
|
|
62
|
+
|
|
63
|
+
const mem = memoryFindings(P, cfg.memoryIndexLimit);
|
|
64
|
+
sect(`Memory indexes over ${cfg.memoryIndexLimit} chars (move details to topic files)`, mem.map((m) => `- **${m.project}/${m.file}**: ${m.chars} chars`));
|
|
65
|
+
|
|
66
|
+
const bk = backupFindings(cfg.backups, now);
|
|
67
|
+
sect('Backups', bk.map((b) => `- ${b.ok ? 'OK' : 'STALE'} **${b.name}**: ${b.ageHours == null ? 'nothing found at ' + b.path : `newest file ${b.ageHours}h old`} (limit ${b.maxAgeHours}h)`));
|
|
68
|
+
if (!bk.length) L.push('## Backups', '- not configured: add folders to "backups" in ~/.director-kit/config.json, see README', '');
|
|
69
|
+
|
|
70
|
+
return { markdown: L.join('\n'), memory: mem, backups: bk };
|
|
71
|
+
}
|
package/lib/board.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// board.json + a single self-contained board.html (no server, no CDN, opens from disk).
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const COLUMNS = [
|
|
6
|
+
{ id: 'waiting', label: 'Waiting on you' },
|
|
7
|
+
{ id: 'active', label: 'Active' },
|
|
8
|
+
{ id: 'stalled', label: 'Stalled' },
|
|
9
|
+
{ id: 'paused', label: 'Paused' },
|
|
10
|
+
{ id: 'done', label: 'Done' },
|
|
11
|
+
{ id: 'dormant', label: 'Dormant' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function buildBoard(scan, { privatePaths = false } = {}) {
|
|
15
|
+
const unique = new Set();
|
|
16
|
+
for (const p of scan.projects) for (const h of p.git?.hashes || []) unique.add(h);
|
|
17
|
+
const projects = scan.projects.map((p) => {
|
|
18
|
+
const { hashes, ...g } = p.git || {};
|
|
19
|
+
return { ...p, git: p.git ? g : null, path: privatePaths ? undefined : p.path };
|
|
20
|
+
});
|
|
21
|
+
const count = (s) => projects.filter((p) => p.status === s).length;
|
|
22
|
+
const summary = Object.fromEntries(COLUMNS.map((c) => [c.id, count(c.id)]));
|
|
23
|
+
summary.total = projects.length;
|
|
24
|
+
// Worktrees share history, count each commit once. Data without hashes (demo) falls back to the sum.
|
|
25
|
+
summary.commits30d = unique.size || projects.reduce((n, p) => n + (p.git?.commits30d || 0), 0);
|
|
26
|
+
summary.withCheckpoint = projects.filter((p) => p.hasCheckpoint).length;
|
|
27
|
+
summary.atRisk = projects.filter((p) => p.risks.length).length;
|
|
28
|
+
return {
|
|
29
|
+
generatedAt: new Date().toISOString(),
|
|
30
|
+
root: privatePaths ? path.basename(scan.root) : scan.root,
|
|
31
|
+
summary,
|
|
32
|
+
columns: COLUMNS,
|
|
33
|
+
projects,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeBoard(board, outDir) {
|
|
38
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
39
|
+
const json = path.join(outDir, 'board.json');
|
|
40
|
+
const html = path.join(outDir, 'board.html');
|
|
41
|
+
fs.writeFileSync(json, JSON.stringify(board, null, 2));
|
|
42
|
+
fs.writeFileSync(html, renderHtml(board));
|
|
43
|
+
return { json, html };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderHtml(board) {
|
|
47
|
+
const data = JSON.stringify(board).replace(/</g, '\\u003c');
|
|
48
|
+
return `<!doctype html>
|
|
49
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
50
|
+
<title>Board: ${board.summary.total} projects</title>
|
|
51
|
+
<style>
|
|
52
|
+
:root{--bg:#f6f7f9;--card:#fff;--ink:#16181d;--mute:#6b7280;--line:#e5e7eb;--warn:#b45309;--bad:#b91c1c;--ok:#15803d;--acc:#2563eb}
|
|
53
|
+
@media (prefers-color-scheme:dark){:root{--bg:#0f1115;--card:#181b21;--ink:#e6e8ec;--mute:#9aa1ac;--line:#2a2f38;--warn:#f59e0b;--bad:#f87171;--ok:#4ade80;--acc:#60a5fa}}
|
|
54
|
+
*{box-sizing:border-box}body{margin:0;font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--ink)}
|
|
55
|
+
header{padding:20px 24px 8px;display:flex;flex-wrap:wrap;gap:16px;align-items:baseline}
|
|
56
|
+
h1{font-size:22px;margin:0}.stat{color:var(--mute)}.stat b{color:var(--ink);font-size:18px}
|
|
57
|
+
.tools{padding:0 24px 12px;display:flex;gap:8px}input{flex:1;max-width:360px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--ink)}
|
|
58
|
+
.cols{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(260px,1fr);gap:12px;padding:0 24px 24px;overflow-x:auto}
|
|
59
|
+
.col h2{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--mute);margin:4px 0 8px}
|
|
60
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px}
|
|
61
|
+
.card h3{font-size:14px;margin:0 0 4px;word-break:break-word}.t{color:var(--mute);font-size:12.5px;margin:2px 0}
|
|
62
|
+
.meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;font-size:12px;color:var(--mute)}
|
|
63
|
+
.pill{border:1px solid var(--line);border-radius:999px;padding:0 7px}.risk{color:var(--bad);border-color:currentColor}.blk{color:var(--warn)}
|
|
64
|
+
.bar{height:4px;background:var(--line);border-radius:2px;margin-top:6px}.bar i{display:block;height:4px;background:var(--acc);border-radius:2px}
|
|
65
|
+
footer{padding:0 24px 24px;color:var(--mute);font-size:12px}
|
|
66
|
+
</style></head><body>
|
|
67
|
+
<header><h1>Board</h1><span class="stat" id="stats"></span></header>
|
|
68
|
+
<div class="tools"><input id="q" placeholder="Filter projects" autofocus></div>
|
|
69
|
+
<div class="cols" id="cols"></div>
|
|
70
|
+
<footer id="foot"></footer>
|
|
71
|
+
<script id="data" type="application/json">${data}</script>
|
|
72
|
+
<script>
|
|
73
|
+
const B=JSON.parse(document.getElementById('data').textContent);
|
|
74
|
+
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
75
|
+
const S=B.summary;
|
|
76
|
+
document.getElementById('stats').innerHTML=[['projects',S.total],['commits in 30 days',S.commits30d],['waiting on you',S.waiting],['stalled',S.stalled],['at risk',S.atRisk]].map(([k,v])=>'<b>'+v+'</b> '+k).join(' · ');
|
|
77
|
+
document.getElementById('foot').textContent='Generated '+new Date(B.generatedAt).toLocaleString()+' from '+B.root+' by director-kit. '+S.withCheckpoint+' of '+S.total+' projects keep a CHECKPOINT.md.';
|
|
78
|
+
function card(p){
|
|
79
|
+
const g=p.git||{};const idle=p.idleDays==null?'':(p.idleDays===0?'today':p.idleDays+'d ago');
|
|
80
|
+
return '<div class="card" data-q="'+esc((p.name+' '+(p.title||'')+' '+(p.task||'')+' '+(p.next||'')).toLowerCase())+'">'
|
|
81
|
+
+'<h3>'+esc(p.name)+'</h3>'
|
|
82
|
+
+(p.task?'<div class="t">'+esc(p.task)+'</div>':(p.title&&p.title!==p.name?'<div class="t">'+esc(p.title)+'</div>':''))
|
|
83
|
+
+(p.next?'<div class="t"><b>Next:</b> '+esc(p.next)+'</div>':(p.lastAction?'<div class="t">Last: '+esc(p.lastAction)+'</div>':''))
|
|
84
|
+
+(p.blocker?'<div class="t blk"><b>Blocker:</b> '+esc(p.blocker)+'</div>':'')
|
|
85
|
+
+(p.progress!=null?'<div class="bar"><i style="width:'+p.progress+'%"></i></div>':'')
|
|
86
|
+
+'<div class="meta">'+(idle?'<span class="pill">'+idle+'</span>':'')+(g.commits30d?'<span class="pill">'+g.commits30d+' commits/30d</span>':'')
|
|
87
|
+
+(p.hasCheckpoint?'':'<span class="pill">no checkpoint</span>')+p.risks.map(r=>'<span class="pill risk">'+esc(r)+'</span>').join('')+'</div></div>';
|
|
88
|
+
}
|
|
89
|
+
function render(q){
|
|
90
|
+
q=(q||'').toLowerCase();
|
|
91
|
+
document.getElementById('cols').innerHTML=B.columns.map(c=>{
|
|
92
|
+
const ps=B.projects.filter(p=>p.status===c.id).sort((a,b)=>(a.idleDays??1e9)-(b.idleDays??1e9));
|
|
93
|
+
if(!ps.length)return '';
|
|
94
|
+
return '<section class="col"><h2>'+esc(c.label)+' ('+ps.length+')</h2>'+ps.map(card).join('')+'</section>';
|
|
95
|
+
}).join('');
|
|
96
|
+
if(q)document.querySelectorAll('.card').forEach(el=>{el.style.display=el.dataset.q.includes(q)?'':'none'});
|
|
97
|
+
}
|
|
98
|
+
document.getElementById('q').addEventListener('input',e=>render(e.target.value));render();
|
|
99
|
+
</script></body></html>
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// CHECKPOINT.md protocol: plain "KEY: value" lines, first occurrence wins.
|
|
2
|
+
// List fields (DO_NOT_REDO) collect the "- item" lines that follow them.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const FIELDS = ['STATUS', 'TASK', 'DATE', 'LAST_ACTION', 'NEXT', 'BLOCKER', 'PROGRESS', 'STAGE', 'GOAL', 'METRIC', 'VERIFY_BEFORE_ACT'];
|
|
7
|
+
const LIST_FIELDS = new Set(['DO_NOT_REDO']);
|
|
8
|
+
|
|
9
|
+
export function parseCheckpoint(text) {
|
|
10
|
+
const fields = {};
|
|
11
|
+
const lists = {};
|
|
12
|
+
let list = null;
|
|
13
|
+
for (const raw of String(text).replace(/^/, '').split(/\r?\n/)) {
|
|
14
|
+
const m = /^([A-Z][A-Z_]{1,24}):[ \t]*(.*)$/.exec(raw);
|
|
15
|
+
if (m) {
|
|
16
|
+
list = LIST_FIELDS.has(m[1]) ? (lists[m[1]] ||= []) : null;
|
|
17
|
+
if (!(m[1] in fields)) fields[m[1]] = m[2].trim();
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const item = /^\s*[-*]\s+(.+)$/.exec(raw);
|
|
21
|
+
if (list && item) list.push(item[1].trim());
|
|
22
|
+
else if (raw.trim()) list = null;
|
|
23
|
+
}
|
|
24
|
+
return { fields, lists };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readCheckpoint(dir) {
|
|
28
|
+
const p = path.join(dir, 'CHECKPOINT.md');
|
|
29
|
+
try {
|
|
30
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
31
|
+
return { path: p, mtime: fs.statSync(p).mtimeMs, ...parseCheckpoint(text) };
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Normalised status: done | waiting | active | paused | unknown
|
|
38
|
+
export function statusOf(raw) {
|
|
39
|
+
const s = String(raw || '').trim().split(/[\s(|]/)[0].toUpperCase();
|
|
40
|
+
if (!s) return 'unknown';
|
|
41
|
+
if (/^(COMPLETED|DONE|SHIPPED|CLOSED)$/.test(s)) return 'done';
|
|
42
|
+
if (/^(AWAITING|WAITING|BLOCKED|NEEDS)/.test(s)) return 'waiting';
|
|
43
|
+
if (/^(IN_PROGRESS|ACTIVE|WIP|STARTED)$/.test(s)) return 'active';
|
|
44
|
+
if (/^(PAUSED|DEFERRED|IDLE|ON_HOLD|NOT_STARTED)$/.test(s)) return 'paused';
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function checkpointTemplate({ task = '<what this project is doing right now>', date = new Date().toISOString() } = {}) {
|
|
49
|
+
return [
|
|
50
|
+
'STATUS: IN_PROGRESS',
|
|
51
|
+
`TASK: ${task}`,
|
|
52
|
+
`DATE: ${date}`,
|
|
53
|
+
'LAST_ACTION: <the last thing that was actually done>',
|
|
54
|
+
'NEXT: <the next concrete step, or "await user input">',
|
|
55
|
+
'BLOCKER: none',
|
|
56
|
+
'DO_NOT_REDO:',
|
|
57
|
+
'- <idempotent steps that are already done and must not be repeated>',
|
|
58
|
+
'VERIFY_BEFORE_ACT: <commands that check the current state before deploy/migration/delete>',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const HOME = process.env.DIRECTOR_KIT_HOME || path.join(os.homedir(), '.director-kit');
|
|
6
|
+
const CONFIG = path.join(HOME, 'config.json');
|
|
7
|
+
|
|
8
|
+
export const DEFAULTS = {
|
|
9
|
+
root: null,
|
|
10
|
+
activeDays: 7, // no commits / checkpoint edits for longer than this: stalled
|
|
11
|
+
dormantDays: 30, // longer than this without a checkpoint: dormant
|
|
12
|
+
memoryIndexLimit: 6000, // chars, the "index floor" of the memory floors
|
|
13
|
+
backups: [], // [{ name, path, maxAgeHours }]
|
|
14
|
+
skip: ['node_modules', '.git', '.cache', '.tmp', 'tmp'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function loadConfig() {
|
|
18
|
+
try {
|
|
19
|
+
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG, 'utf8').replace(/^/, '')) };
|
|
20
|
+
} catch {
|
|
21
|
+
return { ...DEFAULTS };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function saveConfig(cfg) {
|
|
26
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
27
|
+
fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2) + '\n');
|
|
28
|
+
return CONFIG;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// First existing folder that looks like a projects root.
|
|
32
|
+
export function guessRoot(cwd = process.cwd()) {
|
|
33
|
+
const h = os.homedir();
|
|
34
|
+
const candidates = ['Projects', 'projects', 'code', 'Code', 'dev', 'src', 'repos', 'work', 'Documents/GitHub', 'GitHub']
|
|
35
|
+
.map((d) => path.join(h, d));
|
|
36
|
+
for (const c of candidates) if (isDir(c) && subdirs(c).length) return c;
|
|
37
|
+
return cwd;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
41
|
+
export const subdirs = (p) => { try { return fs.readdirSync(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); } catch { return []; } };
|
|
42
|
+
export { CONFIG as CONFIG_PATH };
|
package/lib/demo.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Sample data for screenshots and the screencast: no real names, paths or people.
|
|
2
|
+
const DAY = 864e5;
|
|
3
|
+
const SAMPLE = [
|
|
4
|
+
['landing-page', 'waiting', 0, 14, 'Publish the pricing page', 'Approve the copy for the hero block', 'you: approve copy'],
|
|
5
|
+
['invoice-bot', 'active', 0, 31, 'Telegram bot that drafts invoices', 'Add VAT field to the PDF', null],
|
|
6
|
+
['podcast-clips', 'active', 1, 22, 'Cut 5 clips per episode automatically', 'Tune the silence threshold', null],
|
|
7
|
+
['crm-lite', 'active', 2, 18, 'Own CRM instead of a SaaS seat', 'Import contacts from CSV', null],
|
|
8
|
+
['newsletter', 'waiting', 1, 3, 'Weekly newsletter #12', 'Pick the cover image', 'you: choose image'],
|
|
9
|
+
['tax-2026', 'stalled', 9, 0, 'Self-assessment return', 'Download bank statements', null],
|
|
10
|
+
['course-ai-basics', 'stalled', 12, 2, 'Module 3: agents', 'Record lesson 3.2', null],
|
|
11
|
+
['scraper-jobs', 'active', 0, 40, 'Daily job board digest', 'Deduplicate by company', null],
|
|
12
|
+
['home-server', 'done', 3, 5, 'Backups to object storage', null, null],
|
|
13
|
+
['portfolio-site', 'done', 6, 9, 'Move to static hosting', null, null],
|
|
14
|
+
['family-photos', 'paused', 20, 0, 'Sort 2019 archive', 'Resume after holidays', null],
|
|
15
|
+
['old-game-jam', 'dormant', 140, 0, null, null, null],
|
|
16
|
+
['dotfiles', 'dormant', 61, 0, null, null, null],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function demoScan(now = Date.now()) {
|
|
20
|
+
const projects = SAMPLE.map(([name, status, idle, commits, task, next, blocker], i) => ({
|
|
21
|
+
name,
|
|
22
|
+
title: null,
|
|
23
|
+
path: `~/Projects/${name}`,
|
|
24
|
+
status,
|
|
25
|
+
hasCheckpoint: !!task,
|
|
26
|
+
task,
|
|
27
|
+
next,
|
|
28
|
+
lastAction: task ? 'see CHECKPOINT.md' : 'initial commit',
|
|
29
|
+
blocker,
|
|
30
|
+
progress: status === 'done' ? 100 : task ? (i * 17) % 90 + 5 : null,
|
|
31
|
+
lastActivity: new Date(now - idle * DAY).toISOString(),
|
|
32
|
+
idleDays: idle,
|
|
33
|
+
git: { branch: 'main', commits30d: commits, dirtyFiles: 0, hasRemote: name !== 'tax-2026', unpushed: name === 'crm-lite' ? 3 : 0 },
|
|
34
|
+
risks: name === 'tax-2026' ? ['no remote: not backed up'] : name === 'crm-lite' ? ['3 unpushed commits'] : [],
|
|
35
|
+
}));
|
|
36
|
+
return { root: '~/Projects (demo)', folders: projects.length, projects };
|
|
37
|
+
}
|
package/lib/scan.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Walks a projects root: every subfolder with a CHECKPOINT.md or a .git is a project.
|
|
2
|
+
// Works on day zero: without checkpoints the status comes from git activity.
|
|
3
|
+
// Git is the slow part, so projects are scanned in parallel and each needs only 4 git calls.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { readCheckpoint, statusOf } from './checkpoint.mjs';
|
|
9
|
+
import { isDir, subdirs } from './config.mjs';
|
|
10
|
+
|
|
11
|
+
const DAY = 864e5;
|
|
12
|
+
|
|
13
|
+
const git = (dir, args) => new Promise((resolve) => {
|
|
14
|
+
execFile('git', ['-C', dir, ...args], { encoding: 'utf8', timeout: 15000, maxBuffer: 64 << 20, windowsHide: true },
|
|
15
|
+
(err, out) => resolve(err ? null : out.trim()));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function gitInfo(dir) {
|
|
19
|
+
if (!fs.existsSync(path.join(dir, '.git'))) return null;
|
|
20
|
+
const [last, recent, status, remotes] = await Promise.all([
|
|
21
|
+
git(dir, ['log', '-1', '--format=%cI%x09%s']),
|
|
22
|
+
git(dir, ['log', '--no-merges', '--since=30.days', '--format=%H']),
|
|
23
|
+
git(dir, ['status', '--porcelain=v2', '--branch']), // branch, ahead of upstream and dirty files in one call
|
|
24
|
+
git(dir, ['remote']),
|
|
25
|
+
]);
|
|
26
|
+
const hashes = recent ? recent.split('\n').filter(Boolean) : [];
|
|
27
|
+
const lines = status ? status.split('\n').filter(Boolean) : [];
|
|
28
|
+
const header = (k) => lines.find((l) => l.startsWith(`# branch.${k} `))?.slice(`# branch.${k} `.length) ?? null;
|
|
29
|
+
const ab = /^\+(\d+) -\d+$/.exec(header('ab') || '');
|
|
30
|
+
const [lastDate, lastSubject] = last ? last.split('\t') : [null, null];
|
|
31
|
+
return {
|
|
32
|
+
branch: header('head') === '(detached)' ? 'HEAD' : header('head'),
|
|
33
|
+
lastCommit: lastDate || null,
|
|
34
|
+
lastSubject: lastSubject || null,
|
|
35
|
+
commits30d: hashes.length,
|
|
36
|
+
worktree: fs.statSync(path.join(dir, '.git')).isFile(), // linked worktree or submodule
|
|
37
|
+
hashes, // dropped from the board; counts unique commits across worktrees
|
|
38
|
+
dirtyFiles: lines.filter((l) => !l.startsWith('#')).length,
|
|
39
|
+
hasRemote: !!(remotes && remotes.length),
|
|
40
|
+
unpushed: ab ? Number(ab[1]) : null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function firstHeading(dir) {
|
|
45
|
+
for (const f of ['README.md', 'readme.md', 'README']) {
|
|
46
|
+
try {
|
|
47
|
+
const m = /^#\s+(.+)$/m.exec(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
48
|
+
if (m) return m[1].trim();
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function scanProject(dir, cfg, now = Date.now()) {
|
|
55
|
+
const cp = readCheckpoint(dir);
|
|
56
|
+
const g = await gitInfo(dir);
|
|
57
|
+
if (!cp && !g) return null;
|
|
58
|
+
|
|
59
|
+
const times = [cp?.mtime, g?.lastCommit && Date.parse(g.lastCommit)].filter(Boolean);
|
|
60
|
+
if (g?.dirtyFiles) times.push(newestMtime(dir));
|
|
61
|
+
const lastActivity = times.length ? Math.max(...times) : null;
|
|
62
|
+
const idleDays = lastActivity ? Math.floor((now - lastActivity) / DAY) : null;
|
|
63
|
+
|
|
64
|
+
let status = cp ? statusOf(cp.fields.STATUS) : 'unknown';
|
|
65
|
+
// "NEXT: await user input" means the ball is with you, not that the project is stuck.
|
|
66
|
+
if (cp && status !== 'done' && /^(await|waiting for|blocked on)\b/i.test(cp.fields.NEXT || '')) status = 'waiting';
|
|
67
|
+
if (status === 'unknown') status = idleDays == null || idleDays > cfg.dormantDays ? 'dormant' : 'active';
|
|
68
|
+
if (status === 'active' && idleDays != null && idleDays > cfg.activeDays) status = idleDays > cfg.dormantDays && !cp ? 'dormant' : 'stalled';
|
|
69
|
+
|
|
70
|
+
const risks = [];
|
|
71
|
+
if (g && !g.hasRemote) risks.push('no remote: not backed up');
|
|
72
|
+
if (g?.unpushed) risks.push(`${g.unpushed} unpushed commit${g.unpushed > 1 ? 's' : ''}`);
|
|
73
|
+
if (g?.dirtyFiles && idleDays != null && idleDays > 2) risks.push(`${g.dirtyFiles} uncommitted file${g.dirtyFiles > 1 ? 's' : ''} for ${idleDays}d`);
|
|
74
|
+
|
|
75
|
+
const f = cp?.fields || {};
|
|
76
|
+
return {
|
|
77
|
+
name: path.basename(dir),
|
|
78
|
+
title: firstHeading(dir),
|
|
79
|
+
path: dir,
|
|
80
|
+
status,
|
|
81
|
+
hasCheckpoint: !!cp,
|
|
82
|
+
task: f.TASK || null,
|
|
83
|
+
next: f.NEXT || null,
|
|
84
|
+
lastAction: f.LAST_ACTION || g?.lastSubject || null,
|
|
85
|
+
blocker: f.BLOCKER && !/^(none|no|-|n\/a)$/i.test(f.BLOCKER) ? f.BLOCKER : null,
|
|
86
|
+
progress: /^\d{1,3}/.test(f.PROGRESS || '') ? Math.min(100, parseInt(f.PROGRESS, 10)) : null,
|
|
87
|
+
lastActivity: lastActivity ? new Date(lastActivity).toISOString() : null,
|
|
88
|
+
idleDays,
|
|
89
|
+
git: g,
|
|
90
|
+
risks,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Newest mtime among top-level files (cheap proxy for "someone is editing right now").
|
|
95
|
+
function newestMtime(dir) {
|
|
96
|
+
let max = 0;
|
|
97
|
+
for (const f of safeList(dir)) {
|
|
98
|
+
if (f.startsWith('.')) continue;
|
|
99
|
+
try { max = Math.max(max, fs.statSync(path.join(dir, f)).mtimeMs); } catch {}
|
|
100
|
+
}
|
|
101
|
+
return max || null;
|
|
102
|
+
}
|
|
103
|
+
const safeList = (d) => { try { return fs.readdirSync(d); } catch { return []; } };
|
|
104
|
+
|
|
105
|
+
// Runs fn over items with at most `limit` in flight, keeps order.
|
|
106
|
+
async function pool(items, limit, fn) {
|
|
107
|
+
const out = new Array(items.length);
|
|
108
|
+
let next = 0;
|
|
109
|
+
const worker = async () => { while (next < items.length) { const i = next++; out[i] = await fn(items[i]); } };
|
|
110
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function scanRoot(root, cfg, now = Date.now()) {
|
|
115
|
+
const skip = new Set(cfg.skip || []);
|
|
116
|
+
const dirs = subdirs(root).filter((name) => !skip.has(name) && !name.startsWith('.')).map((name) => path.join(root, name));
|
|
117
|
+
const limit = cfg.concurrency || Math.max(4, Math.min(16, (os.availableParallelism?.() || os.cpus().length) * 2));
|
|
118
|
+
const projects = (await pool(dirs, limit, (d) => scanProject(d, cfg, now))).filter(Boolean);
|
|
119
|
+
// A root that is itself a single repo.
|
|
120
|
+
if (!projects.length && isDir(root)) {
|
|
121
|
+
const p = await scanProject(root, cfg, now);
|
|
122
|
+
if (p) projects.push(p);
|
|
123
|
+
}
|
|
124
|
+
return { root, folders: dirs.length, projects };
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "director-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "One board over all your projects, plus the CHECKPOINT and memory protocol that keeps Claude Code sessions from redoing work.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "director-kit": "bin/director-kit.mjs" },
|
|
7
|
+
"files": ["bin", "lib", "templates", "skills", "commands", "hooks", ".claude-plugin"],
|
|
8
|
+
"scripts": { "test": "node test/smoke.mjs" },
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"keywords": ["claude-code", "claude-code-plugin", "dashboard", "kanban", "projects", "ai-agents"],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Vladislav Popov (https://github.com/devladpopov)",
|
|
13
|
+
"homepage": "https://github.com/devladpopov/director-kit",
|
|
14
|
+
"repository": { "type": "git", "url": "git+https://github.com/devladpopov/director-kit.git" },
|
|
15
|
+
"bugs": "https://github.com/devladpopov/director-kit/issues"
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checkpoint-protocol
|
|
3
|
+
description: Keep CHECKPOINT.md in the project root so any new session, compaction or crash resumes exactly where work stopped and never repeats finished steps. Use after finishing a significant task, before a risky action, after context compaction or "continue where you left off", and when the user asks what state a project is in.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CHECKPOINT protocol
|
|
7
|
+
|
|
8
|
+
One file, `CHECKPOINT.md` in the project root, plain `KEY: value` lines. It is the single source of truth about where the work is. The director-kit board and audit read it, and the SessionStart hook shows it to you at the start of every session.
|
|
9
|
+
|
|
10
|
+
## Format
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
STATUS: IN_PROGRESS | AWAITING_USER | COMPLETED | PAUSED
|
|
14
|
+
TASK: <one line: what this project is doing now>
|
|
15
|
+
DATE: <ISO timestamp of this update>
|
|
16
|
+
LAST_ACTION: <the last thing actually done, with evidence: commit, URL, file>
|
|
17
|
+
NEXT: <the next concrete step, or "await user input">
|
|
18
|
+
BLOCKER: none | you: <what the user must do> | external: <who we wait for>
|
|
19
|
+
DO_NOT_REDO:
|
|
20
|
+
- <idempotent steps already done: "migration 012 applied", "email to X sent 21.10">
|
|
21
|
+
VERIFY_BEFORE_ACT: <commands that check real state: git log -1, curl /health, list migrations>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Optional: `PROGRESS: 40%`, `GOAL:`, `METRIC: name | target | fact`.
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
|
|
28
|
+
1. **Read first.** At session start, after compaction, or on "continue", read CHECKPOINT.md before anything else. If `STATUS: COMPLETED`, do not redo the work: say it is done and wait for a new instruction.
|
|
29
|
+
2. **Verify before acting.** Before any deploy, migration, payment, message or delete, run `VERIFY_BEFORE_ACT` and compare with `DO_NOT_REDO`. If the goal is already reached, log "already done" and skip.
|
|
30
|
+
3. **Never duplicate a successful side effect** (sent email, posted message, charged card) without a new explicit reason.
|
|
31
|
+
4. **Rewrite on milestones.** After each significant step, rewrite the file: new LAST_ACTION, new NEXT, append to DO_NOT_REDO. Rewrite, do not append history: history belongs in git or an archive file.
|
|
32
|
+
5. **Waiting is a status.** When the next step needs the user, set `STATUS: AWAITING_USER`, `NEXT: await user input` and say exactly what is needed in BLOCKER. The board puts it in "Waiting on you".
|
|
33
|
+
6. **No secrets.** Never write tokens, passwords or personal data into CHECKPOINT.md. Point to where they live instead.
|
|
34
|
+
|
|
35
|
+
## Creating one
|
|
36
|
+
|
|
37
|
+
`director-kit checkpoint <dir>` writes the template without overwriting an existing file. Fill TASK, LAST_ACTION and NEXT from what you actually know; leave nothing as a placeholder.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-floors
|
|
3
|
+
description: Keep project memory in three floors (a small index, topic files, an archive) so it stays cheap to load and nothing is lost. Use when CLAUDE.md, MEMORY.md or another memory file grows past about 6000 characters, when saving a new long-lived fact, or when the user asks to clean up, compress or reorganise memory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory floors
|
|
7
|
+
|
|
8
|
+
Memory that is loaded into every session must stay small, or every session pays for it and the important rules drown. Split it into floors.
|
|
9
|
+
|
|
10
|
+
| Floor | File | Loaded | Limit | Holds |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| 1. Index | `CLAUDE.md` or `MEMORY.md` | every session | 6000 chars | rules that apply to every task + a map "topic -> file" |
|
|
13
|
+
| 2. Topics | `memory/<topic>.md` | when the topic comes up | none, keep focused | details: accounts, decisions, how-tos, people |
|
|
14
|
+
| 3. Archive | `memory/archive/<YYYY-MM>.md` | only on request | none | history, superseded decisions, old logs |
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
1. **Index is a map, not a diary.** One line per topic: `- deploy: memory/deploy.md (servers, order of steps, rollback)`. Rules stay in the index only if they apply to almost every task.
|
|
19
|
+
2. **Never delete, move.** When the index is over the limit, move the details down a floor and leave a one-line pointer. Superseded facts go to the archive with a date, not to the bin.
|
|
20
|
+
3. **One fact, one place.** Before adding, search the topic files; update the existing entry instead of duplicating it.
|
|
21
|
+
4. **Dates are absolute.** Write "2026-10-23", never "yesterday".
|
|
22
|
+
5. **Test before and after.** Before compressing memory, write 3 to 5 questions a session must still answer from it (e.g. "where is the deploy key?", "what is forbidden without approval?"). Answer them from the old version, compress, answer again from the new one. If any answer got worse, put the fact back into the index.
|
|
23
|
+
6. **No secrets in memory.** Point to where a secret lives (a password manager entry, an env file name), never paste the value.
|
|
24
|
+
|
|
25
|
+
`director-kit audit` lists every index file that is over the limit.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-audit
|
|
3
|
+
description: Audit all projects at once (what waits on the user, what stalled, what is not backed up, which memory outgrew its floor) and turn it into a short action list. Use when the user asks "what is going on across my projects", "what is stuck", "what do I need to do", for a weekly review, or to refresh the director-kit board.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project audit
|
|
7
|
+
|
|
8
|
+
Run the audit (it also rebuilds the board):
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/director-kit.mjs" audit --no-open
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If `CLAUDE_PLUGIN_ROOT` is not set, use `npx -y director-kit audit --no-open`. If it says there is no projects folder yet, ask the user for it and run `... --root=<folder>` once.
|
|
15
|
+
|
|
16
|
+
Then report to the user, shortest useful form:
|
|
17
|
+
|
|
18
|
+
1. **Waiting on you**: one line each, the concrete action (from BLOCKER or NEXT). These come first, they are the only items nobody else can move.
|
|
19
|
+
2. **Stalled**: the 3 to 5 longest-silent projects that are not finished, with the last NEXT. Ask whether to resume, pause (`STATUS: PAUSED`) or close (`STATUS: COMPLETED`) each one.
|
|
20
|
+
3. **Not safe**: repos without a remote, unpushed commits, old uncommitted work. Offer the exact command (`git push`, create remote), do not run it without a yes.
|
|
21
|
+
4. **Memory over the floor** and **stale backups**, if any.
|
|
22
|
+
|
|
23
|
+
Do not change other projects' files during an audit. Changing a project's CHECKPOINT.md status is fine only when the user decided it.
|
|
24
|
+
|
|
25
|
+
The board is at `~/.director-kit/board.html`; the full report is saved to `~/.director-kit/audits/`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
STATUS: IN_PROGRESS
|
|
2
|
+
TASK: <what this project is doing right now>
|
|
3
|
+
DATE: 2026-01-01T00:00:00Z
|
|
4
|
+
LAST_ACTION: <the last thing that was actually done>
|
|
5
|
+
NEXT: <the next concrete step, or "await user input">
|
|
6
|
+
BLOCKER: none
|
|
7
|
+
DO_NOT_REDO:
|
|
8
|
+
- <idempotent steps that are already done and must not be repeated>
|
|
9
|
+
VERIFY_BEFORE_ACT: <commands that check the current state before deploy/migration/delete>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# MEMORY.md: index (floor 1, keep under 6000 characters)
|
|
2
|
+
|
|
3
|
+
Rules that apply to almost every task, then a map to topic files. Details live in memory/<topic>.md, history in memory/archive/.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
- <rule that applies to every session>
|
|
7
|
+
|
|
8
|
+
## Map
|
|
9
|
+
- <topic>: memory/<topic>.md (<what is inside, 5 to 10 words>)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SOUL.md: who the assistant is in this workspace
|
|
2
|
+
|
|
3
|
+
Loaded into every session. Keep it under 2000 characters.
|
|
4
|
+
|
|
5
|
+
## Role
|
|
6
|
+
<One line. Example: senior developer and personal assistant to <name>, works across all projects in this folder.>
|
|
7
|
+
|
|
8
|
+
## How to talk to me
|
|
9
|
+
- Language: <English / Russian / ...>
|
|
10
|
+
- Style: facts and actions first, explanation after. No preamble, no "certainly", no emoji.
|
|
11
|
+
- Length: short. A table when comparing options, prose otherwise.
|
|
12
|
+
|
|
13
|
+
## What you may do without asking
|
|
14
|
+
- Read anything in the projects folder, run tests, create files and branches, write drafts.
|
|
15
|
+
|
|
16
|
+
## What needs my explicit "yes"
|
|
17
|
+
- Money: payments, subscriptions, upgrades.
|
|
18
|
+
- Deleting: files, branches, databases, servers, force push.
|
|
19
|
+
- Anything public or irreversible: posts, emails to clients, production deploys without a backup.
|
|
20
|
+
- When unsure, it is a "yes" question.
|
|
21
|
+
|
|
22
|
+
## Habits
|
|
23
|
+
- Read CHECKPOINT.md first; never redo what DO_NOT_REDO lists.
|
|
24
|
+
- Verify the real state before acting; report results with evidence, including failures.
|
|
25
|
+
- Save long-lived facts to memory (see memory-floors), never secrets.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# VISION.md: why this project exists
|
|
2
|
+
|
|
3
|
+
The stable header of the project. Every session reads it first, so keep it under 1500 characters and change it rarely.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
<One sentence: what changes in the world when this is done.>
|
|
7
|
+
|
|
8
|
+
## Success criteria
|
|
9
|
+
- <Measurable: "20 paying users by 2026-12-01", "page loads under 1 s">
|
|
10
|
+
|
|
11
|
+
## Constraints
|
|
12
|
+
- <Budget, deadline, stack, things that are off-limits>
|
|
13
|
+
|
|
14
|
+
## Not doing
|
|
15
|
+
- <Explicitly out of scope, so sessions do not wander>
|