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/README.md +142 -0
- package/SKILL.md +291 -0
- package/dist/get.d.ts +13 -0
- package/dist/get.js +84 -0
- package/dist/push.d.ts +24 -0
- package/dist/push.js +143 -0
- package/dist/status.d.ts +16 -0
- package/dist/status.js +98 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +4 -0
- package/dist/utils.d.ts +39 -0
- package/dist/utils.js +114 -0
- package/package.json +36 -0
- package/scripts/get.sh +13 -0
- package/scripts/push.sh +13 -0
- package/scripts/status.sh +13 -0
- package/src/get.ts +99 -0
- package/src/push.ts +161 -0
- package/src/status.ts +111 -0
- package/src/types.ts +87 -0
- package/src/utils.ts +132 -0
- package/templates/decision.md +77 -0
- package/tsconfig.json +18 -0
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
|
+
}
|