@tcanaud/playbook 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thibaud Canaud
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 ADDED
@@ -0,0 +1,69 @@
1
+ # @tcanaud/playbook
2
+
3
+ YAML-driven orchestration for kai feature workflows — autonomous playbook execution with crash recovery, gates, and git-tracked audit journals.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx @tcanaud/playbook init
9
+ ```
10
+
11
+ This creates:
12
+ - `.playbooks/` directory with built-in playbooks and template
13
+ - `.claude/commands/playbook.run.md` and `playbook.resume.md` slash commands
14
+
15
+ ## CLI Commands
16
+
17
+ | Command | Description |
18
+ |---------|-------------|
19
+ | `npx @tcanaud/playbook init [--yes]` | Scaffold `.playbooks/` and install slash commands |
20
+ | `npx @tcanaud/playbook update` | Refresh commands and built-in playbooks |
21
+ | `npx @tcanaud/playbook start {playbook} {feature}` | Create worktree session for parallel execution |
22
+ | `npx @tcanaud/playbook check {file}` | Validate playbook YAML against schema |
23
+ | `npx @tcanaud/playbook help` | Show usage |
24
+
25
+ ## Claude Code Commands
26
+
27
+ After installation, use these in the Claude Code TUI:
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `/playbook.run {playbook} {feature}` | Launch supervisor to orchestrate playbook steps |
32
+ | `/playbook.resume` | Auto-detect and resume an interrupted session |
33
+
34
+ ## Built-in Playbooks
35
+
36
+ | Playbook | Steps | Description |
37
+ |----------|-------|-------------|
38
+ | `auto-feature` | 8 | plan → tasks → agreement → implement → agreement check → QA plan → QA run → PR |
39
+ | `auto-validate` | 2 | QA plan → QA run |
40
+
41
+ ## Custom Playbooks
42
+
43
+ ```bash
44
+ cp .playbooks/playbooks/playbook.tpl.yaml .playbooks/playbooks/my-workflow.yaml
45
+ # Edit the file, then validate:
46
+ npx @tcanaud/playbook check .playbooks/playbooks/my-workflow.yaml
47
+ ```
48
+
49
+ ## Parallel Execution
50
+
51
+ Run two features simultaneously in separate worktrees:
52
+
53
+ ```bash
54
+ npx @tcanaud/playbook start auto-feature 013-another-feature
55
+ # Follow the printed instructions
56
+ ```
57
+
58
+ ## Session Files
59
+
60
+ After a run, session files are in `.playbooks/sessions/{id}/`:
61
+
62
+ - `session.yaml` — manifest (playbook, feature, status, timestamps)
63
+ - `journal.yaml` — step-by-step execution log (status, decision type, duration, human responses)
64
+
65
+ These files are git-tracked and appear in PR diffs for auditability.
66
+
67
+ ## License
68
+
69
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { argv, exit } from "node:process";
4
+
5
+ const command = argv[2];
6
+ const args = argv.slice(3);
7
+
8
+ const HELP = `
9
+ playbook — YAML-driven orchestration for kai feature workflows.
10
+
11
+ Usage:
12
+ npx @tcanaud/playbook init Scaffold .playbooks/ directory and install slash commands
13
+ npx @tcanaud/playbook update Refresh slash commands and built-in playbooks
14
+ npx @tcanaud/playbook start Create a worktree session for parallel execution
15
+ npx @tcanaud/playbook check Validate a playbook YAML file against the schema
16
+ npx @tcanaud/playbook help Show this help message
17
+
18
+ Commands:
19
+ init [--yes] Skip confirmation prompts
20
+ update Refresh commands without touching sessions or custom playbooks
21
+ start {playbook} {feature} Create git worktree + session for parallel playbook execution
22
+ check {file} Validate playbook YAML against schema
23
+
24
+ Claude Code commands (after init):
25
+ /playbook.run {playbook} {feature} Launch supervisor to orchestrate playbook steps
26
+ /playbook.resume Auto-detect and resume an interrupted session
27
+ `;
28
+
29
+ switch (command) {
30
+ case "init": {
31
+ const { install } = await import("../src/installer.js");
32
+ await install(args);
33
+ break;
34
+ }
35
+ case "update": {
36
+ const { update } = await import("../src/updater.js");
37
+ await update(args);
38
+ break;
39
+ }
40
+ case "start": {
41
+ const { start } = await import("../src/worktree.js");
42
+ await start(args);
43
+ break;
44
+ }
45
+ case "check": {
46
+ const { check } = await import("../src/validator.js");
47
+ await check(args);
48
+ break;
49
+ }
50
+ case "help":
51
+ case "--help":
52
+ case "-h":
53
+ case undefined:
54
+ console.log(HELP);
55
+ break;
56
+ default:
57
+ console.error(`Unknown command: ${command}`);
58
+ console.log(HELP);
59
+ exit(1);
60
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tcanaud/playbook",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "YAML-driven orchestration for kai feature workflows — autonomous playbook execution with crash recovery, gates, and git-tracked audit journals.",
6
+ "bin": {
7
+ "playbook": "./bin/cli.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "templates/"
16
+ ],
17
+ "keywords": [
18
+ "kai",
19
+ "playbook",
20
+ "orchestration",
21
+ "supervisor",
22
+ "workflow",
23
+ "claude-code"
24
+ ],
25
+ "author": "Thibaud Canaud",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/tcanaud/playbook.git"
30
+ }
31
+ }
package/src/detect.js ADDED
@@ -0,0 +1,118 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+
5
+ // ── Directory detection ─────────────────────────────────
6
+
7
+ /**
8
+ * Checks if `.playbooks/` exists relative to cwd.
9
+ * @param {string} [cwd] - Directory to check from. Defaults to process.cwd().
10
+ * @returns {string|null} Absolute path to `.playbooks/` if found, null otherwise.
11
+ */
12
+ export function detectPlaybooksDir(cwd = process.cwd()) {
13
+ const dir = resolve(cwd, ".playbooks");
14
+ return existsSync(dir) ? dir : null;
15
+ }
16
+
17
+ /**
18
+ * Checks if `.claude/commands/` exists relative to cwd.
19
+ * @param {string} [cwd] - Directory to check from. Defaults to process.cwd().
20
+ * @returns {string|null} Absolute path to `.claude/commands/` if found, null otherwise.
21
+ */
22
+ export function detectClaudeCommands(cwd = process.cwd()) {
23
+ const dir = resolve(cwd, join(".claude", "commands"));
24
+ return existsSync(dir) ? dir : null;
25
+ }
26
+
27
+ // ── Git state ───────────────────────────────────────────
28
+
29
+ /**
30
+ * Returns the absolute path of the git repository root.
31
+ * @returns {string} Trimmed output of `git rev-parse --show-toplevel`.
32
+ * @throws {Error} If not in a git repository or git is unavailable.
33
+ */
34
+ export function getRepoRoot() {
35
+ try {
36
+ return execSync("git rev-parse --show-toplevel", {
37
+ encoding: "utf8",
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ }).trim();
40
+ } catch (err) {
41
+ throw new Error(
42
+ `Failed to determine git repository root: ${err.message ?? String(err)}`
43
+ );
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Determines whether the current directory is a git worktree (not the main working tree).
49
+ *
50
+ * Compares `git rev-parse --show-toplevel` with `git rev-parse --git-common-dir`.
51
+ * In the main working tree, `--git-common-dir` resolves to `{toplevel}/.git`.
52
+ * In a linked worktree, it resolves to the common `.git` directory of the main tree.
53
+ *
54
+ * @returns {boolean} True if current directory is a linked worktree, false otherwise.
55
+ * @throws {Error} If not in a git repository or git is unavailable.
56
+ */
57
+ export function isWorktree() {
58
+ try {
59
+ const toplevel = execSync("git rev-parse --show-toplevel", {
60
+ encoding: "utf8",
61
+ stdio: ["pipe", "pipe", "pipe"],
62
+ }).trim();
63
+
64
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
65
+ encoding: "utf8",
66
+ stdio: ["pipe", "pipe", "pipe"],
67
+ }).trim();
68
+
69
+ // In the main working tree, git-common-dir is "{toplevel}/.git" (or ".git" relative).
70
+ // Resolve both to absolute paths for a reliable comparison.
71
+ const expectedMainGitDir = join(toplevel, ".git");
72
+ const resolvedCommonDir = resolve(toplevel, gitCommonDir);
73
+
74
+ return resolvedCommonDir !== expectedMainGitDir;
75
+ } catch (err) {
76
+ throw new Error(
77
+ `Failed to determine worktree status: ${err.message ?? String(err)}`
78
+ );
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Returns the name of the current git branch.
84
+ * @returns {string} Current branch name (trimmed).
85
+ * @throws {Error} If not in a git repository, in detached HEAD state, or git is unavailable.
86
+ */
87
+ export function getCurrentBranch() {
88
+ try {
89
+ return execSync("git rev-parse --abbrev-ref HEAD", {
90
+ encoding: "utf8",
91
+ stdio: ["pipe", "pipe", "pipe"],
92
+ }).trim();
93
+ } catch (err) {
94
+ throw new Error(
95
+ `Failed to determine current branch: ${err.message ?? String(err)}`
96
+ );
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Returns true if the working tree has no uncommitted changes (clean state).
102
+ * Equivalent to checking that `git status --porcelain` produces no output.
103
+ * @returns {boolean} True if working tree is clean, false if there are uncommitted changes.
104
+ * @throws {Error} If not in a git repository or git is unavailable.
105
+ */
106
+ export function isCleanWorkingTree() {
107
+ try {
108
+ const output = execSync("git status --porcelain", {
109
+ encoding: "utf8",
110
+ stdio: ["pipe", "pipe", "pipe"],
111
+ });
112
+ return output.trim() === "";
113
+ } catch (err) {
114
+ throw new Error(
115
+ `Failed to check working tree status: ${err.message ?? String(err)}`
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ copyFileSync,
5
+ readFileSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createInterface } from "node:readline";
11
+ import { detectPlaybooksDir, detectClaudeCommands } from "./detect.js";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const TEMPLATES = join(__dirname, "..", "templates");
15
+
16
+ // ── Helpers ──────────────────────────────────────────────
17
+
18
+ function ensureDir(dir) {
19
+ if (!existsSync(dir)) {
20
+ mkdirSync(dir, { recursive: true });
21
+ }
22
+ }
23
+
24
+ function copyTemplate(src, dest) {
25
+ ensureDir(dirname(dest));
26
+ copyFileSync(src, dest);
27
+ }
28
+
29
+ function ask(question) {
30
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
31
+ return new Promise((resolve) => {
32
+ rl.question(question, (answer) => {
33
+ rl.close();
34
+ resolve(answer.trim().toLowerCase());
35
+ });
36
+ });
37
+ }
38
+
39
+ // ── install ──────────────────────────────────────────────
40
+
41
+ export async function install(flags = []) {
42
+ const projectRoot = process.cwd();
43
+ const autoYes = flags.includes("--yes");
44
+
45
+ console.log("\n @tcanaud/playbook v1.0.0\n");
46
+
47
+ const playbooksDir = join(projectRoot, ".playbooks");
48
+ const alreadyExists = existsSync(playbooksDir);
49
+
50
+ // ── Confirmation prompt ──────────────────────────────
51
+ if (alreadyExists && !autoYes) {
52
+ const answer = await ask(
53
+ " Playbook system already initialized. Re-install? (y/N) "
54
+ );
55
+ if (answer !== "y" && answer !== "yes") {
56
+ console.log(" Skipping. Use '@tcanaud/playbook update' to refresh commands only.\n");
57
+ return;
58
+ }
59
+ }
60
+
61
+ // ── Phase 1/3: Create .playbooks/ directory tree ─────
62
+ console.log(" [1/3] Creating .playbooks/ directory tree...");
63
+
64
+ const dirs = [
65
+ join(playbooksDir, "playbooks"),
66
+ join(playbooksDir, "sessions"),
67
+ join(playbooksDir, "templates"),
68
+ ];
69
+
70
+ for (const dir of dirs) {
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true });
73
+ const rel = dir.replace(projectRoot + "/", "");
74
+ console.log(` created .${rel.startsWith(".") ? "" : "/"}${rel}`);
75
+ }
76
+ }
77
+
78
+ // ── Phase 2/3: Copy built-in playbook files ──────────
79
+ console.log(" [2/3] Installing built-in playbooks...");
80
+
81
+ const playbookFiles = ["auto-feature.yaml", "auto-validate.yaml"];
82
+
83
+ for (const file of playbookFiles) {
84
+ const src = join(TEMPLATES, "playbooks", file);
85
+ const dest = join(playbooksDir, "playbooks", file);
86
+ if (existsSync(src)) {
87
+ copyTemplate(src, dest);
88
+ console.log(` created .playbooks/playbooks/${file}`);
89
+ }
90
+ }
91
+
92
+ // Copy playbook template file
93
+ const tplSrc = join(TEMPLATES, "core", "playbook.tpl.yaml");
94
+ const tplDest = join(playbooksDir, "playbooks", "playbook.tpl.yaml");
95
+ if (existsSync(tplSrc)) {
96
+ copyTemplate(tplSrc, tplDest);
97
+ console.log(" created .playbooks/playbooks/playbook.tpl.yaml");
98
+ }
99
+
100
+ // Generate _index.yaml from template (replace {{TIMESTAMP}})
101
+ const indexSrc = join(TEMPLATES, "core", "_index.yaml");
102
+ const indexDest = join(playbooksDir, "_index.yaml");
103
+ if (existsSync(indexSrc)) {
104
+ const timestamp = new Date().toISOString();
105
+ const content = readFileSync(indexSrc, "utf8").replace(
106
+ /\{\{TIMESTAMP\}\}/g,
107
+ timestamp
108
+ );
109
+ writeFileSync(indexDest, content, "utf8");
110
+ console.log(" created .playbooks/_index.yaml");
111
+ }
112
+
113
+ // ── Phase 3/3: Install Claude Code commands ──────────
114
+ console.log(" [3/3] Installing Claude Code commands...");
115
+
116
+ const claudeCommandsDir = join(projectRoot, ".claude", "commands");
117
+ if (!detectClaudeCommands(projectRoot)) {
118
+ mkdirSync(claudeCommandsDir, { recursive: true });
119
+ console.log(" created .claude/commands/");
120
+ }
121
+
122
+ const commandFiles = ["playbook.run.md", "playbook.resume.md"];
123
+
124
+ for (const file of commandFiles) {
125
+ const src = join(TEMPLATES, "commands", file);
126
+ const dest = join(claudeCommandsDir, file);
127
+ if (existsSync(src)) {
128
+ copyTemplate(src, dest);
129
+ console.log(` created .claude/commands/${file}`);
130
+ }
131
+ }
132
+
133
+ // ── Done ─────────────────────────────────────────────
134
+ console.log();
135
+ console.log(" Done! Playbook system installed.");
136
+ console.log();
137
+ console.log(" Next steps:");
138
+ console.log(" 1. Run /playbook.run {playbook} {feature} to start a supervised workflow");
139
+ console.log(" 2. Run /playbook.resume to recover an interrupted session");
140
+ console.log(" 3. Explore .playbooks/playbooks/ to see built-in playbooks");
141
+ console.log();
142
+ }