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.
package/src/status.ts ADDED
@@ -0,0 +1,111 @@
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
+
17
+ import type { StatusResult, DecisionStatus } from './types.js';
18
+ import { findPlanFile, parsePlanFile, parseDecisions } from './utils.js';
19
+
20
+ /**
21
+ * Get status of a decision plan
22
+ */
23
+ function status(planId?: string, tag?: string): StatusResult {
24
+ if (!planId && !tag) {
25
+ return {
26
+ planId: '',
27
+ title: '',
28
+ status: 'pending',
29
+ total: 0,
30
+ answered: 0,
31
+ remaining: 0,
32
+ decisions: {},
33
+ error: 'Either planId or tag is required'
34
+ };
35
+ }
36
+
37
+ const file = findPlanFile(planId, tag);
38
+ if (!file) {
39
+ return {
40
+ planId: planId || '',
41
+ title: '',
42
+ status: 'pending',
43
+ total: 0,
44
+ answered: 0,
45
+ remaining: 0,
46
+ decisions: {},
47
+ error: 'Plan not found'
48
+ };
49
+ }
50
+
51
+ const { frontmatter, content } = parsePlanFile(file);
52
+ const decisions = parseDecisions(content);
53
+
54
+ const decisionMap: Record<string, DecisionStatus> = {};
55
+ for (const d of decisions) {
56
+ decisionMap[d.id] = {
57
+ status: d.status,
58
+ answer: d.answer
59
+ };
60
+ }
61
+
62
+ return {
63
+ planId: frontmatter.id,
64
+ title: frontmatter.title,
65
+ status: frontmatter.status,
66
+ total: frontmatter.total,
67
+ answered: frontmatter.answered,
68
+ remaining: frontmatter.remaining,
69
+ decisions: decisionMap
70
+ };
71
+ }
72
+
73
+ // CLI entry point
74
+ function main() {
75
+ const args = process.argv.slice(2);
76
+
77
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
78
+ console.log(`
79
+ arbiter status - Check status of a decision plan
80
+
81
+ Usage: arbiter-status <plan-id>
82
+ arbiter-status --tag <tag>
83
+
84
+ Example:
85
+ arbiter-status abc12345
86
+ arbiter-status --tag nft-marketplace
87
+ `);
88
+ process.exit(0);
89
+ }
90
+
91
+ let planId: string | undefined;
92
+ let tag: string | undefined;
93
+
94
+ for (let i = 0; i < args.length; i++) {
95
+ if (args[i] === '--tag' && args[i + 1]) {
96
+ tag = args[i + 1];
97
+ i++;
98
+ } else if (!args[i].startsWith('-')) {
99
+ planId = args[i];
100
+ }
101
+ }
102
+
103
+ const result = status(planId, tag);
104
+ console.log(JSON.stringify(result, null, 2));
105
+
106
+ if (result.error) {
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ main();
package/src/types.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Type definitions for Arbiter Skill
3
+ */
4
+
5
+ export type Priority = 'low' | 'normal' | 'high' | 'urgent';
6
+ export type Status = 'pending' | 'in_progress' | 'completed';
7
+
8
+ export interface DecisionOption {
9
+ key: string;
10
+ label: string;
11
+ note?: string;
12
+ }
13
+
14
+ export interface Decision {
15
+ id: string;
16
+ title: string;
17
+ context: string;
18
+ options: DecisionOption[];
19
+ allowCustom?: boolean;
20
+ }
21
+
22
+ export interface PushArgs {
23
+ title: string;
24
+ tag?: string;
25
+ context?: string;
26
+ priority?: Priority;
27
+ notify?: string;
28
+ agent?: string;
29
+ session?: string;
30
+ decisions: Decision[];
31
+ }
32
+
33
+ export interface PushResult {
34
+ planId: string;
35
+ file: string;
36
+ total: number;
37
+ status: 'pending';
38
+ }
39
+
40
+ export interface DecisionStatus {
41
+ status: Status;
42
+ answer: string | null;
43
+ }
44
+
45
+ export interface StatusResult {
46
+ planId: string;
47
+ title: string;
48
+ status: Status;
49
+ total: number;
50
+ answered: number;
51
+ remaining: number;
52
+ decisions: Record<string, DecisionStatus>;
53
+ error?: string;
54
+ }
55
+
56
+ export interface GetResult {
57
+ planId: string;
58
+ status: 'completed';
59
+ completedAt: string;
60
+ answers: Record<string, string>;
61
+ error?: string;
62
+ }
63
+
64
+ export interface Frontmatter {
65
+ id: string;
66
+ version: number;
67
+ agent: string;
68
+ session: string;
69
+ tag: string;
70
+ title: string;
71
+ priority: Priority;
72
+ status: Status;
73
+ created_at: string;
74
+ updated_at: string;
75
+ completed_at: string | null;
76
+ total: number;
77
+ answered: number;
78
+ remaining: number;
79
+ notify_session?: string;
80
+ }
81
+
82
+ export interface ParsedDecision {
83
+ id: string;
84
+ status: Status;
85
+ answer: string | null;
86
+ answeredAt: string | null;
87
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Utility functions for Arbiter Skill
3
+ */
4
+
5
+ import { readdirSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join, basename } from 'node:path';
8
+ import matter from 'gray-matter';
9
+ import type { Frontmatter, ParsedDecision, Status } from './types.js';
10
+
11
+ /**
12
+ * Get the base arbiter directory
13
+ */
14
+ export function getArbiterDir(): string {
15
+ return join(homedir(), '.arbiter');
16
+ }
17
+
18
+ /**
19
+ * Get the queue directory
20
+ */
21
+ export function getQueueDir(subdir: 'pending' | 'completed' | 'notify'): string {
22
+ return join(getArbiterDir(), 'queue', subdir);
23
+ }
24
+
25
+ /**
26
+ * Ensure queue directories exist
27
+ */
28
+ export function ensureQueueDirs(): void {
29
+ const dirs = ['pending', 'completed', 'notify'] as const;
30
+ for (const dir of dirs) {
31
+ const path = getQueueDir(dir);
32
+ if (!existsSync(path)) {
33
+ mkdirSync(path, { recursive: true });
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Slugify a string for use in filenames
40
+ */
41
+ export function slugify(text: string): string {
42
+ return text
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9]+/g, '-')
45
+ .replace(/^-|-$/g, '')
46
+ .slice(0, 30);
47
+ }
48
+
49
+ /**
50
+ * Find a plan file by ID or tag
51
+ */
52
+ export function findPlanFile(planId?: string, tag?: string): string | null {
53
+ const dirs = [getQueueDir('pending'), getQueueDir('completed')];
54
+
55
+ for (const dir of dirs) {
56
+ if (!existsSync(dir)) continue;
57
+
58
+ const files = readdirSync(dir).filter(f => f.endsWith('.md'));
59
+
60
+ for (const file of files) {
61
+ const filepath = join(dir, file);
62
+
63
+ // Quick check by filename for planId
64
+ if (planId && file.includes(planId)) {
65
+ return filepath;
66
+ }
67
+
68
+ // Need to read file for tag match
69
+ if (tag) {
70
+ try {
71
+ const content = readFileSync(filepath, 'utf-8');
72
+ const { data } = matter(content);
73
+ if (data.tag === tag || data.id === planId) {
74
+ return filepath;
75
+ }
76
+ } catch {
77
+ continue;
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Parse a plan file and extract frontmatter
88
+ */
89
+ export function parsePlanFile(filepath: string): { frontmatter: Frontmatter; content: string } {
90
+ const raw = readFileSync(filepath, 'utf-8');
91
+ const { data, content } = matter(raw);
92
+ return {
93
+ frontmatter: data as Frontmatter,
94
+ content
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Parse decision blocks from markdown content
100
+ */
101
+ export function parseDecisions(content: string): ParsedDecision[] {
102
+ const decisions: ParsedDecision[] = [];
103
+
104
+ // Split by decision headers (## Decision N: ...)
105
+ const sections = content.split(/^---$/m);
106
+
107
+ for (const section of sections) {
108
+ // Look for decision metadata
109
+ const idMatch = section.match(/^id:\s*(.+)$/m);
110
+ const statusMatch = section.match(/^status:\s*(.+)$/m);
111
+ const answerMatch = section.match(/^answer:\s*(.+)$/m);
112
+ const answeredAtMatch = section.match(/^answered_at:\s*(.+)$/m);
113
+
114
+ if (idMatch) {
115
+ decisions.push({
116
+ id: idMatch[1].trim(),
117
+ status: (statusMatch?.[1]?.trim() || 'pending') as Status,
118
+ answer: answerMatch?.[1]?.trim() === 'null' ? null : answerMatch?.[1]?.trim() || null,
119
+ answeredAt: answeredAtMatch?.[1]?.trim() === 'null' ? null : answeredAtMatch?.[1]?.trim() || null
120
+ });
121
+ }
122
+ }
123
+
124
+ return decisions;
125
+ }
126
+
127
+ /**
128
+ * Get current ISO timestamp
129
+ */
130
+ export function nowISO(): string {
131
+ return new Date().toISOString();
132
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ id: {{id}}
3
+ version: 1
4
+ agent: {{agent}}
5
+ session: {{session}}
6
+ tag: {{tag}}
7
+ title: "{{title}}"
8
+ priority: {{priority}}
9
+ status: pending
10
+ created_at: {{created_at}}
11
+ updated_at: {{created_at}}
12
+ completed_at: null
13
+ total: {{total}}
14
+ answered: 0
15
+ remaining: {{total}}
16
+ notify_session: {{notify_session}}
17
+ ---
18
+
19
+ # {{title}}
20
+
21
+ {{context}}
22
+
23
+ ---
24
+
25
+ ## Decision 1: {{decision_1_title}}
26
+
27
+ id: {{decision_1_id}}
28
+ status: pending
29
+ answer: null
30
+ answered_at: null
31
+
32
+ **Context:** {{decision_1_context}}
33
+
34
+ **Options:**
35
+ - `option1` — Description of option 1
36
+ - `option2` — Description of option 2
37
+ - `option3` — Description of option 3 (optional note)
38
+
39
+ ---
40
+
41
+ ## Decision 2: {{decision_2_title}}
42
+
43
+ id: {{decision_2_id}}
44
+ status: pending
45
+ answer: null
46
+ answered_at: null
47
+ allow_custom: true
48
+
49
+ **Context:** {{decision_2_context}}
50
+
51
+ **Options:**
52
+ - `option1` — Description
53
+ - `option2` — Description
54
+
55
+ ---
56
+
57
+ <!--
58
+ Template variables:
59
+ - {{id}} - Plan ID (auto-generated nanoid)
60
+ - {{agent}} - Agent ID (from CLAWDBOT_AGENT env or argument)
61
+ - {{session}} - Session key (from CLAWDBOT_SESSION env or argument)
62
+ - {{tag}} - Project tag for filtering
63
+ - {{title}} - Plan title
64
+ - {{priority}} - low|normal|high|urgent
65
+ - {{created_at}} - ISO timestamp
66
+ - {{total}} - Number of decisions
67
+ - {{notify_session}} - Session to notify on completion
68
+
69
+ Each decision:
70
+ - id: Unique within the plan
71
+ - status: pending|answered
72
+ - answer: null or the chosen option key
73
+ - answered_at: null or ISO timestamp
74
+ - allow_custom: true to allow free-text answers
75
+
76
+ This template is for reference. The actual files are generated by arbiter_push.
77
+ -->
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }