openstoat 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,254 @@
1
+ /**
2
+ * Task commands: ls, create, claim, start, done, self-unblock, show
3
+ */
4
+
5
+ import type { Argv } from 'yargs';
6
+ import {
7
+ createTask,
8
+ getTask,
9
+ listTasks,
10
+ claimTask,
11
+ startTask,
12
+ completeTask,
13
+ selfUnblockTask,
14
+ } from 'openstoat-core';
15
+ import type { TaskStatus } from 'openstoat-types';
16
+
17
+ export const taskCommands = {
18
+ command: 'task <action>',
19
+ describe:
20
+ 'Manage tasks. Planner: ls before create, then create. Worker: claim → start → done. Self-unblock when blocked by human.',
21
+ builder: (yargs: Argv) =>
22
+ yargs
23
+ .command({
24
+ command: 'ls',
25
+ describe:
26
+ 'List tasks. PLANNER: run before create to avoid duplicates. Use --status ready,in_progress for unfinished.',
27
+ builder: (y: Argv) =>
28
+ y
29
+ .option('project', {
30
+ type: 'string',
31
+ demandOption: true,
32
+ describe: 'Project ID (from project ls)',
33
+ })
34
+ .option('status', {
35
+ type: 'string',
36
+ describe: 'Filter by status (comma-separated: ready,in_progress,done)',
37
+ })
38
+ .option('owner', {
39
+ type: 'string',
40
+ describe: 'Filter by owner (agent_worker or human_worker)',
41
+ })
42
+ .option('json', {
43
+ type: 'boolean',
44
+ default: false,
45
+ describe: 'Output as JSON',
46
+ }),
47
+ handler: (argv: Record<string, unknown>) => {
48
+ const statusStr = argv.status as string | undefined;
49
+ const statuses: TaskStatus[] | undefined = statusStr
50
+ ? (statusStr.split(',').map((s) => s.trim()) as TaskStatus[])
51
+ : undefined;
52
+
53
+ const tasks = listTasks(argv.project as string, {
54
+ status: statuses,
55
+ owner: argv.owner as string | undefined,
56
+ });
57
+
58
+ if (argv.json) {
59
+ console.log(JSON.stringify(tasks, null, 2));
60
+ } else {
61
+ if (tasks.length === 0) {
62
+ console.log('No tasks found.');
63
+ return;
64
+ }
65
+ for (const t of tasks) {
66
+ console.log(`${t.id}\t${t.status}\t${t.owner}\t${t.title}`);
67
+ }
68
+ }
69
+ },
70
+ })
71
+ .command({
72
+ command: 'create',
73
+ describe:
74
+ 'Create a task. All required: --project, --title, --description, --acceptance-criteria (×N), --status, --owner, --task-type. Use --depends-on for dependencies.',
75
+ builder: (y: Argv) =>
76
+ y
77
+ .option('project', { type: 'string', demandOption: true })
78
+ .option('title', { type: 'string', demandOption: true })
79
+ .option('description', { type: 'string', demandOption: true })
80
+ .option('acceptance-criteria', {
81
+ type: 'array',
82
+ demandOption: true,
83
+ describe: 'Pass multiple times: --acceptance-criteria "A" --acceptance-criteria "B"',
84
+ })
85
+ .option('depends-on', {
86
+ type: 'array',
87
+ default: [],
88
+ describe: 'Task IDs this task depends on; omit when no dependencies',
89
+ })
90
+ .option('status', {
91
+ type: 'string',
92
+ demandOption: true,
93
+ choices: ['ready', 'in_progress', 'done'],
94
+ })
95
+ .option('owner', {
96
+ type: 'string',
97
+ demandOption: true,
98
+ choices: ['agent_worker', 'human_worker'],
99
+ })
100
+ .option('task-type', {
101
+ type: 'string',
102
+ demandOption: true,
103
+ choices: ['implementation', 'testing', 'review', 'credentials', 'deploy', 'docs', 'custom'],
104
+ describe:
105
+ 'implementation|testing|review|credentials|deploy|docs|custom. credentials/review/deploy often human_worker.',
106
+ })
107
+ .option('created-by', { type: 'string', describe: 'Creator role (e.g. agent_planner)' }),
108
+ handler: (argv: Record<string, unknown>) => {
109
+ const task = createTask({
110
+ project: argv.project as string,
111
+ title: argv.title as string,
112
+ description: argv.description as string,
113
+ acceptance_criteria: (argv['acceptance-criteria'] as string[]) || [],
114
+ depends_on: (argv['depends-on'] as string[]) || [],
115
+ status: argv.status as TaskStatus,
116
+ owner: argv.owner as 'agent_worker' | 'human_worker',
117
+ task_type: argv['task-type'] as any,
118
+ created_by: argv['created-by'] as string | undefined,
119
+ });
120
+ console.log(task.id);
121
+ },
122
+ })
123
+ .command({
124
+ command: 'claim <task_id>',
125
+ describe: 'WORKER: claim ready task. Moves ready→in_progress. Use --logs-append for audit.',
126
+ builder: (y: Argv) =>
127
+ y
128
+ .positional('task_id', { type: 'string', demandOption: true })
129
+ .option('as', {
130
+ type: 'string',
131
+ demandOption: true,
132
+ choices: ['agent_worker', 'human_worker'],
133
+ })
134
+ .option('logs-append', { type: 'string', describe: 'Append to task logs (required for agents)' }),
135
+ handler: (argv: Record<string, unknown>) => {
136
+ const task = claimTask(
137
+ argv.task_id as string,
138
+ argv.as as 'agent_worker' | 'human_worker',
139
+ argv['logs-append'] as string | undefined
140
+ );
141
+ console.log(`Claimed ${task.id}`);
142
+ },
143
+ })
144
+ .command({
145
+ command: 'start <task_id>',
146
+ describe: 'WORKER: start/continue task. Use --logs-append to record progress.',
147
+ builder: (y: Argv) =>
148
+ y
149
+ .positional('task_id', { type: 'string', demandOption: true })
150
+ .option('as', {
151
+ type: 'string',
152
+ demandOption: true,
153
+ choices: ['agent_worker', 'human_worker'],
154
+ })
155
+ .option('logs-append', { type: 'string' }),
156
+ handler: (argv: Record<string, unknown>) => {
157
+ const task = startTask(
158
+ argv.task_id as string,
159
+ argv.as as 'agent_worker' | 'human_worker',
160
+ argv['logs-append'] as string | undefined
161
+ );
162
+ console.log(`Started ${task.id}`);
163
+ },
164
+ })
165
+ .command({
166
+ command: 'done <task_id>',
167
+ describe: 'WORKER: complete task. Handoff mandatory (min 200 chars). Transfers context to downstream.',
168
+ builder: (y: Argv) =>
169
+ y
170
+ .positional('task_id', { type: 'string', demandOption: true })
171
+ .option('output', { type: 'string', demandOption: true, describe: 'What was delivered' })
172
+ .option('handoff-summary', {
173
+ type: 'string',
174
+ demandOption: true,
175
+ describe: 'Context for downstream tasks. Min 200 chars. No credentials in body.',
176
+ })
177
+ .option('logs-append', { type: 'string' })
178
+ .option('as', {
179
+ type: 'string',
180
+ demandOption: true,
181
+ choices: ['agent_worker', 'human_worker'],
182
+ }),
183
+ handler: (argv: Record<string, unknown>) => {
184
+ const task = completeTask(
185
+ argv.task_id as string,
186
+ {
187
+ output: argv.output as string,
188
+ handoff_summary: argv['handoff-summary'] as string,
189
+ logs_append: argv['logs-append'] as string | undefined,
190
+ },
191
+ argv.as as 'agent_worker' | 'human_worker'
192
+ );
193
+ console.log(`Done ${task.id}`);
194
+ },
195
+ })
196
+ .command({
197
+ command: 'self-unblock <task_id>',
198
+ describe:
199
+ 'WORKER: rollback in_progress→ready when blocked. Must add --depends-on (human task). Create human task first.',
200
+ builder: (y: Argv) =>
201
+ y
202
+ .positional('task_id', { type: 'string', demandOption: true })
203
+ .option('depends-on', {
204
+ type: 'array',
205
+ demandOption: true,
206
+ describe: 'New human task ID(s) - at least one required',
207
+ })
208
+ .option('logs-append', { type: 'string' })
209
+ .option('as', {
210
+ type: 'string',
211
+ default: 'agent_worker',
212
+ choices: ['agent_worker'],
213
+ }),
214
+ handler: (argv: Record<string, unknown>) => {
215
+ const deps = argv['depends-on'] as string[];
216
+ const task = selfUnblockTask(
217
+ argv.task_id as string,
218
+ {
219
+ depends_on: deps,
220
+ logs_append: argv['logs-append'] as string | undefined,
221
+ },
222
+ 'agent_worker'
223
+ );
224
+ console.log(`Self-unblocked ${task.id}`);
225
+ },
226
+ })
227
+ .command({
228
+ command: 'show <task_id>',
229
+ describe: 'Show task details. Use --json for machine-readable. Read handoff from dependent tasks.',
230
+ builder: (y: Argv) =>
231
+ y
232
+ .positional('task_id', { type: 'string', demandOption: true })
233
+ .option('json', { type: 'boolean', default: false }),
234
+ handler: (argv: Record<string, unknown>) => {
235
+ const task = getTask(argv.task_id as string);
236
+ if (!task) {
237
+ console.error(`Task '${argv.task_id}' not found.`);
238
+ process.exit(1);
239
+ }
240
+ console.log(argv.json ? JSON.stringify(task, null, 2) : formatTask(task));
241
+ },
242
+ })
243
+ .demandCommand(1, 'Specify ls, create, claim, start, done, self-unblock, or show'),
244
+ handler: () => {},
245
+ };
246
+
247
+ function formatTask(t: { id: string; title: string; status: string; owner: string; logs: string[] }): string {
248
+ let out = `ID: ${t.id}\nTitle: ${t.title}\nStatus: ${t.status}\nOwner: ${t.owner}\n`;
249
+ if (t.logs.length > 0) {
250
+ out += 'Logs:\n';
251
+ for (const log of t.logs) out += ` - ${log}\n`;
252
+ }
253
+ return out;
254
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * OpenStoat CLI - Agent super-manual for task orchestration
4
+ * CLI is the operational guide for both agents and humans.
5
+ */
6
+
7
+ import yargs from 'yargs';
8
+ import { hideBin } from 'yargs/helpers';
9
+ import { projectCommands } from './commands/project.js';
10
+ import { taskCommands } from './commands/task.js';
11
+ import { daemonCommands } from './commands/daemon.js';
12
+ import { installCommands } from './commands/install.js';
13
+ import { AGENT_MANUAL } from './agent-manual.js';
14
+
15
+ const cli = yargs(hideBin(process.argv))
16
+ .scriptName('openstoat')
17
+ .usage(
18
+ 'Usage: $0 <command> [options]\n\n' +
19
+ 'OpenStoat orchestrates tasks between AI agents and humans. Local-first, CLI-first.\n' +
20
+ 'Storage: ~/.openstoat/ (SQLite). No account/API Key required.\n\n' +
21
+ 'WHEN TO USE: Planning work, creating tasks, claiming/executing tasks, handing off to humans,\n' +
22
+ 'self-unblocking when blocked. Run \'openstoat manual\' for full agent operational manual.\n\n' +
23
+ 'Commands:\n' +
24
+ ' project init Initialize project (--id, --name, --template required)\n' +
25
+ ' project ls List projects (get --project IDs for task commands)\n' +
26
+ ' project show <id> Show project details\n' +
27
+ ' task ls List tasks (PLANNER: run before create to avoid duplicates)\n' +
28
+ ' task create Create task (all required: --project, --title, --description,\n' +
29
+ ' --acceptance-criteria, --status, --owner, --task-type)\n' +
30
+ ' task claim <id> WORKER: claim ready task (--as agent_worker|human_worker)\n' +
31
+ ' task start <id> WORKER: start working (append to logs)\n' +
32
+ ' task done <id> WORKER: complete (--output, --handoff-summary min 200 chars)\n' +
33
+ ' task self-unblock WORKER: rollback when blocked (--depends-on human_task required)\n' +
34
+ ' task show <id> Show task details\n' +
35
+ ' daemon init Interactively create .openstoat.json\n' +
36
+ ' daemon start Start worker daemon (polls for ready agent_worker tasks)\n' +
37
+ ' install skill Install planner and worker skills (--here for current dir)\n' +
38
+ ' web Start Web UI server (http://localhost:3080)\n' +
39
+ ' manual Print full agent operational manual (SKILL-style)'
40
+ )
41
+ .command({
42
+ command: 'web',
43
+ describe: 'Start Web UI server. Opens http://localhost:3080 (set PORT env to change).',
44
+ handler: async () => {
45
+ await import('openstoat-web');
46
+ },
47
+ })
48
+ .command({
49
+ command: 'manual',
50
+ describe: 'Print full agent operational manual. Use when agent needs detailed workflow, rules, and examples.',
51
+ handler: () => {
52
+ console.log(AGENT_MANUAL.trim());
53
+ },
54
+ })
55
+ .command(projectCommands)
56
+ .command(taskCommands)
57
+ .command(daemonCommands)
58
+ .command(installCommands)
59
+ .demandCommand(1, 'Specify a command. Run openstoat --help or openstoat manual for details.')
60
+ .strict()
61
+ .help()
62
+ .alias('h', 'help')
63
+ .version('0.1.0')
64
+ .alias('v', 'version')
65
+ .epilog(
66
+ 'RULES: (1) Planner must run task ls before create. (2) Handoff required on every done (min 200 chars). ' +
67
+ '(3) No generic status update; use claim/start/done/self-unblock only. (4) Self-unblock requires --depends-on.'
68
+ );
69
+
70
+ cli.parse();
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Install OpenStoat skills to .agent/skills and .claude/skills in the target directory.
3
+ * Used by `openstoat install skill` and when daemon starts.
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { createRequire } from 'module';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ const SKILL_NAMES = ['openstoat-planner', 'openstoat-worker'];
14
+ const TARGET_DIRS_DEFAULT = ['.agent/skills', '.claude/skills'];
15
+ const TARGET_DIRS_HERE = [''];
16
+
17
+ /**
18
+ * Get the path to the openstoat-skills package skills directory.
19
+ */
20
+ function getSkillsSourcePath(): string {
21
+ try {
22
+ // Resolve from openstoat-cli's node_modules (workspace or hoisted)
23
+ const pkgPath = require.resolve('openstoat-skills/package.json', {
24
+ paths: [path.dirname(fileURLToPath(import.meta.url))],
25
+ });
26
+ return path.join(path.dirname(pkgPath), 'skills');
27
+ } catch {
28
+ // Fallback: relative to this file (e.g. in monorepo packages/openstoat-cli)
29
+ const dir = path.dirname(fileURLToPath(import.meta.url));
30
+ return path.join(dir, '../../openstoat-skills/skills');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Copy a skill directory recursively.
36
+ */
37
+ function copySkill(source: string, dest: string): void {
38
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
39
+ fs.cpSync(source, dest, { recursive: true });
40
+ }
41
+
42
+ export interface InstallSkillsOptions {
43
+ /** When true, install to current directory (no skills/, .agent, or .claude) */
44
+ here?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Install OpenStoat skills to .agent/skills and .claude/skills, or current dir when here=true.
49
+ * @param targetRoot - Root directory (default: process.cwd())
50
+ * @param options - { here: true } to install to current directory (./openstoat-planner, ./openstoat-worker)
51
+ * @returns Paths where skills were installed
52
+ */
53
+ export function installSkills(
54
+ targetRoot = process.cwd(),
55
+ options?: InstallSkillsOptions
56
+ ): string[] {
57
+ const sourcePath = getSkillsSourcePath();
58
+ const installed: string[] = [];
59
+ const targetDirs = options?.here ? TARGET_DIRS_HERE : TARGET_DIRS_DEFAULT;
60
+
61
+ for (const skillName of SKILL_NAMES) {
62
+ const skillSource = path.join(sourcePath, skillName);
63
+ if (!fs.existsSync(skillSource)) {
64
+ console.warn(`Skill not found: ${skillName} at ${skillSource}`);
65
+ continue;
66
+ }
67
+
68
+ for (const targetDir of targetDirs) {
69
+ const dest = path.join(targetRoot, targetDir, skillName);
70
+ copySkill(skillSource, dest);
71
+ installed.push(dest);
72
+ }
73
+ }
74
+
75
+ return installed;
76
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Interactive prompt helper using readline.
3
+ */
4
+
5
+ import { createInterface } from 'readline';
6
+
7
+ export function prompt(question: string, defaultValue = ''): Promise<string> {
8
+ return new Promise((resolve) => {
9
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
10
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
11
+ rl.question(`${question}${suffix}: `, (answer) => {
12
+ rl.close();
13
+ const trimmed = answer.trim();
14
+ resolve(trimmed || defaultValue);
15
+ });
16
+ });
17
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*"]
9
+ }