@hotwired-sh/playbooks 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/README.md +57 -0
- package/dist/data.d.ts +19 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +87 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/validate.d.ts +13 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +91 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +75 -0
- package/package.json +69 -0
- package/playbooks/doc-editor/critiquer.md +214 -0
- package/playbooks/doc-editor/playbook.json +52 -0
- package/playbooks/doc-editor/protocol.md +163 -0
- package/playbooks/doc-editor/writer.md +268 -0
- package/playbooks/plan-build/builder.md +65 -0
- package/playbooks/plan-build/playbook.json +48 -0
- package/playbooks/plan-build/protocol.md +89 -0
- package/playbooks/plan-build/strategist.md +68 -0
- package/playbooks/protocol-base.md +75 -0
- package/schemas/playbook.schema.json +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @hotwired-sh/playbooks
|
|
2
|
+
|
|
3
|
+
[](https://hotwired.sh)
|
|
4
|
+
|
|
5
|
+
Playbook definitions for [Hotwired](https://hotwired.sh) multi-agent workflows.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @hotwired-sh/playbooks
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### TypeScript/JavaScript
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { loadAllPlaybooks, loadPlaybook, type Playbook } from '@hotwired-sh/playbooks';
|
|
19
|
+
|
|
20
|
+
// Load all active playbooks
|
|
21
|
+
const playbooks = loadAllPlaybooks();
|
|
22
|
+
|
|
23
|
+
// Load a specific playbook
|
|
24
|
+
const playbook = loadPlaybook('plan-build');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Direct file access
|
|
28
|
+
|
|
29
|
+
The playbook files are included in the package and can be accessed directly:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createRequire } from 'module';
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const packagePath = require.resolve('@hotwired-sh/playbooks/package.json');
|
|
35
|
+
// Playbooks are in ./playbooks/ relative to package root
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Playbook Structure
|
|
39
|
+
|
|
40
|
+
Each playbook is a directory containing:
|
|
41
|
+
|
|
42
|
+
- `playbook.json` - Metadata (name, roles, flow, etc.)
|
|
43
|
+
- `protocol.md` - Shared protocol/rules for all agents
|
|
44
|
+
- `*.md` - Role-specific prompts
|
|
45
|
+
|
|
46
|
+
## Available Playbooks
|
|
47
|
+
|
|
48
|
+
- **plan-build** - Planning and building features (Strategist + Builder)
|
|
49
|
+
- **doc-editor** - Documentation writing and editing (Writer + Critiquer)
|
|
50
|
+
|
|
51
|
+
## Creating Custom Playbooks
|
|
52
|
+
|
|
53
|
+
Fork this repository and add your own playbook directories following the same structure in `./playbooks/`.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook data loader
|
|
3
|
+
*/
|
|
4
|
+
import type { Playbook } from './types.js';
|
|
5
|
+
export declare const PLAYBOOK_IDS: readonly ["doc-editor", "plan-build"];
|
|
6
|
+
export type PlaybookId = typeof PLAYBOOK_IDS[number];
|
|
7
|
+
/**
|
|
8
|
+
* List all available playbook IDs (active only by default)
|
|
9
|
+
*/
|
|
10
|
+
export declare function listPlaybookIds(activeOnly?: boolean): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Load a specific playbook by ID
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadPlaybook(playbookId: string): Playbook | null;
|
|
15
|
+
/**
|
|
16
|
+
* Load all active playbooks
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadAllPlaybooks(): Playbook[];
|
|
19
|
+
//# sourceMappingURL=data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAO7D,eAAO,MAAM,YAAY,uCAGf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC;AAErD;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,UAAO,GAAG,MAAM,EAAE,CAe3D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAwChE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAY7C"}
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook data loader
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
// Go up from dist/ to package root, then into playbooks/
|
|
9
|
+
const PLAYBOOKS_DIR = join(__dirname, '..', 'playbooks');
|
|
10
|
+
export const PLAYBOOK_IDS = [
|
|
11
|
+
'doc-editor',
|
|
12
|
+
'plan-build',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* List all available playbook IDs (active only by default)
|
|
16
|
+
*/
|
|
17
|
+
export function listPlaybookIds(activeOnly = true) {
|
|
18
|
+
return PLAYBOOK_IDS.filter((id) => {
|
|
19
|
+
if (!activeOnly)
|
|
20
|
+
return true;
|
|
21
|
+
const metadataPath = join(PLAYBOOKS_DIR, id, 'playbook.json');
|
|
22
|
+
if (!existsSync(metadataPath))
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(metadataPath, 'utf-8');
|
|
26
|
+
const metadata = JSON.parse(content);
|
|
27
|
+
return metadata.active !== false;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Load a specific playbook by ID
|
|
36
|
+
*/
|
|
37
|
+
export function loadPlaybook(playbookId) {
|
|
38
|
+
const playbookDir = join(PLAYBOOKS_DIR, playbookId);
|
|
39
|
+
if (!existsSync(playbookDir)) {
|
|
40
|
+
console.error(`[playbooks] Playbook directory not found: ${playbookDir}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// Load metadata
|
|
45
|
+
const metadataPath = join(playbookDir, 'playbook.json');
|
|
46
|
+
const metadataContent = readFileSync(metadataPath, 'utf-8');
|
|
47
|
+
const metadata = JSON.parse(metadataContent);
|
|
48
|
+
// Load protocol
|
|
49
|
+
const protocolPath = join(playbookDir, 'protocol.md');
|
|
50
|
+
const protocol = existsSync(protocolPath)
|
|
51
|
+
? readFileSync(protocolPath, 'utf-8')
|
|
52
|
+
: '';
|
|
53
|
+
// Load role prompts
|
|
54
|
+
const rolePrompts = {};
|
|
55
|
+
for (const role of metadata.roles) {
|
|
56
|
+
if (role.promptFile) {
|
|
57
|
+
const promptPath = join(playbookDir, role.promptFile);
|
|
58
|
+
if (existsSync(promptPath)) {
|
|
59
|
+
rolePrompts[role.id] = readFileSync(promptPath, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
metadata,
|
|
65
|
+
protocol,
|
|
66
|
+
role_prompts: rolePrompts,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(`[playbooks] Error loading playbook: ${playbookId}`, error);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load all active playbooks
|
|
76
|
+
*/
|
|
77
|
+
export function loadAllPlaybooks() {
|
|
78
|
+
const ids = listPlaybookIds();
|
|
79
|
+
const playbooks = [];
|
|
80
|
+
for (const id of ids) {
|
|
81
|
+
const playbook = loadPlaybook(id);
|
|
82
|
+
if (playbook) {
|
|
83
|
+
playbooks.push(playbook);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return playbooks;
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hotwired-sh/playbooks
|
|
3
|
+
*
|
|
4
|
+
* Playbook definitions for Hotwired multi-agent workflows.
|
|
5
|
+
*
|
|
6
|
+
* @example Loading playbooks
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { loadAllPlaybooks, loadPlaybook } from '@hotwired-sh/playbooks';
|
|
9
|
+
*
|
|
10
|
+
* const playbooks = loadAllPlaybooks();
|
|
11
|
+
* const playbook = loadPlaybook('plan-build');
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example Creating custom playbooks
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import type { PlaybookMetadata, PlaybookRole } from '@hotwired-sh/playbooks';
|
|
17
|
+
* import { validatePlaybook } from '@hotwired-sh/playbooks';
|
|
18
|
+
*
|
|
19
|
+
* // Validate your playbook directory
|
|
20
|
+
* const result = validatePlaybook('./my-playbook');
|
|
21
|
+
* if (!result.valid) {
|
|
22
|
+
* console.error(result.errors);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export type { Playbook, PlaybookMetadata, PlaybookRole, PlaybooksManifest, ValidationResult, } from './types.js';
|
|
27
|
+
export { loadAllPlaybooks, loadPlaybook, listPlaybookIds, PLAYBOOK_IDS, type PlaybookId, } from './data.js';
|
|
28
|
+
export { validatePlaybook, validateAllPlaybooks, } from './validate.js';
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,YAAY,EACV,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hotwired-sh/playbooks
|
|
3
|
+
*
|
|
4
|
+
* Playbook definitions for Hotwired multi-agent workflows.
|
|
5
|
+
*
|
|
6
|
+
* @example Loading playbooks
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { loadAllPlaybooks, loadPlaybook } from '@hotwired-sh/playbooks';
|
|
9
|
+
*
|
|
10
|
+
* const playbooks = loadAllPlaybooks();
|
|
11
|
+
* const playbook = loadPlaybook('plan-build');
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example Creating custom playbooks
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import type { PlaybookMetadata, PlaybookRole } from '@hotwired-sh/playbooks';
|
|
17
|
+
* import { validatePlaybook } from '@hotwired-sh/playbooks';
|
|
18
|
+
*
|
|
19
|
+
* // Validate your playbook directory
|
|
20
|
+
* const result = validatePlaybook('./my-playbook');
|
|
21
|
+
* if (!result.valid) {
|
|
22
|
+
* console.error(result.errors);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
// Data loading
|
|
27
|
+
export { loadAllPlaybooks, loadPlaybook, listPlaybookIds, PLAYBOOK_IDS, } from './data.js';
|
|
28
|
+
// Validation
|
|
29
|
+
export { validatePlaybook, validateAllPlaybooks, } from './validate.js';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook type definitions
|
|
3
|
+
*
|
|
4
|
+
* These types define the structure of playbook configurations
|
|
5
|
+
* used by the Hotwired multi-agent system.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Defines an agent role within a playbook
|
|
9
|
+
*/
|
|
10
|
+
export interface PlaybookRole {
|
|
11
|
+
/** Unique identifier for the role (kebab-case, e.g., 'test-writer') */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Display name for the role (e.g., 'Test Writer') */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Role type identifier (e.g., 'orchestrator', 'implementer') */
|
|
16
|
+
role: string;
|
|
17
|
+
/** Description of what this role does */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Filename of the prompt markdown file (e.g., 'writer.md') */
|
|
20
|
+
promptFile: string;
|
|
21
|
+
/** Role capabilities */
|
|
22
|
+
capabilities?: {
|
|
23
|
+
canResolveImpediments?: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Metadata for a playbook, stored in playbook.json
|
|
28
|
+
*/
|
|
29
|
+
export interface PlaybookMetadata {
|
|
30
|
+
/** Unique identifier for the playbook (kebab-case, e.g., 'plan-build') */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Display name for the playbook */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Short tagline describing the playbook */
|
|
35
|
+
tagline: string;
|
|
36
|
+
/** Detailed description of what the playbook does */
|
|
37
|
+
description: string;
|
|
38
|
+
/** Version string */
|
|
39
|
+
version?: string;
|
|
40
|
+
/** Icon identifier (Lucide icon name) */
|
|
41
|
+
icon: string;
|
|
42
|
+
/** Whether the playbook is active and available for use (default: true) */
|
|
43
|
+
active?: boolean;
|
|
44
|
+
/** Agent roles in this playbook */
|
|
45
|
+
roles: PlaybookRole[];
|
|
46
|
+
/** Workflow phases */
|
|
47
|
+
phases?: string[];
|
|
48
|
+
/** Flow description (array of step descriptions) */
|
|
49
|
+
flow: string[];
|
|
50
|
+
/** Description of human's role in the workflow */
|
|
51
|
+
humanRole: string;
|
|
52
|
+
/** List of use cases this playbook is best suited for */
|
|
53
|
+
bestFor: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A fully loaded playbook with all content
|
|
57
|
+
*/
|
|
58
|
+
export interface Playbook {
|
|
59
|
+
/** Playbook metadata from playbook.json */
|
|
60
|
+
metadata: PlaybookMetadata;
|
|
61
|
+
/** Protocol markdown content (shared rules for all agents) */
|
|
62
|
+
protocol: string;
|
|
63
|
+
/** Map of role ID to prompt markdown content */
|
|
64
|
+
role_prompts: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Response format for the playbooks API
|
|
68
|
+
*/
|
|
69
|
+
export interface PlaybooksManifest {
|
|
70
|
+
/** List of all playbooks */
|
|
71
|
+
playbooks: Playbook[];
|
|
72
|
+
/** Version timestamp for cache invalidation */
|
|
73
|
+
version: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Result of validating a playbook
|
|
77
|
+
*/
|
|
78
|
+
export interface ValidationResult {
|
|
79
|
+
/** Whether the playbook is valid */
|
|
80
|
+
valid: boolean;
|
|
81
|
+
/** List of validation errors (empty if valid) */
|
|
82
|
+
errors: string[];
|
|
83
|
+
/** List of validation warnings */
|
|
84
|
+
warnings: string[];
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,YAAY,CAAC,EAAE;QACb,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,EAAE,EAAE,MAAM,CAAC;IACX,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mCAAmC;IACnC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oDAAoD;IACpD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,2CAA2C;IAC3C,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,iDAAiD;IACjD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook validation utilities
|
|
3
|
+
*/
|
|
4
|
+
import type { ValidationResult } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Validate a playbook directory
|
|
7
|
+
*/
|
|
8
|
+
export declare function validatePlaybook(playbookDir: string): ValidationResult;
|
|
9
|
+
/**
|
|
10
|
+
* Validate all playbooks in a directory
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateAllPlaybooks(playbooksDir: string): Map<string, ValidationResult>;
|
|
13
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CA4EtE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAYxF"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook validation utilities
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Validate a playbook directory
|
|
8
|
+
*/
|
|
9
|
+
export function validatePlaybook(playbookDir) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
// Check playbook.json exists
|
|
13
|
+
const metadataPath = join(playbookDir, 'playbook.json');
|
|
14
|
+
if (!existsSync(metadataPath)) {
|
|
15
|
+
errors.push('Missing playbook.json');
|
|
16
|
+
return { valid: false, errors, warnings };
|
|
17
|
+
}
|
|
18
|
+
// Parse and validate playbook.json
|
|
19
|
+
let metadata;
|
|
20
|
+
try {
|
|
21
|
+
const content = readFileSync(metadataPath, 'utf-8');
|
|
22
|
+
metadata = JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
errors.push(`Invalid playbook.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
26
|
+
return { valid: false, errors, warnings };
|
|
27
|
+
}
|
|
28
|
+
// Required fields
|
|
29
|
+
const requiredFields = ['id', 'name', 'tagline', 'description', 'icon', 'roles', 'flow', 'humanRole', 'bestFor'];
|
|
30
|
+
for (const field of requiredFields) {
|
|
31
|
+
if (!(field in metadata)) {
|
|
32
|
+
errors.push(`Missing required field: ${field}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Validate id format
|
|
36
|
+
if (metadata.id && !/^[a-z][a-z0-9-]*$/.test(metadata.id)) {
|
|
37
|
+
errors.push(`Invalid id format: "${metadata.id}" (must be kebab-case)`);
|
|
38
|
+
}
|
|
39
|
+
// Validate roles
|
|
40
|
+
if (Array.isArray(metadata.roles)) {
|
|
41
|
+
if (metadata.roles.length === 0) {
|
|
42
|
+
errors.push('Playbook must have at least one role');
|
|
43
|
+
}
|
|
44
|
+
const roleIds = new Set();
|
|
45
|
+
for (const role of metadata.roles) {
|
|
46
|
+
// Check for duplicate role IDs
|
|
47
|
+
if (roleIds.has(role.id)) {
|
|
48
|
+
errors.push(`Duplicate role id: "${role.id}"`);
|
|
49
|
+
}
|
|
50
|
+
roleIds.add(role.id);
|
|
51
|
+
// Check role id format
|
|
52
|
+
if (!/^[a-z][a-z0-9-]*$/.test(role.id)) {
|
|
53
|
+
errors.push(`Invalid role id format: "${role.id}" (must be kebab-case)`);
|
|
54
|
+
}
|
|
55
|
+
// Check prompt file exists
|
|
56
|
+
if (role.promptFile) {
|
|
57
|
+
const promptPath = join(playbookDir, role.promptFile);
|
|
58
|
+
if (!existsSync(promptPath)) {
|
|
59
|
+
errors.push(`Missing prompt file for role "${role.id}": ${role.promptFile}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
errors.push(`Missing promptFile for role "${role.id}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Check for protocol.md (warning if missing)
|
|
68
|
+
const protocolPath = join(playbookDir, 'protocol.md');
|
|
69
|
+
if (!existsSync(protocolPath)) {
|
|
70
|
+
warnings.push('Missing protocol.md (recommended for shared agent instructions)');
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
valid: errors.length === 0,
|
|
74
|
+
errors,
|
|
75
|
+
warnings,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate all playbooks in a directory
|
|
80
|
+
*/
|
|
81
|
+
export function validateAllPlaybooks(playbooksDir) {
|
|
82
|
+
const results = new Map();
|
|
83
|
+
const entries = readdirSync(playbooksDir, { withFileTypes: true });
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
const playbookPath = join(playbooksDir, entry.name);
|
|
87
|
+
results.set(entry.name, validatePlaybook(playbookPath));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.test.d.ts","sourceRoot":"","sources":["../src/validate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { validatePlaybook, validateAllPlaybooks } from './validate.js';
|
|
5
|
+
import { loadPlaybook, loadAllPlaybooks, listPlaybookIds, PLAYBOOK_IDS } from './data.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PLAYBOOKS_DIR = join(__dirname, '..', 'playbooks');
|
|
8
|
+
describe('validatePlaybook', () => {
|
|
9
|
+
it('validates plan-build playbook', () => {
|
|
10
|
+
const result = validatePlaybook(join(PLAYBOOKS_DIR, 'plan-build'));
|
|
11
|
+
expect(result.valid).toBe(true);
|
|
12
|
+
expect(result.errors).toHaveLength(0);
|
|
13
|
+
});
|
|
14
|
+
it('validates doc-editor playbook', () => {
|
|
15
|
+
const result = validatePlaybook(join(PLAYBOOKS_DIR, 'doc-editor'));
|
|
16
|
+
expect(result.valid).toBe(true);
|
|
17
|
+
expect(result.errors).toHaveLength(0);
|
|
18
|
+
});
|
|
19
|
+
it('returns error for non-existent directory', () => {
|
|
20
|
+
const result = validatePlaybook(join(PLAYBOOKS_DIR, 'non-existent'));
|
|
21
|
+
expect(result.valid).toBe(false);
|
|
22
|
+
expect(result.errors).toContain('Missing playbook.json');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('validateAllPlaybooks', () => {
|
|
26
|
+
it('validates all playbooks in directory', () => {
|
|
27
|
+
const results = validateAllPlaybooks(PLAYBOOKS_DIR);
|
|
28
|
+
expect(results.size).toBeGreaterThan(0);
|
|
29
|
+
for (const [id, result] of results) {
|
|
30
|
+
expect(result.valid, `Playbook "${id}" should be valid`).toBe(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('loadPlaybook', () => {
|
|
35
|
+
it('loads plan-build playbook', () => {
|
|
36
|
+
const playbook = loadPlaybook('plan-build');
|
|
37
|
+
expect(playbook).not.toBeNull();
|
|
38
|
+
expect(playbook?.metadata.id).toBe('plan-build');
|
|
39
|
+
expect(playbook?.metadata.roles.length).toBeGreaterThan(0);
|
|
40
|
+
expect(playbook?.protocol).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
it('loads doc-editor playbook', () => {
|
|
43
|
+
const playbook = loadPlaybook('doc-editor');
|
|
44
|
+
expect(playbook).not.toBeNull();
|
|
45
|
+
expect(playbook?.metadata.id).toBe('doc-editor');
|
|
46
|
+
});
|
|
47
|
+
it('returns null for non-existent playbook', () => {
|
|
48
|
+
const playbook = loadPlaybook('non-existent');
|
|
49
|
+
expect(playbook).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('loadAllPlaybooks', () => {
|
|
53
|
+
it('loads all active playbooks', () => {
|
|
54
|
+
const playbooks = loadAllPlaybooks();
|
|
55
|
+
expect(playbooks.length).toBeGreaterThan(0);
|
|
56
|
+
for (const playbook of playbooks) {
|
|
57
|
+
expect(playbook.metadata.id).toBeTruthy();
|
|
58
|
+
expect(playbook.metadata.active).not.toBe(false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('listPlaybookIds', () => {
|
|
63
|
+
it('returns all playbook IDs', () => {
|
|
64
|
+
const ids = listPlaybookIds();
|
|
65
|
+
expect(ids.length).toBeGreaterThan(0);
|
|
66
|
+
expect(ids).toContain('plan-build');
|
|
67
|
+
expect(ids).toContain('doc-editor');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('PLAYBOOK_IDS', () => {
|
|
71
|
+
it('contains expected playbooks', () => {
|
|
72
|
+
expect(PLAYBOOK_IDS).toContain('plan-build');
|
|
73
|
+
expect(PLAYBOOK_IDS).toContain('doc-editor');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hotwired-sh/playbooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Playbook definitions for Hotwired multi-agent workflows",
|
|
5
|
+
"author": "Hotwired <hello@hotwired.sh>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./schemas/playbook.schema.json": "./schemas/playbook.schema.json",
|
|
16
|
+
"./playbooks": "./playbooks",
|
|
17
|
+
"./playbooks/*": "./playbooks/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"playbooks",
|
|
22
|
+
"schemas"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/hotwired-sh/playbooks.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://hotwired.sh",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/hotwired-sh/playbooks/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"hotwired",
|
|
34
|
+
"playbooks",
|
|
35
|
+
"ai-agents",
|
|
36
|
+
"multi-agent",
|
|
37
|
+
"workflow"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"lint": "eslint src --ext .ts",
|
|
44
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"validate": "node --loader ts-node/esm scripts/validate-playbooks.ts",
|
|
47
|
+
"prepublishOnly": "npm run build",
|
|
48
|
+
"prepare": "husky"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@commitlint/cli": "^19.0.0",
|
|
52
|
+
"@commitlint/config-conventional": "^19.0.0",
|
|
53
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
54
|
+
"@semantic-release/exec": "^6.0.3",
|
|
55
|
+
"@semantic-release/git": "^10.0.1",
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
58
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
59
|
+
"conventional-changelog-conventionalcommits": "^7.0.2",
|
|
60
|
+
"eslint": "^8.57.0",
|
|
61
|
+
"husky": "^9.0.0",
|
|
62
|
+
"semantic-release": "^24.0.0",
|
|
63
|
+
"typescript": "^5.6.3",
|
|
64
|
+
"vitest": "^2.0.0"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=18"
|
|
68
|
+
}
|
|
69
|
+
}
|