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/dist/status.d.ts
ADDED
|
@@ -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();
|
package/dist/types.d.ts
ADDED
|
@@ -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
package/dist/utils.d.ts
ADDED
|
@@ -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
|
package/scripts/push.sh
ADDED
|
@@ -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();
|