arbiter-skill 0.1.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.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arbiter status - Check status of a decision plan
4
+ *
5
+ * Usage: arbiter-status <plan-id> [--tag <tag>]
6
+ *
7
+ * Returns JSON with:
8
+ * - planId: Plan ID
9
+ * - title: Plan title
10
+ * - status: pending|in_progress|completed
11
+ * - total: Total decisions
12
+ * - answered: Number answered
13
+ * - remaining: Number remaining
14
+ * - decisions: Map of decision ID -> { status, answer }
15
+ */
16
+ export {};
package/dist/status.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arbiter status - Check status of a decision plan
4
+ *
5
+ * Usage: arbiter-status <plan-id> [--tag <tag>]
6
+ *
7
+ * Returns JSON with:
8
+ * - planId: Plan ID
9
+ * - title: Plan title
10
+ * - status: pending|in_progress|completed
11
+ * - total: Total decisions
12
+ * - answered: Number answered
13
+ * - remaining: Number remaining
14
+ * - decisions: Map of decision ID -> { status, answer }
15
+ */
16
+ import { findPlanFile, parsePlanFile, parseDecisions } from './utils.js';
17
+ /**
18
+ * Get status of a decision plan
19
+ */
20
+ function status(planId, tag) {
21
+ if (!planId && !tag) {
22
+ return {
23
+ planId: '',
24
+ title: '',
25
+ status: 'pending',
26
+ total: 0,
27
+ answered: 0,
28
+ remaining: 0,
29
+ decisions: {},
30
+ error: 'Either planId or tag is required'
31
+ };
32
+ }
33
+ const file = findPlanFile(planId, tag);
34
+ if (!file) {
35
+ return {
36
+ planId: planId || '',
37
+ title: '',
38
+ status: 'pending',
39
+ total: 0,
40
+ answered: 0,
41
+ remaining: 0,
42
+ decisions: {},
43
+ error: 'Plan not found'
44
+ };
45
+ }
46
+ const { frontmatter, content } = parsePlanFile(file);
47
+ const decisions = parseDecisions(content);
48
+ const decisionMap = {};
49
+ for (const d of decisions) {
50
+ decisionMap[d.id] = {
51
+ status: d.status,
52
+ answer: d.answer
53
+ };
54
+ }
55
+ return {
56
+ planId: frontmatter.id,
57
+ title: frontmatter.title,
58
+ status: frontmatter.status,
59
+ total: frontmatter.total,
60
+ answered: frontmatter.answered,
61
+ remaining: frontmatter.remaining,
62
+ decisions: decisionMap
63
+ };
64
+ }
65
+ // CLI entry point
66
+ function main() {
67
+ const args = process.argv.slice(2);
68
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
69
+ console.log(`
70
+ arbiter status - Check status of a decision plan
71
+
72
+ Usage: arbiter-status <plan-id>
73
+ arbiter-status --tag <tag>
74
+
75
+ Example:
76
+ arbiter-status abc12345
77
+ arbiter-status --tag nft-marketplace
78
+ `);
79
+ process.exit(0);
80
+ }
81
+ let planId;
82
+ let tag;
83
+ for (let i = 0; i < args.length; i++) {
84
+ if (args[i] === '--tag' && args[i + 1]) {
85
+ tag = args[i + 1];
86
+ i++;
87
+ }
88
+ else if (!args[i].startsWith('-')) {
89
+ planId = args[i];
90
+ }
91
+ }
92
+ const result = status(planId, tag);
93
+ console.log(JSON.stringify(result, null, 2));
94
+ if (result.error) {
95
+ process.exit(1);
96
+ }
97
+ }
98
+ main();
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Type definitions for Arbiter Skill
3
+ */
4
+ export type Priority = 'low' | 'normal' | 'high' | 'urgent';
5
+ export type Status = 'pending' | 'in_progress' | 'completed';
6
+ export interface DecisionOption {
7
+ key: string;
8
+ label: string;
9
+ note?: string;
10
+ }
11
+ export interface Decision {
12
+ id: string;
13
+ title: string;
14
+ context: string;
15
+ options: DecisionOption[];
16
+ allowCustom?: boolean;
17
+ }
18
+ export interface PushArgs {
19
+ title: string;
20
+ tag?: string;
21
+ context?: string;
22
+ priority?: Priority;
23
+ notify?: string;
24
+ agent?: string;
25
+ session?: string;
26
+ decisions: Decision[];
27
+ }
28
+ export interface PushResult {
29
+ planId: string;
30
+ file: string;
31
+ total: number;
32
+ status: 'pending';
33
+ }
34
+ export interface DecisionStatus {
35
+ status: Status;
36
+ answer: string | null;
37
+ }
38
+ export interface StatusResult {
39
+ planId: string;
40
+ title: string;
41
+ status: Status;
42
+ total: number;
43
+ answered: number;
44
+ remaining: number;
45
+ decisions: Record<string, DecisionStatus>;
46
+ error?: string;
47
+ }
48
+ export interface GetResult {
49
+ planId: string;
50
+ status: 'completed';
51
+ completedAt: string;
52
+ answers: Record<string, string>;
53
+ error?: string;
54
+ }
55
+ export interface Frontmatter {
56
+ id: string;
57
+ version: number;
58
+ agent: string;
59
+ session: string;
60
+ tag: string;
61
+ title: string;
62
+ priority: Priority;
63
+ status: Status;
64
+ created_at: string;
65
+ updated_at: string;
66
+ completed_at: string | null;
67
+ total: number;
68
+ answered: number;
69
+ remaining: number;
70
+ notify_session?: string;
71
+ }
72
+ export interface ParsedDecision {
73
+ id: string;
74
+ status: Status;
75
+ answer: string | null;
76
+ answeredAt: string | null;
77
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for Arbiter Skill
3
+ */
4
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Utility functions for Arbiter Skill
3
+ */
4
+ import type { Frontmatter, ParsedDecision } from './types.js';
5
+ /**
6
+ * Get the base arbiter directory
7
+ */
8
+ export declare function getArbiterDir(): string;
9
+ /**
10
+ * Get the queue directory
11
+ */
12
+ export declare function getQueueDir(subdir: 'pending' | 'completed' | 'notify'): string;
13
+ /**
14
+ * Ensure queue directories exist
15
+ */
16
+ export declare function ensureQueueDirs(): void;
17
+ /**
18
+ * Slugify a string for use in filenames
19
+ */
20
+ export declare function slugify(text: string): string;
21
+ /**
22
+ * Find a plan file by ID or tag
23
+ */
24
+ export declare function findPlanFile(planId?: string, tag?: string): string | null;
25
+ /**
26
+ * Parse a plan file and extract frontmatter
27
+ */
28
+ export declare function parsePlanFile(filepath: string): {
29
+ frontmatter: Frontmatter;
30
+ content: string;
31
+ };
32
+ /**
33
+ * Parse decision blocks from markdown content
34
+ */
35
+ export declare function parseDecisions(content: string): ParsedDecision[];
36
+ /**
37
+ * Get current ISO timestamp
38
+ */
39
+ export declare function nowISO(): string;
package/dist/utils.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Utility functions for Arbiter Skill
3
+ */
4
+ import { readdirSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import matter from 'gray-matter';
8
+ /**
9
+ * Get the base arbiter directory
10
+ */
11
+ export function getArbiterDir() {
12
+ return join(homedir(), '.arbiter');
13
+ }
14
+ /**
15
+ * Get the queue directory
16
+ */
17
+ export function getQueueDir(subdir) {
18
+ return join(getArbiterDir(), 'queue', subdir);
19
+ }
20
+ /**
21
+ * Ensure queue directories exist
22
+ */
23
+ export function ensureQueueDirs() {
24
+ const dirs = ['pending', 'completed', 'notify'];
25
+ for (const dir of dirs) {
26
+ const path = getQueueDir(dir);
27
+ if (!existsSync(path)) {
28
+ mkdirSync(path, { recursive: true });
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Slugify a string for use in filenames
34
+ */
35
+ export function slugify(text) {
36
+ return text
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/^-|-$/g, '')
40
+ .slice(0, 30);
41
+ }
42
+ /**
43
+ * Find a plan file by ID or tag
44
+ */
45
+ export function findPlanFile(planId, tag) {
46
+ const dirs = [getQueueDir('pending'), getQueueDir('completed')];
47
+ for (const dir of dirs) {
48
+ if (!existsSync(dir))
49
+ continue;
50
+ const files = readdirSync(dir).filter(f => f.endsWith('.md'));
51
+ for (const file of files) {
52
+ const filepath = join(dir, file);
53
+ // Quick check by filename for planId
54
+ if (planId && file.includes(planId)) {
55
+ return filepath;
56
+ }
57
+ // Need to read file for tag match
58
+ if (tag) {
59
+ try {
60
+ const content = readFileSync(filepath, 'utf-8');
61
+ const { data } = matter(content);
62
+ if (data.tag === tag || data.id === planId) {
63
+ return filepath;
64
+ }
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Parse a plan file and extract frontmatter
76
+ */
77
+ export function parsePlanFile(filepath) {
78
+ const raw = readFileSync(filepath, 'utf-8');
79
+ const { data, content } = matter(raw);
80
+ return {
81
+ frontmatter: data,
82
+ content
83
+ };
84
+ }
85
+ /**
86
+ * Parse decision blocks from markdown content
87
+ */
88
+ export function parseDecisions(content) {
89
+ const decisions = [];
90
+ // Split by decision headers (## Decision N: ...)
91
+ const sections = content.split(/^---$/m);
92
+ for (const section of sections) {
93
+ // Look for decision metadata
94
+ const idMatch = section.match(/^id:\s*(.+)$/m);
95
+ const statusMatch = section.match(/^status:\s*(.+)$/m);
96
+ const answerMatch = section.match(/^answer:\s*(.+)$/m);
97
+ const answeredAtMatch = section.match(/^answered_at:\s*(.+)$/m);
98
+ if (idMatch) {
99
+ decisions.push({
100
+ id: idMatch[1].trim(),
101
+ status: (statusMatch?.[1]?.trim() || 'pending'),
102
+ answer: answerMatch?.[1]?.trim() === 'null' ? null : answerMatch?.[1]?.trim() || null,
103
+ answeredAt: answeredAtMatch?.[1]?.trim() === 'null' ? null : answeredAtMatch?.[1]?.trim() || null
104
+ });
105
+ }
106
+ }
107
+ return decisions;
108
+ }
109
+ /**
110
+ * Get current ISO timestamp
111
+ */
112
+ export function nowISO() {
113
+ return new Date().toISOString();
114
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "arbiter-skill",
3
+ "version": "0.1.0",
4
+ "description": "Clawdbot skill for Arbiter Zebu - async human-in-the-loop decisions",
5
+ "type": "module",
6
+ "bin": {
7
+ "arbiter-push": "./dist/push.js",
8
+ "arbiter-status": "./dist/status.js",
9
+ "arbiter-get": "./dist/get.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "push": "node dist/push.js",
14
+ "status": "node dist/status.js",
15
+ "get": "node dist/get.js"
16
+ },
17
+ "dependencies": {
18
+ "gray-matter": "^4.0.3",
19
+ "nanoid": "^5.0.4"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.3.3",
23
+ "@types/node": "^20.11.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.0.0"
27
+ },
28
+ "keywords": [
29
+ "clawdbot",
30
+ "skill",
31
+ "arbiter",
32
+ "decisions"
33
+ ],
34
+ "author": "Zebu Agency",
35
+ "license": "MIT"
36
+ }
package/scripts/get.sh ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # arbiter get - Retrieve answers from a completed decision plan
4
+ #
5
+ # Usage: get.sh <plan-id>
6
+ #
7
+ # This script will be implemented in Phase 7.
8
+ # It will read answers from completed decision files.
9
+ #
10
+
11
+ echo "🚧 arbiter get - Not yet implemented"
12
+ echo "This will retrieve answers from ~/.arbiter/queue/completed/"
13
+ exit 1
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # arbiter push - Create a decision file in the queue
4
+ #
5
+ # Usage: push.sh <tag> <title> [options]
6
+ #
7
+ # This script will be implemented in Phase 7.
8
+ # It will create a properly formatted decision file in ~/.arbiter/queue/pending/
9
+ #
10
+
11
+ echo "🚧 arbiter push - Not yet implemented"
12
+ echo "This will create decision files in ~/.arbiter/queue/pending/"
13
+ exit 1
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # arbiter status - Check status of a decision plan
4
+ #
5
+ # Usage: status.sh <plan-id>
6
+ #
7
+ # This script will be implemented in Phase 7.
8
+ # It will check the status of a decision file (pending/completed).
9
+ #
10
+
11
+ echo "🚧 arbiter status - Not yet implemented"
12
+ echo "This will check decision file status in ~/.arbiter/queue/"
13
+ exit 1
package/src/get.ts ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arbiter get - Retrieve answers from a completed decision plan
4
+ *
5
+ * Usage: arbiter-get <plan-id> [--tag <tag>]
6
+ *
7
+ * Returns JSON with:
8
+ * - planId: Plan ID
9
+ * - status: completed (or error if not complete)
10
+ * - completedAt: ISO timestamp
11
+ * - answers: Map of decision ID -> answer
12
+ */
13
+
14
+ import type { GetResult } from './types.js';
15
+ import { findPlanFile, parsePlanFile, parseDecisions } from './utils.js';
16
+
17
+ /**
18
+ * Get answers from a completed decision plan
19
+ */
20
+ function get(planId?: string, tag?: string): GetResult | { error: string } {
21
+ if (!planId && !tag) {
22
+ return { error: 'Either planId or tag is required' };
23
+ }
24
+
25
+ const file = findPlanFile(planId, tag);
26
+ if (!file) {
27
+ return { error: 'Plan not found' };
28
+ }
29
+
30
+ const { frontmatter, content } = parsePlanFile(file);
31
+
32
+ if (frontmatter.status !== 'completed') {
33
+ return {
34
+ error: `Plan not complete (status: ${frontmatter.status})`,
35
+ planId: frontmatter.id,
36
+ status: frontmatter.status as 'completed',
37
+ completedAt: '',
38
+ answers: {}
39
+ } as GetResult & { error: string };
40
+ }
41
+
42
+ const decisions = parseDecisions(content);
43
+
44
+ const answers: Record<string, string> = {};
45
+ for (const d of decisions) {
46
+ if (d.answer !== null) {
47
+ answers[d.id] = d.answer;
48
+ }
49
+ }
50
+
51
+ return {
52
+ planId: frontmatter.id,
53
+ status: 'completed',
54
+ completedAt: frontmatter.completed_at || '',
55
+ answers
56
+ };
57
+ }
58
+
59
+ // CLI entry point
60
+ function main() {
61
+ const args = process.argv.slice(2);
62
+
63
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
64
+ console.log(`
65
+ arbiter get - Retrieve answers from a completed decision plan
66
+
67
+ Usage: arbiter-get <plan-id>
68
+ arbiter-get --tag <tag>
69
+
70
+ Example:
71
+ arbiter-get abc12345
72
+ arbiter-get --tag nft-marketplace
73
+
74
+ Note: Returns error if plan is not yet completed.
75
+ `);
76
+ process.exit(0);
77
+ }
78
+
79
+ let planId: string | undefined;
80
+ let tag: string | undefined;
81
+
82
+ for (let i = 0; i < args.length; i++) {
83
+ if (args[i] === '--tag' && args[i + 1]) {
84
+ tag = args[i + 1];
85
+ i++;
86
+ } else if (!args[i].startsWith('-')) {
87
+ planId = args[i];
88
+ }
89
+ }
90
+
91
+ const result = get(planId, tag);
92
+ console.log(JSON.stringify(result, null, 2));
93
+
94
+ if ('error' in result) {
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ main();
package/src/push.ts ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * arbiter push - Create a decision file in the queue
4
+ *
5
+ * Usage: arbiter-push <json-args>
6
+ *
7
+ * The JSON should contain:
8
+ * - title: Plan title (required)
9
+ * - tag: Project tag (optional)
10
+ * - context: Context for reviewer (optional)
11
+ * - priority: low|normal|high|urgent (optional, default: normal)
12
+ * - notify: Session to notify on completion (optional)
13
+ * - agent: Agent ID (optional, uses CLAWDBOT_AGENT env)
14
+ * - session: Session key (optional, uses CLAWDBOT_SESSION env)
15
+ * - decisions: Array of decisions (required)
16
+ *
17
+ * Each decision:
18
+ * - id: Unique ID within the plan
19
+ * - title: Decision title
20
+ * - context: Context for this decision
21
+ * - options: Array of { key, label, note? }
22
+ * - allowCustom: Allow custom text answer (optional)
23
+ */
24
+
25
+ import { writeFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { nanoid } from 'nanoid';
28
+ import type { PushArgs, PushResult, Decision } from './types.js';
29
+ import { getQueueDir, ensureQueueDirs, slugify, nowISO } from './utils.js';
30
+
31
+ /**
32
+ * Generate the markdown content for a decision plan
33
+ */
34
+ function generateMarkdown(args: PushArgs & { id: string; agent: string; session: string }): string {
35
+ const now = nowISO();
36
+ const total = args.decisions.length;
37
+
38
+ // Build frontmatter
39
+ const frontmatter = `---
40
+ id: ${args.id}
41
+ version: 1
42
+ agent: ${args.agent}
43
+ session: ${args.session}
44
+ tag: ${args.tag || 'general'}
45
+ title: "${args.title}"
46
+ priority: ${args.priority || 'normal'}
47
+ status: pending
48
+ created_at: ${now}
49
+ updated_at: ${now}
50
+ completed_at: null
51
+ total: ${total}
52
+ answered: 0
53
+ remaining: ${total}
54
+ ${args.notify ? `notify_session: ${args.notify}` : ''}
55
+ ---`;
56
+
57
+ // Build context section
58
+ const contextSection = `
59
+ # ${args.title}
60
+
61
+ ${args.context || 'Please review and answer the following decisions.'}
62
+ `;
63
+
64
+ // Build decision sections
65
+ const decisionSections = args.decisions.map((d, i) => {
66
+ const optionsMarkdown = d.options
67
+ .map(o => `- \`${o.key}\` — ${o.label}${o.note ? ` (${o.note})` : ''}`)
68
+ .join('\n');
69
+
70
+ return `
71
+ ---
72
+
73
+ ## Decision ${i + 1}: ${d.title}
74
+
75
+ id: ${d.id}
76
+ status: pending
77
+ answer: null
78
+ answered_at: null
79
+ ${d.allowCustom ? 'allow_custom: true' : ''}
80
+
81
+ **Context:** ${d.context}
82
+
83
+ **Options:**
84
+ ${optionsMarkdown}
85
+ `;
86
+ }).join('\n');
87
+
88
+ return frontmatter + contextSection + decisionSections;
89
+ }
90
+
91
+ /**
92
+ * Push a decision plan to the queue
93
+ */
94
+ function push(args: PushArgs): PushResult {
95
+ // Validate required fields
96
+ if (!args.title) {
97
+ throw new Error('title is required');
98
+ }
99
+ if (!args.decisions || args.decisions.length === 0) {
100
+ throw new Error('decisions array is required and must not be empty');
101
+ }
102
+
103
+ // Validate each decision
104
+ for (const d of args.decisions) {
105
+ if (!d.id || !d.title || !d.options || d.options.length === 0) {
106
+ throw new Error(`Invalid decision: ${JSON.stringify(d)}`);
107
+ }
108
+ }
109
+
110
+ const id = nanoid(8);
111
+ const agent = args.agent || process.env.CLAWDBOT_AGENT || 'unknown';
112
+ const session = args.session || process.env.CLAWDBOT_SESSION || 'unknown';
113
+
114
+ const filename = `${agent}-${slugify(args.title)}-${id}.md`;
115
+ const filepath = join(getQueueDir('pending'), filename);
116
+
117
+ const content = generateMarkdown({
118
+ ...args,
119
+ id,
120
+ agent,
121
+ session
122
+ });
123
+
124
+ ensureQueueDirs();
125
+ writeFileSync(filepath, content, 'utf-8');
126
+
127
+ return {
128
+ planId: id,
129
+ file: filepath,
130
+ total: args.decisions.length,
131
+ status: 'pending'
132
+ };
133
+ }
134
+
135
+ // CLI entry point
136
+ function main() {
137
+ const args = process.argv.slice(2);
138
+
139
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
140
+ console.log(`
141
+ arbiter push - Create a decision file in the queue
142
+
143
+ Usage: arbiter-push '<json>'
144
+
145
+ Example:
146
+ arbiter-push '{"title":"API Decisions","decisions":[{"id":"auth","title":"Auth Method","context":"Choose auth","options":[{"key":"jwt","label":"JWT tokens"},{"key":"session","label":"Sessions"}]}]}'
147
+ `);
148
+ process.exit(0);
149
+ }
150
+
151
+ try {
152
+ const input = JSON.parse(args.join(' ')) as PushArgs;
153
+ const result = push(input);
154
+ console.log(JSON.stringify(result, null, 2));
155
+ } catch (err) {
156
+ console.error('Error:', err instanceof Error ? err.message : err);
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ main();