Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ workflow even when no native skill creator or repository validator is installed.
- `automations/benny/` keeps the optional Cursor automation pack from pstack.
- `docs/guide/` contains the adapted upstream workflow guide.
- `tools/meta-mode/` contains the optional Bun orchestration and PR watcher
tools.
tools, plus the project-playbook checker.
- `scripts/install.mjs` stages, validates, and commits an installation.
- `scripts/remote-install.mjs` stages and transfers an SSH environment install.
- `scripts/validate.mjs` checks inventory, frontmatter, links, and portability.
Expand Down Expand Up @@ -461,6 +461,18 @@ review the source-to-adaptation patches, then use `--write` to update
content changes. The hashes detect drift; they do not establish that an adapted
workflow behaves like its source. See [the baseline review process](docs/skill-integrity.md).

To validate project playbooks that extend the bundled `meta-mode` playbooks, run:

```bash
node tools/meta-mode/check-playbooks.mjs <project-root>
```

A project playbook belongs in `<project-root>/.agents/playbooks/`. Its frontmatter
must contain `when:` and may contain a comma-separated `extends:` list. Changes
must be list items that quote the exact bundled step they anchor to with `After`,
`Before`, `Replace`, or `In`. Use `--bundled <path>` when checking a staged skill
installation instead of this checkout.

To compare the pstack inventory and non-skill artifacts, run:

```bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"check-models": "node scripts/model-config.mjs",
"model-budget": "node scripts/model-budget.mjs",
"check-upstream": "node scripts/check-upstream.mjs",
"check-playbooks": "node tools/meta-mode/check-playbooks.mjs",
"check-harness-policy": "node scripts/check-harness-policy.mjs",
"skill-baseline": "node scripts/skill-baseline.mjs",
"run-role": "node scripts/run-role.mjs",
Expand All @@ -30,7 +31,7 @@
"install-skills": "node scripts/install.mjs",
"optimize-context": "node scripts/optimize-context.mjs",
"reconcile-context": "node scripts/reconcile-context.mjs",
"test": "node scripts/validate.mjs && node scripts/skill-baseline.mjs --check && node --test scripts/install.test.mjs scripts/install-migration.test.mjs scripts/migration-integration.test.mjs scripts/skill-discovery.test.mjs scripts/harness-targets.test.mjs scripts/remote-discovery.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/model-budget.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/skill-baseline.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs scripts/history.test.mjs scripts/harness-policy.test.mjs scripts/harness-project.test.mjs scripts/check-upstream-adaptations.test.mjs scripts/pack-claude-skills.test.mjs"
"test": "node scripts/validate.mjs && node scripts/skill-baseline.mjs --check && node --test scripts/install.test.mjs scripts/install-migration.test.mjs scripts/migration-integration.test.mjs scripts/skill-discovery.test.mjs scripts/harness-targets.test.mjs scripts/remote-discovery.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/model-budget.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/skill-baseline.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs scripts/history.test.mjs scripts/harness-policy.test.mjs scripts/harness-project.test.mjs scripts/check-upstream-adaptations.test.mjs scripts/check-playbooks.test.mjs scripts/pack-claude-skills.test.mjs"
},
"bin": {
"mstack": "scripts/install.mjs",
Expand Down
9 changes: 7 additions & 2 deletions profiles/skill-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@
"upstream": "pstack",
"source": "skills/poteto-mode/SKILL.md",
"sourceDigest": "b3c505602b82fb3641a3983e9c46fb974881faa12ece1af804fae27afb7be532",
"targetDigest": "4efa5f64e1f6bd01fd08eb6fb720ab4c84c7f3d787fff9df71d0e07ab65b73fe"
"targetDigest": "32363f67837dfe03bc5fd1b58ba940f2cfde2dfe46bce808416ad7cd4f63fd90"
},
"skills/no-comments/SKILL.md": {
"upstream": "pstack",
Expand Down Expand Up @@ -718,6 +718,11 @@
"targetDigest": "ddcdb05af01b37d7513b2017d44e29c30bdaa79ca360c5913d701c16dde70907",
"reason": "Runtime tools are packaged separately from portable skill instructions."
},
"tools/meta-mode/check-playbooks.mjs": {
"upstream": null,
"reason": "Local portability guidance or implementation; review alongside its owning skill.",
"targetDigest": "3a753d840c391cbf93979d6d474bc3965b387bc507de6925420ca78ff5db6aaf"
},
"tools/meta-mode/orch/orch.test.ts": {
"upstream": "pstack",
"source": "skills/poteto-mode/scripts/orch/orch.test.ts",
Expand Down Expand Up @@ -749,7 +754,7 @@
"tools/meta-mode/README.md": {
"upstream": null,
"reason": "Local portability guidance or implementation; review alongside its owning skill.",
"targetDigest": "c27f30aa0fa15655a9c8dbd9df698a2c69393778bbe516ad5ebdf82ca0de71bd"
"targetDigest": "5a2dcf51ad2a816941a116b0e4c9c4e113c9d2a721a15f5ac8ef7a4b1a9afeaf"
},
"tools/meta-mode/watch-pr/cli.test.ts": {
"upstream": "pstack",
Expand Down
2 changes: 1 addition & 1 deletion profiles/upstream-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
},
"meta-mode": {
"source": "d5817d996f6da294c86cacb2bd1b0c40c85968844a6afd194fe69efa8654878f",
"target": "4efa5f64e1f6bd01fd08eb6fb720ab4c84c7f3d787fff9df71d0e07ab65b73fe"
"target": "32363f67837dfe03bc5fd1b58ba940f2cfde2dfe46bce808416ad7cd4f63fd90"
},
"no-comments": {
"source": "5c5b0882297d704c3a9720c52b7a793c68b013eaf717989f0945624efdfe2b05",
Expand Down
109 changes: 109 additions & 0 deletions scripts/check-playbooks.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { checkPlaybooks } from "../tools/meta-mode/check-playbooks.mjs";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const script = join(repoRoot, "tools", "meta-mode", "check-playbooks.mjs");

function fixture(t, playbooks, bundled = { "bug-fix.md": "Binary-search the cause.\n" }) {
const root = mkdtempSync(join(tmpdir(), "mstack-check-playbooks-"));
const bundledRoot = join(root, "bundled");
mkdirSync(join(root, ".agents", "playbooks"), { recursive: true });
mkdirSync(bundledRoot, { recursive: true });
for (const [name, content] of Object.entries(playbooks)) writeFileSync(join(root, ".agents", "playbooks", name), content);
for (const [name, content] of Object.entries(bundled)) writeFileSync(join(bundledRoot, name), content);
t.after(() => rmSync(root, { recursive: true, force: true }));
return { root, bundledRoot };
}

function run(root, bundledRoot, ...args) {
return spawnSync(process.execPath, [script, "--root", root, "--bundled", bundledRoot, ...args], { encoding: "utf8" });
}

test("accepts an anchored project playbook and normalizes CRLF", (t) => {
const { root, bundledRoot } = fixture(t, {
"bug-fix.md": "---\r\nextends: bug-fix\r\nwhen: Use it for bug reports.\r\n---\r\n- **In** \"Binary-search the cause\": compare with main.\r\n",
});
assert.deepEqual(checkPlaybooks(root, bundledRoot), []);
const result = run(root, bundledRoot);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "Every project playbook matches this mstack's playbooks.\n");
});

test("reports missing bases, stale anchors, missing when fields, and unquoted changes", (t) => {
const { root, bundledRoot } = fixture(t, {
"bug-fix.md": "---\nextends: bug-fix\nwhen: Use it for bugs.\n---\n- **After** \"Ask the user to reproduce it\": compare with main.\n",
"ship.md": "---\nextends: shipping-v2\nwhen: Use it to ship.\n---\n",
"broken.md": "---\nextends: bug-fix\n---\n- **Before** Binary-search the cause: compare with main.\n",
});
const result = run(root, bundledRoot);
assert.equal(result.status, 1);
assert.match(result.stderr, /Ask the user to reproduce it.*not in any playbook/);
assert.match(result.stderr, /shipping-v2.*no playbook/);
assert.match(result.stderr, /broken\.md: its frontmatter needs a \"when:\" line/);
assert.match(result.stderr, /broken\.md: a change has no straight-quoted step text/);
});

test("runs through a symlink", (t) => {
const { root, bundledRoot } = fixture(t, {
"ship.md": "---\nextends: shipping-v2\nwhen: Use it to ship.\n---\n",
});
const entry = join(root, "check-playbooks.mjs");
try {
symlinkSync(script, entry);
} catch (error) {
if (error.code === "EPERM") return t.skip("creating symlinks needs privileges on this system");
throw error;
}
const result = spawnSync(process.execPath, [entry, root, "--bundled", bundledRoot], { encoding: "utf8" });
assert.equal(result.status, 1);
assert.match(result.stderr, /shipping-v2.*no playbook/);
});

test("rejects unknown options and a missing project root", (t) => {
const { root, bundledRoot } = fixture(t, {});
const unknown = run(root, bundledRoot, "--unknown");
assert.equal(unknown.status, 1);
assert.match(unknown.stderr, /Unknown option: --unknown/);

const missing = run(join(root, "typo"), bundledRoot);
assert.equal(missing.status, 1);
assert.match(missing.stderr, /Project root is not a directory/);
});

test("parses BOM and unterminated frontmatter and keeps bases inside the bundled directory", (t) => {
const { root, bundledRoot } = fixture(t, {
"bom.md": "---\nwhen: Use it.\n---\nbody\n",
"bare.md": "---\nwhen: Use it.\n---",
"case.md": "---\nextends: Bug-Fix\nwhen: Use it.\n---\n",
"escape.md": "---\nextends: ../.agents/playbooks/bom\nwhen: Use it.\n---\n",
});
assert.deepEqual(checkPlaybooks(root, bundledRoot), [
".agents/playbooks/case.md: extends `Bug-Fix`, which this mstack has no playbook for",
".agents/playbooks/escape.md: extends `../.agents/playbooks/bom`, which this mstack has no playbook for",
]);
});

test("validates against the playbooks bundled with this checkout by default", (t) => {
const root = mkdtempSync(join(tmpdir(), "mstack-check-playbooks-bundled-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(join(root, ".agents", "playbooks"), { recursive: true });
writeFileSync(
join(root, ".agents", "playbooks", "bug-fix.md"),
'---\nextends: bug-fix\nwhen: Use it for bugs.\n---\n- **After** "Binary-search the cause." run the profiler.\n',
);
const result = spawnSync(process.execPath, [script, root], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
});

test("leaves a repository without project playbooks valid", (t) => {
const root = mkdtempSync(join(tmpdir(), "mstack-check-playbooks-empty-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
assert.deepEqual(checkPlaybooks(root, join(root, "missing-bundled")), []);
execFileSync(process.execPath, [script, "--root", root], { encoding: "utf8" });
});
4 changes: 4 additions & 0 deletions skills/meta-mode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ Comments follow the same rule as the reply. Write them clean as you go. Keep a c

## Playbooks

**Project playbooks.** A repository can add playbooks under `.agents/playbooks/`, one Markdown file per playbook. The frontmatter can name comma-separated `extends` stems and must include `when`, a sentence that names the requests the playbook serves. When the selected bundled playbook has a project extension, or the request matches a project's `when`, open the project playbook too. Copy the bundled steps into the todolist verbatim. Apply each project change at the step named by its quoted text. A change is a list item that starts with `**After**`, `**Before**`, `**Replace**`, or `**In**`, followed by straight-quoted text copied from an extended playbook.

Before using a project playbook, run `node "<meta-mode-tools>/check-playbooks.mjs" --bundled "<mstack-skills>/meta-mode/playbooks" <repository-root>`. The checker reports missing bases, missing `when` fields, unanchored changes, and anchors that no longer occur after a mstack update. Report every line it prints. Do not guess where an unanchored change belongs.

Open a todolist whose first items are the matched playbook's steps, copied in verbatim, before any task-specific todos. A step you choose not to do stays in the list with a one-line `skip: <reason>`. Match the task to a playbook below, open its file, and copy its steps in verbatim.

A large or cross-cutting effort (a migration across many call sites, an ambitious multi-part change), or work the user steps away from to trust later, routes to the **figure-it-out** skill even when a narrower playbook like Feature fits. Use **figure-it-out** whenever no bundled playbook fits. It designs a bespoke, rigorous playbook for the task. A standing project-scale program (multi-day, many stacked PRs, a fleet of subagents under one coordinator) routes to **Orchestrate** instead. figure-it-out designs one bespoke run, orchestrate runs the program.
Expand Down
3 changes: 3 additions & 0 deletions tools/meta-mode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ This directory contains the optional Bun tools that support `meta-mode`:
- `orch/` stores units, evidence, and gates for a long-running program.
- `watch-pr/` reads GitHub checks and review signals for a pull request stack.
- `check-plan.mjs` validates a generated plan.
- `check-playbooks.mjs` validates project playbook extensions against the bundled playbooks.
- `worktree-audit.sh` produces a read-only worktree audit.

The tools are not required by the portable skill installer. Run them from this
directory with Bun after installing dependencies from `package.json`. Their
inputs and outputs are deliberately separate from the canonical
`skills/meta-mode/SKILL.md`, so each Harness can use its own equivalent runtime.

Run `node check-playbooks.mjs <project-root>` to validate `.agents/playbooks/` before using project extensions. Pass `--bundled <path>` to check against another installed skill tree.

`worktree-audit.sh` can include the latest session that touched a worktree when
you set `MSTACK_TRANSCRIPTS_DIR` to a directory containing the active Harness's
workspace transcripts. Leave it unset when transcript history is unavailable;
Expand Down
119 changes: 119 additions & 0 deletions tools/meta-mode/check-playbooks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node

import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const BUNDLED = resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/meta-mode/playbooks");
const CHANGE = /^\s*(?:[-*]|\d+\.)\s+\*\*(?:After|Before|Replace|In)\*\*\s+"([^"]+)"/;
const CHANGE_VERB = /^\s*(?:[-*]|\d+\.)\s+\*\*(?:After|Before|Replace|In)\*\*/;

function flat(text) {
return text.replace(/\s+/g, " ");
}

function frontmatter(text) {
return text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1] ?? "";
}

function readText(path) {
return readFileSync(path, "utf8").replace(/^/, "").replaceAll("\r\n", "\n");
}

function field(text, key) {
return frontmatter(text).match(new RegExp(`^${key}:[ \\t]*(.*)$`, "m"))?.[1].trim() ?? "";
}

// Match against the directory listing so a stem resolves the same on case-insensitive and case-sensitive filesystems and cannot escape the directory.
function readPlaybook(root, stem) {
const file = `${stem}.md`;
return readdirSync(root).includes(file) ? readText(join(root, file)) : null;
}

export function checkPlaybooks(root, bundled = BUNDLED) {
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`Project root is not a directory: ${root}`);
const directory = join(resolve(root), ".agents", "playbooks");
if (!existsSync(directory)) return [];
if (!existsSync(bundled) || !statSync(bundled).isDirectory()) throw new Error(`Bundled playbooks directory not found: ${bundled}`);

const problems = [];
for (const name of readdirSync(directory).filter((file) => file.endsWith(".md")).sort()) {
const relativePath = `.agents/playbooks/${name}`;
const text = readText(join(directory, name));
const when = field(text, "when");
if (!when) problems.push(`${relativePath}: its frontmatter needs a "when:" line`);

const bases = field(text, "extends")
.split(",")
.map((stem) => stem.trim())
.filter(Boolean)
.map((stem) => ({ stem, text: readPlaybook(bundled, stem) }));
for (const base of bases) {
if (base.text === null) problems.push(`${relativePath}: extends \`${base.stem}\`, which this mstack has no playbook for`);
}

for (const line of text.split("\n")) {
if (CHANGE_VERB.test(line) && !CHANGE.test(line)) {
problems.push(`${relativePath}: a change has no straight-quoted step text to anchor on: ${line.trim().slice(0, 80)}`);
}
const anchor = line.match(CHANGE)?.[1];
if (anchor && !bases.some((base) => base.text && flat(base.text).includes(flat(anchor)))) {
problems.push(`${relativePath}: "${anchor}" is not in any playbook it extends`);
}
}
}
return problems;
}

function parseArgs(args) {
const values = new Map();
const positional = [];
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--help") return { help: true, values, positional };
if (arg === "--root" || arg === "--bundled") {
if (values.has(arg)) throw new Error(`Duplicate option: ${arg}`);
const value = args[++index];
if (!value || value.startsWith("--")) throw new Error(`${arg} requires a value`);
values.set(arg, value);
} else if (arg.startsWith("--")) {
throw new Error(`Unknown option: ${arg}`);
} else {
positional.push(arg);
}
}
if (positional.length > 1) throw new Error("Only one project root may be supplied");
return { help: false, values, positional };
}

function directInvocation() {
if (!process.argv[1]) return false;
try {
return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
} catch {
return false;
}
}

if (directInvocation()) {
try {
const parsed = parseArgs(process.argv.slice(2));
if (parsed.help) {
console.log("Usage: node tools/meta-mode/check-playbooks.mjs [--root <project-root>] [--bundled <playbooks>]\n\nChecks .agents/playbooks against bundled meta-mode playbooks.");
process.exit(0);
}
const root = parsed.values.get("--root") ?? parsed.positional[0] ?? ".";
const bundled = parsed.values.get("--bundled") ?? BUNDLED;
const problems = checkPlaybooks(root, bundled);
if (problems.length > 0) {
console.error(problems.join("\n"));
process.exitCode = 1;
} else {
console.log("Every project playbook matches this mstack's playbooks.");
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
Loading