@polderlabs/openkan 0.4.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/CHANGELOG.md +226 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/agents/openkan.md +254 -0
- package/bin/install-agent.mjs +63 -0
- package/bin/ok.mjs +17 -0
- package/bin/openkan.mjs +10 -0
- package/dist/.claude/skills/ok-planning/SKILL.md +285 -0
- package/dist/.claude/skills/ok-planning/references/integration.md +153 -0
- package/dist/.claude/skills/ok-planning/references/schemas.md +270 -0
- package/dist/.claude/skills/ok-planning/references/workflows.md +185 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-init.sh +14 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-resume.sh +38 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-status.sh +24 -0
- package/dist/agents/openkan.md +254 -0
- package/dist/bin/install-agent.mjs +76 -0
- package/dist/bin/ok-install.js +58 -0
- package/dist/bin/ok.js +138 -0
- package/dist/bin/openkan.js +804 -0
- package/dist/commands/organize.md +15 -0
- package/dist/kanban/agent-profile.js +8 -0
- package/dist/kanban/archive.js +49 -0
- package/dist/kanban/bizar.js +242 -0
- package/dist/kanban/board.js +367 -0
- package/dist/kanban/bulk.js +139 -0
- package/dist/kanban/changelog.js +186 -0
- package/dist/kanban/chat.js +1280 -0
- package/dist/kanban/claude-state.js +974 -0
- package/dist/kanban/comments.js +80 -0
- package/dist/kanban/docs.js +144 -0
- package/dist/kanban/fs.js +163 -0
- package/dist/kanban/git.js +196 -0
- package/dist/kanban/images.js +140 -0
- package/dist/kanban/import.js +295 -0
- package/dist/kanban/inputs.js +94 -0
- package/dist/kanban/insights.js +140 -0
- package/dist/kanban/io.js +75 -0
- package/dist/kanban/mdx-render.js +348 -0
- package/dist/kanban/mdx.js +231 -0
- package/dist/kanban/projects.js +545 -0
- package/dist/kanban/search.js +121 -0
- package/dist/kanban/server.js +3296 -0
- package/dist/kanban/tags.js +124 -0
- package/dist/kanban/template.js +145 -0
- package/dist/kanban/tsx-sandbox.js +187 -0
- package/dist/kanban/watcher.js +270 -0
- package/dist/ok/commands/goal.js +65 -0
- package/dist/ok/commands/index.js +87 -0
- package/dist/ok/commands/init.js +15 -0
- package/dist/ok/commands/plan.js +155 -0
- package/dist/ok/commands/prd.js +202 -0
- package/dist/ok/commands/progress.js +31 -0
- package/dist/ok/commands/task.js +377 -0
- package/dist/ok/ids.js +98 -0
- package/dist/ok/lock.js +156 -0
- package/dist/ok/migrate.js +197 -0
- package/dist/ok/schemas.js +402 -0
- package/dist/ok/storage.js +222 -0
- package/dist/skills/openkan/SKILL.md +111 -0
- package/dist/skills/openkan/agents/openai.yaml +4 -0
- package/dist/skills/openkan/examples/simple-task.mdx +34 -0
- package/dist/skills/openkan/examples/with-ask.mdx +32 -0
- package/dist/skills/openkan/examples/with-choice.mdx +51 -0
- package/dist/skills/openkan/examples/with-preview.mdx +54 -0
- package/dist/skills/openkan/references/api.md +169 -0
- package/dist/skills/openkan/templates/task.mdx +46 -0
- package/dist/web/api.js +257 -0
- package/dist/web/app.js +4251 -0
- package/dist/web/bizar.js +39 -0
- package/dist/web/brand/agent-activity-sprite.svg +1 -0
- package/dist/web/brand/banner-docs.svg +24 -0
- package/dist/web/brand/banner.svg +32 -0
- package/dist/web/brand/empty-sessions.svg +17 -0
- package/dist/web/brand/empty-tasks.svg +17 -0
- package/dist/web/brand/favicon.svg +9 -0
- package/dist/web/brand/infinity-loader-animated.svg +220 -0
- package/dist/web/brand/infinity-loader-spritesheet.svg +230 -0
- package/dist/web/brand/logo-wordmark.svg +10 -0
- package/dist/web/brand/logo.svg +9 -0
- package/dist/web/brand/pixel-infinity-track.svg +1 -0
- package/dist/web/brand/social-card.svg +26 -0
- package/dist/web/changelog-view.js +456 -0
- package/dist/web/charts.js +269 -0
- package/dist/web/chat-sidebar.js +2397 -0
- package/dist/web/chat-status-motion.js +154 -0
- package/dist/web/claude-pane.js +820 -0
- package/dist/web/command-palette.js +381 -0
- package/dist/web/contributors-view.js +317 -0
- package/dist/web/cross-tab.js +102 -0
- package/dist/web/docs-view.js +168 -0
- package/dist/web/experience.css +165 -0
- package/dist/web/goals-view.js +45 -0
- package/dist/web/home-view.js +113 -0
- package/dist/web/images.js +311 -0
- package/dist/web/index.html +485 -0
- package/dist/web/insights.js +217 -0
- package/dist/web/keyboard.js +446 -0
- package/dist/web/mdx-viewer.js +600 -0
- package/dist/web/path-picker.js +787 -0
- package/dist/web/preview-frame.html +187 -0
- package/dist/web/settings.js +582 -0
- package/dist/web/style.css +8545 -0
- package/dist/web/task-view.js +1759 -0
- package/dist/web/vendor/gsap.min.js +11 -0
- package/dist/web/workspace.css +1513 -0
- package/package.json +71 -0
- package/skills/openkan/SKILL.md +111 -0
- package/skills/openkan/agents/openai.yaml +4 -0
- package/skills/openkan/examples/simple-task.mdx +34 -0
- package/skills/openkan/examples/with-ask.mdx +32 -0
- package/skills/openkan/examples/with-choice.mdx +51 -0
- package/skills/openkan/examples/with-preview.mdx +54 -0
- package/skills/openkan/references/api.md +169 -0
- package/skills/openkan/templates/task.mdx +46 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// ok/commands/prd.ts — `ok prd add|list|show|update`.
|
|
2
|
+
import { touch, byUpdatedDesc, } from "../schemas.js";
|
|
3
|
+
import { initIfMissing, readPrd, writePrd, listPrds, readConfig, rebuildIndex, paths as computePaths, } from "../storage.js";
|
|
4
|
+
import { newId, nowIso, parseArgs, flagString, flagCsv, flagBool } from "../ids.js";
|
|
5
|
+
const STATUSES = ["draft", "active", "shipped", "abandoned"];
|
|
6
|
+
const GOAL_STATUSES = ["open", "in_progress", "met", "dropped"];
|
|
7
|
+
const MILESTONE_STATUSES = ["open", "hit", "missed", "dropped"];
|
|
8
|
+
function parsePrdStatus(v) {
|
|
9
|
+
if (!v)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!STATUSES.includes(v))
|
|
12
|
+
throw new Error(`status must be one of ${STATUSES.join("|")}`);
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
function parseGoalStatus(v) {
|
|
16
|
+
if (!v)
|
|
17
|
+
throw new Error("goal status required");
|
|
18
|
+
if (!GOAL_STATUSES.includes(v)) {
|
|
19
|
+
throw new Error(`goal status must be one of ${GOAL_STATUSES.join("|")}`);
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
function parseMilestoneStatus(v) {
|
|
24
|
+
if (!v)
|
|
25
|
+
throw new Error("milestone status required");
|
|
26
|
+
if (!MILESTONE_STATUSES.includes(v)) {
|
|
27
|
+
throw new Error(`milestone status must be one of ${MILESTONE_STATUSES.join("|")}`);
|
|
28
|
+
}
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
async function paths() {
|
|
32
|
+
const p = computePaths(process.cwd());
|
|
33
|
+
if (!(await readConfig(p))) {
|
|
34
|
+
return initIfMissing(process.cwd());
|
|
35
|
+
}
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
function printTable(rows) {
|
|
39
|
+
if (rows.length === 0) {
|
|
40
|
+
process.stdout.write("(no PRDs)\n");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const w = Math.max(2, ...rows.map((r) => r.id.length));
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
process.stdout.write(`${r.id.padEnd(w)} ${r.status.padEnd(10)} ${r.updatedAt} ${r.title}\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function runPrd(args) {
|
|
49
|
+
const [sub, ...rest] = args;
|
|
50
|
+
switch (sub) {
|
|
51
|
+
case "add": return cmdPrdAdd(rest);
|
|
52
|
+
case "list": return cmdPrdList(rest);
|
|
53
|
+
case "show": return cmdPrdShow(rest);
|
|
54
|
+
case "update": return cmdPrdUpdate(rest);
|
|
55
|
+
default:
|
|
56
|
+
process.stderr.write("usage: ok prd <add|list|show|update>\n");
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function cmdPrdAdd(args) {
|
|
61
|
+
const { positionals, flags } = parseArgs(args);
|
|
62
|
+
if (positionals.length === 0) {
|
|
63
|
+
process.stderr.write("usage: ok prd add <title> [--vision …] [--goals g1|g2|g3] [--non-goals …] [--milestones m1|m2]\n");
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
const title = positionals.join(" ");
|
|
67
|
+
const vision = flagString(flags, "vision") ?? "";
|
|
68
|
+
if (!vision) {
|
|
69
|
+
process.stderr.write("--vision is required (one paragraph)\n");
|
|
70
|
+
return 2;
|
|
71
|
+
}
|
|
72
|
+
const goalTexts = flagCsv(flags, "goals");
|
|
73
|
+
const nonGoals = flagCsv(flags, "non-goals");
|
|
74
|
+
const metricSpecs = flagCsv(flags, "metrics");
|
|
75
|
+
const milestoneTexts = flagCsv(flags, "milestones");
|
|
76
|
+
const owners = flagCsv(flags, "owners");
|
|
77
|
+
const cadence = flagString(flags, "review-cadence");
|
|
78
|
+
const p = await paths();
|
|
79
|
+
const now = nowIso();
|
|
80
|
+
const prd = {
|
|
81
|
+
schema: "ok.prd.v1",
|
|
82
|
+
id: newId("prd"),
|
|
83
|
+
title,
|
|
84
|
+
vision,
|
|
85
|
+
goals: goalTexts.map((text, i) => ({
|
|
86
|
+
id: `g${i + 1}`,
|
|
87
|
+
text,
|
|
88
|
+
status: "open",
|
|
89
|
+
})),
|
|
90
|
+
nonGoals,
|
|
91
|
+
successMetrics: metricSpecs.map((spec) => {
|
|
92
|
+
const [name, target, current] = spec.split("|");
|
|
93
|
+
return {
|
|
94
|
+
name: name ?? spec,
|
|
95
|
+
target: target ?? "",
|
|
96
|
+
current: current || undefined,
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
milestones: milestoneTexts.map((text, i) => ({
|
|
100
|
+
id: `m${i + 1}`,
|
|
101
|
+
title: text,
|
|
102
|
+
status: "open",
|
|
103
|
+
})),
|
|
104
|
+
risks: [],
|
|
105
|
+
plans: [],
|
|
106
|
+
owners: owners.length ? owners : flagString(flags, "owner") ? [flagString(flags, "owner")] : [],
|
|
107
|
+
status: "draft",
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
};
|
|
111
|
+
if (cadence)
|
|
112
|
+
prd.reviewCadence = cadence;
|
|
113
|
+
await writePrd(p, prd);
|
|
114
|
+
await rebuildIndex(p);
|
|
115
|
+
process.stdout.write(`${prd.id}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
async function cmdPrdList(args) {
|
|
119
|
+
const { flags } = parseArgs(args);
|
|
120
|
+
const p = await paths();
|
|
121
|
+
let prds = await listPrds(p);
|
|
122
|
+
const status = parsePrdStatus(flagString(flags, "status"));
|
|
123
|
+
if (status)
|
|
124
|
+
prds = prds.filter((pr) => pr.status === status);
|
|
125
|
+
prds = byUpdatedDesc(prds);
|
|
126
|
+
if (flagBool(flags, "json")) {
|
|
127
|
+
process.stdout.write(JSON.stringify(prds, null, 2) + "\n");
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
printTable(prds.map((pr) => ({ id: pr.id, status: pr.status, title: pr.title, updatedAt: pr.updatedAt })));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
async function cmdPrdShow(args) {
|
|
134
|
+
const { positionals, flags } = parseArgs(args);
|
|
135
|
+
if (positionals.length !== 1) {
|
|
136
|
+
process.stderr.write("usage: ok prd show <id> [--json]\n");
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
const p = await paths();
|
|
140
|
+
const prd = await readPrd(p, positionals[0]);
|
|
141
|
+
if (!prd) {
|
|
142
|
+
process.stderr.write(`no such PRD: ${positionals[0]}\n`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (flagBool(flags, "json")) {
|
|
146
|
+
process.stdout.write(JSON.stringify(prd, null, 2) + "\n");
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
for (const [k, v] of Object.entries(prd)) {
|
|
150
|
+
process.stdout.write(`${k}: ${typeof v === "string" ? v : JSON.stringify(v)}\n`);
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
async function cmdPrdUpdate(args) {
|
|
155
|
+
const { positionals, flags } = parseArgs(args);
|
|
156
|
+
if (positionals.length !== 1) {
|
|
157
|
+
process.stderr.write("usage: ok prd update <id> [--status …] [--goal g1 --goal-status met] [--milestone m1 --milestone-status hit] [--append-plan pln-…] [--review-cadence weekly]\n");
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
const p = await paths();
|
|
161
|
+
const existing = await readPrd(p, positionals[0]);
|
|
162
|
+
if (!existing) {
|
|
163
|
+
process.stderr.write(`no such PRD: ${positionals[0]}\n`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
let next = touch(existing);
|
|
167
|
+
const status = parsePrdStatus(flagString(flags, "status"));
|
|
168
|
+
if (status)
|
|
169
|
+
next.status = status;
|
|
170
|
+
const goalId = flagString(flags, "goal");
|
|
171
|
+
const goalStatusRaw = flagString(flags, "goal-status");
|
|
172
|
+
if (goalId) {
|
|
173
|
+
const goalStatus = parseGoalStatus(goalStatusRaw);
|
|
174
|
+
next = {
|
|
175
|
+
...next,
|
|
176
|
+
goals: next.goals.map((g) => (g.id === goalId ? { ...g, status: goalStatus } : g)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const milestoneId = flagString(flags, "milestone");
|
|
180
|
+
const milestoneStatusRaw = flagString(flags, "milestone-status");
|
|
181
|
+
if (milestoneId) {
|
|
182
|
+
const milestoneStatus = parseMilestoneStatus(milestoneStatusRaw);
|
|
183
|
+
next = {
|
|
184
|
+
...next,
|
|
185
|
+
milestones: next.milestones.map((m) => (m.id === milestoneId ? { ...m, status: milestoneStatus } : m)),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const appendPlan = flagString(flags, "append-plan");
|
|
189
|
+
if (appendPlan && !next.plans.includes(appendPlan)) {
|
|
190
|
+
next.plans = [...next.plans, appendPlan];
|
|
191
|
+
}
|
|
192
|
+
const cadence = flagString(flags, "review-cadence");
|
|
193
|
+
if (cadence !== undefined)
|
|
194
|
+
next.reviewCadence = cadence;
|
|
195
|
+
const nextReview = flagString(flags, "next-review");
|
|
196
|
+
if (nextReview !== undefined)
|
|
197
|
+
next.nextReviewAt = nextReview;
|
|
198
|
+
await writePrd(p, next);
|
|
199
|
+
await rebuildIndex(p);
|
|
200
|
+
process.stdout.write(`${next.id}\n`);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { paths, listTasks, listPlans, listPrds, readPrd } from "../storage.js";
|
|
2
|
+
import { parseArgs, flagString, flagBool } from "../ids.js";
|
|
3
|
+
export async function runProgress(args) {
|
|
4
|
+
const { flags } = parseArgs(args);
|
|
5
|
+
const p = paths(process.cwd());
|
|
6
|
+
const prdId = flagString(flags, 'prd');
|
|
7
|
+
if (prdId && !await readPrd(p, prdId))
|
|
8
|
+
throw new Error(`no such PRD: ${prdId}`);
|
|
9
|
+
const [allTasks, allPlans, allPrds] = await Promise.all([listTasks(p), listPlans(p), listPrds(p)]);
|
|
10
|
+
const prds = allPrds.filter(item => !prdId || item.id === prdId);
|
|
11
|
+
const plans = allPlans.filter(item => !prdId || item.prd === prdId || prds.some(prd => prd.plans.includes(item.id)));
|
|
12
|
+
const tasks = allTasks.filter(item => !prdId || item.prd === prdId || plans.some(plan => plan.id === item.plan || plan.tasks.includes(item.id)));
|
|
13
|
+
const summary = (items, states, done, excluded) => {
|
|
14
|
+
const counts = Object.fromEntries(states.map(state => [state, 0]));
|
|
15
|
+
for (const item of items)
|
|
16
|
+
counts[item.status] = (counts[item.status] || 0) + 1;
|
|
17
|
+
const total = items.length;
|
|
18
|
+
const actionable = total - excluded.reduce((sum, state) => sum + (counts[state] || 0), 0);
|
|
19
|
+
return { ...counts, total, percentComplete: actionable ? Math.round((counts[done] || 0) / actionable * 100) : 0 };
|
|
20
|
+
};
|
|
21
|
+
const report = {
|
|
22
|
+
project: process.cwd(), prd: prdId || null,
|
|
23
|
+
tasks: summary(tasks, ['pending', 'in_progress', 'review', 'done', 'cancelled'], 'done', ['cancelled']),
|
|
24
|
+
plans: summary(plans, ['draft', 'active', 'blocked', 'complete', 'abandoned'], 'complete', ['abandoned']),
|
|
25
|
+
goals: summary(prds.flatMap(prd => prd.goals), ['open', 'in_progress', 'met', 'dropped'], 'met', ['dropped']),
|
|
26
|
+
prds: summary(prds, ['draft', 'active', 'shipped', 'abandoned'], 'shipped', ['abandoned']),
|
|
27
|
+
nextTasks: tasks.filter(task => task.status === 'pending' && (task.deps || []).every(id => allTasks.some(dependency => dependency.id === id && dependency.status === 'done'))).map(task => ({ id: task.id, title: task.title, owner: task.owner || null })),
|
|
28
|
+
};
|
|
29
|
+
process.stdout.write(flagBool(flags, 'json') ? `${JSON.stringify(report, null, 2)}\n` : `Project: ${report.project}\n${Object.entries(report).filter(([name]) => ['tasks', 'plans', 'goals', 'prds'].includes(name)).map(([name, value]) => `${name}: ${value.total} total, ${value.percentComplete}% complete`).join('\n')}\nReady tasks: ${report.nextTasks.map(task => task.id).join(', ') || 'none'}\n`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// ok/commands/task.ts — `ok task add|list|show|update|claim|heartbeat|complete|cancel`.
|
|
2
|
+
import { touch, byUpdatedDesc, } from "../schemas.js";
|
|
3
|
+
import { initIfMissing, readTask, writeTask, listTasks, readConfig, rebuildIndex, paths as computePaths, } from "../storage.js";
|
|
4
|
+
import { newId, nowIso, parseArgs, flagString, flagCsv, flagBool, runMain } from "../ids.js";
|
|
5
|
+
import { claim, heartbeat, release, assertUsable, LockHeldError } from "../lock.js";
|
|
6
|
+
const STATUSES = ["pending", "in_progress", "review", "done", "cancelled"];
|
|
7
|
+
const PRIORITIES = ["p0", "p1", "p2", "p3"];
|
|
8
|
+
function parseStatus(v) {
|
|
9
|
+
if (!v)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!STATUSES.includes(v)) {
|
|
12
|
+
throw new Error(`status must be one of ${STATUSES.join("|")}`);
|
|
13
|
+
}
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
function parsePriority(v) {
|
|
17
|
+
if (!v)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (!PRIORITIES.includes(v)) {
|
|
20
|
+
throw new Error(`priority must be one of ${PRIORITIES.join("|")}`);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
async function paths() {
|
|
25
|
+
const p = computePaths(process.cwd());
|
|
26
|
+
// Auto-init if a session invokes commands but hasn't run `ok init` yet.
|
|
27
|
+
if (!await readConfig(p)) {
|
|
28
|
+
return initIfMissing(process.cwd());
|
|
29
|
+
}
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
function printTable(rows) {
|
|
33
|
+
if (rows.length === 0) {
|
|
34
|
+
process.stdout.write("(no tasks)\n");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const w = Math.max(2, ...rows.map((r) => r.id.length));
|
|
38
|
+
const ts = Math.max(10, ...rows.map((r) => r.updatedAt.length));
|
|
39
|
+
for (const r of rows) {
|
|
40
|
+
process.stdout.write(`${r.id.padEnd(w)} ${r.status.padEnd(11)} ${r.updatedAt.padEnd(ts)} ${r.title}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runTask(args) {
|
|
44
|
+
const [sub, ...rest] = args;
|
|
45
|
+
switch (sub) {
|
|
46
|
+
case "add": return cmdTaskAdd(rest);
|
|
47
|
+
case "list": return cmdTaskList(rest);
|
|
48
|
+
case "show": return cmdTaskShow(rest);
|
|
49
|
+
case "update": return cmdTaskUpdate(rest);
|
|
50
|
+
case "claim": return cmdTaskClaim(rest);
|
|
51
|
+
case "heartbeat": return cmdTaskHeartbeat(rest);
|
|
52
|
+
case "complete": return cmdTaskComplete(rest);
|
|
53
|
+
case "cancel": return cmdTaskCancel(rest);
|
|
54
|
+
case "release": return cmdTaskRelease(rest);
|
|
55
|
+
default:
|
|
56
|
+
process.stderr.write("usage: ok task <add|list|show|update|claim|heartbeat|complete|cancel|release>\n");
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function titleFromArgs(positionals) {
|
|
61
|
+
if (positionals.length === 0) {
|
|
62
|
+
throw new Error("ok task add requires a title positional");
|
|
63
|
+
}
|
|
64
|
+
return positionals.join(" ");
|
|
65
|
+
}
|
|
66
|
+
async function cmdTaskAdd(args) {
|
|
67
|
+
const { positionals, flags } = parseArgs(args);
|
|
68
|
+
const title = titleFromArgs(positionals);
|
|
69
|
+
if (!title || title.trim().length === 0) {
|
|
70
|
+
throw new Error("ok task add: title must be a non-empty, non-whitespace string");
|
|
71
|
+
}
|
|
72
|
+
if (title.length > 200)
|
|
73
|
+
throw new Error("title must be <= 200 chars");
|
|
74
|
+
const status = parseStatus(flagString(flags, "status")) ?? "pending";
|
|
75
|
+
const p = await paths();
|
|
76
|
+
const cfg = (await readConfig(p));
|
|
77
|
+
const now = nowIso();
|
|
78
|
+
const task = {
|
|
79
|
+
schema: "ok.task.v1",
|
|
80
|
+
id: newId("tsk"),
|
|
81
|
+
title,
|
|
82
|
+
status,
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
const owner = flagString(flags, "owner");
|
|
87
|
+
if (owner)
|
|
88
|
+
task.owner = owner;
|
|
89
|
+
else if (cfg.defaultOwner)
|
|
90
|
+
task.owner = cfg.defaultOwner;
|
|
91
|
+
const priority = parsePriority(flagString(flags, "priority"));
|
|
92
|
+
if (priority)
|
|
93
|
+
task.priority = priority;
|
|
94
|
+
const plan = flagString(flags, "plan");
|
|
95
|
+
if (plan)
|
|
96
|
+
task.plan = plan;
|
|
97
|
+
const prd = flagString(flags, "prd");
|
|
98
|
+
if (prd)
|
|
99
|
+
task.prd = prd;
|
|
100
|
+
const scopes = flagCsv(flags, "scope");
|
|
101
|
+
if (scopes.length)
|
|
102
|
+
task.scopes = scopes;
|
|
103
|
+
const deps = flagCsv(flags, "deps");
|
|
104
|
+
if (deps.length)
|
|
105
|
+
task.deps = deps;
|
|
106
|
+
const desc = flagString(flags, "description");
|
|
107
|
+
if (desc)
|
|
108
|
+
task.description = desc;
|
|
109
|
+
const acceptance = flagCsv(flags, "acceptance");
|
|
110
|
+
if (acceptance.length)
|
|
111
|
+
task.acceptance = acceptance;
|
|
112
|
+
await writeTask(p, task);
|
|
113
|
+
await refreshIndex(p);
|
|
114
|
+
process.stdout.write(`${task.id}\n`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
async function cmdTaskList(args) {
|
|
118
|
+
const { flags } = parseArgs(args);
|
|
119
|
+
const p = await paths();
|
|
120
|
+
let tasks = await listTasks(p);
|
|
121
|
+
const wantJson = flagBool(flags, "json");
|
|
122
|
+
const filterStatus = parseStatus(flagString(flags, "status"));
|
|
123
|
+
if (filterStatus)
|
|
124
|
+
tasks = tasks.filter((t) => t.status === filterStatus);
|
|
125
|
+
const owner = flagString(flags, "owner");
|
|
126
|
+
if (owner)
|
|
127
|
+
tasks = tasks.filter((t) => t.owner === owner);
|
|
128
|
+
const plan = flagString(flags, "plan");
|
|
129
|
+
if (plan)
|
|
130
|
+
tasks = tasks.filter((t) => t.plan === plan);
|
|
131
|
+
const prd = flagString(flags, "prd");
|
|
132
|
+
if (prd)
|
|
133
|
+
tasks = tasks.filter((t) => t.prd === prd);
|
|
134
|
+
tasks = byUpdatedDesc(tasks);
|
|
135
|
+
if (wantJson) {
|
|
136
|
+
process.stdout.write(JSON.stringify(tasks, null, 2) + "\n");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const rows = tasks.map((t) => ({
|
|
140
|
+
id: t.id, status: t.status, title: t.title, updatedAt: t.updatedAt,
|
|
141
|
+
}));
|
|
142
|
+
printTable(rows);
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
async function cmdTaskShow(args) {
|
|
147
|
+
const { positionals, flags } = parseArgs(args);
|
|
148
|
+
if (positionals.length !== 1) {
|
|
149
|
+
process.stderr.write("usage: ok task show <id> [--json]\n");
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
const p = await paths();
|
|
153
|
+
const task = await readTask(p, positionals[0]);
|
|
154
|
+
if (!task) {
|
|
155
|
+
process.stderr.write(`no such task: ${positionals[0]}\n`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
if (flagBool(flags, "json")) {
|
|
159
|
+
process.stdout.write(JSON.stringify(task, null, 2) + "\n");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
for (const [k, v] of Object.entries(task)) {
|
|
163
|
+
process.stdout.write(`${k}: ${typeof v === "string" ? v : JSON.stringify(v)}\n`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
async function cmdTaskUpdate(args) {
|
|
169
|
+
const { positionals, flags } = parseArgs(args);
|
|
170
|
+
if (positionals.length !== 1) {
|
|
171
|
+
process.stderr.write("usage: ok task update <id> [--status …] [--owner …] [--priority …] [--evidence text] [--acceptance a,b,c]\n");
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
const p = await paths();
|
|
175
|
+
const existing = await readTask(p, positionals[0]);
|
|
176
|
+
if (!existing) {
|
|
177
|
+
process.stderr.write(`no such task: ${positionals[0]}\n`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
let next = touch(existing);
|
|
181
|
+
const status = parseStatus(flagString(flags, "status"));
|
|
182
|
+
if (status) {
|
|
183
|
+
if (status !== existing.status) {
|
|
184
|
+
next.status = status;
|
|
185
|
+
if (status === "in_progress" && !next.startedAt)
|
|
186
|
+
next.startedAt = nowIso();
|
|
187
|
+
if ((status === "done" || status === "cancelled") && !next.completedAt)
|
|
188
|
+
next.completedAt = nowIso();
|
|
189
|
+
if (status !== "done" && status !== "cancelled") {
|
|
190
|
+
next.completedAt = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const owner = flagString(flags, "owner");
|
|
195
|
+
if (owner !== undefined)
|
|
196
|
+
next.owner = owner;
|
|
197
|
+
const priority = parsePriority(flagString(flags, "priority"));
|
|
198
|
+
if (priority)
|
|
199
|
+
next.priority = priority;
|
|
200
|
+
const evidence = flagString(flags, "evidence");
|
|
201
|
+
if (evidence !== undefined) {
|
|
202
|
+
next.evidence = [...(existing.evidence ?? []), evidence];
|
|
203
|
+
}
|
|
204
|
+
const acceptance = flagCsv(flags, "acceptance");
|
|
205
|
+
if (acceptance.length) {
|
|
206
|
+
next.acceptance = [...(existing.acceptance ?? []), ...acceptance];
|
|
207
|
+
}
|
|
208
|
+
const desc = flagString(flags, "description");
|
|
209
|
+
if (desc !== undefined)
|
|
210
|
+
next.description = desc;
|
|
211
|
+
await writeTask(p, next);
|
|
212
|
+
await refreshIndex(p);
|
|
213
|
+
process.stdout.write(`${next.id}\n`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
async function cmdTaskClaim(args) {
|
|
217
|
+
const { positionals, flags } = parseArgs(args);
|
|
218
|
+
if (positionals.length !== 1) {
|
|
219
|
+
process.stderr.write("usage: ok task claim <id> --owner X [--lease-ms N]\n");
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
const owner = flagString(flags, "owner");
|
|
223
|
+
if (!owner) {
|
|
224
|
+
process.stderr.write("--owner is required\n");
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
const leaseMsRaw = flagString(flags, "lease-ms");
|
|
228
|
+
const leaseMs = leaseMsRaw ? Number(leaseMsRaw) : undefined;
|
|
229
|
+
if (leaseMs !== undefined && (!Number.isFinite(leaseMs) || leaseMs <= 0)) {
|
|
230
|
+
process.stderr.write("--lease-ms must be a positive integer\n");
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
const p = await paths();
|
|
234
|
+
try {
|
|
235
|
+
await claim(p, positionals[0], owner, { leaseMs });
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
if (e instanceof LockHeldError) {
|
|
239
|
+
process.stderr.write(`${e.message}\n`);
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
const task = await readTask(p, positionals[0]);
|
|
245
|
+
if (task && task.status === "pending") {
|
|
246
|
+
const next = touch({ ...task, status: "in_progress", startedAt: task.startedAt ?? nowIso(), owner });
|
|
247
|
+
await writeTask(p, next);
|
|
248
|
+
await refreshIndex(p);
|
|
249
|
+
}
|
|
250
|
+
else if (task) {
|
|
251
|
+
const next = touch({ ...task, owner });
|
|
252
|
+
await writeTask(p, next);
|
|
253
|
+
await refreshIndex(p);
|
|
254
|
+
}
|
|
255
|
+
process.stdout.write(`${positionals[0]}\n`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
async function cmdTaskHeartbeat(args) {
|
|
259
|
+
const { positionals, flags } = parseArgs(args);
|
|
260
|
+
if (positionals.length !== 1) {
|
|
261
|
+
process.stderr.write("usage: ok task heartbeat <id> --owner X [--lease-ms N]\n");
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
const owner = flagString(flags, "owner");
|
|
265
|
+
if (!owner) {
|
|
266
|
+
process.stderr.write("--owner is required\n");
|
|
267
|
+
return 2;
|
|
268
|
+
}
|
|
269
|
+
const leaseMsRaw = flagString(flags, "lease-ms");
|
|
270
|
+
const leaseMs = leaseMsRaw ? Number(leaseMsRaw) : undefined;
|
|
271
|
+
const p = await paths();
|
|
272
|
+
await heartbeat(p, positionals[0], owner, { leaseMs });
|
|
273
|
+
process.stdout.write(`${positionals[0]}\n`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
async function cmdTaskComplete(args) {
|
|
277
|
+
const { positionals, flags } = parseArgs(args);
|
|
278
|
+
if (positionals.length !== 1) {
|
|
279
|
+
process.stderr.write("usage: ok task complete <id> --owner X --evidence \"<text>\"\n");
|
|
280
|
+
return 2;
|
|
281
|
+
}
|
|
282
|
+
const owner = flagString(flags, "owner");
|
|
283
|
+
if (!owner) {
|
|
284
|
+
process.stderr.write("--owner is required\n");
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
287
|
+
const evidence = flagString(flags, "evidence");
|
|
288
|
+
if (!evidence) {
|
|
289
|
+
process.stderr.write("--evidence is required (commit shas, file:line, URLs, etc.)\n");
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
const p = await paths();
|
|
293
|
+
await assertUsable(p, positionals[0], owner);
|
|
294
|
+
const task = await readTask(p, positionals[0]);
|
|
295
|
+
if (!task) {
|
|
296
|
+
process.stderr.write(`no such task: ${positionals[0]}\n`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const now = nowIso();
|
|
300
|
+
const next = touch({
|
|
301
|
+
...task,
|
|
302
|
+
status: "done",
|
|
303
|
+
owner,
|
|
304
|
+
completedAt: task.completedAt ?? now,
|
|
305
|
+
evidence: [...(task.evidence ?? []), evidence],
|
|
306
|
+
});
|
|
307
|
+
await writeTask(p, next);
|
|
308
|
+
await release(p, positionals[0], owner);
|
|
309
|
+
await refreshIndex(p);
|
|
310
|
+
process.stdout.write(`${next.id}\n`);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
async function cmdTaskCancel(args) {
|
|
314
|
+
const { positionals, flags } = parseArgs(args);
|
|
315
|
+
if (positionals.length !== 1) {
|
|
316
|
+
process.stderr.write("usage: ok task cancel <id> --owner X --reason \"<text>\"\n");
|
|
317
|
+
return 2;
|
|
318
|
+
}
|
|
319
|
+
const owner = flagString(flags, "owner");
|
|
320
|
+
if (!owner) {
|
|
321
|
+
process.stderr.write("--owner is required\n");
|
|
322
|
+
return 2;
|
|
323
|
+
}
|
|
324
|
+
const reason = flagString(flags, "reason");
|
|
325
|
+
if (!reason) {
|
|
326
|
+
process.stderr.write("--reason is required\n");
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const p = await paths();
|
|
330
|
+
await assertUsable(p, positionals[0], owner);
|
|
331
|
+
const task = await readTask(p, positionals[0]);
|
|
332
|
+
if (!task) {
|
|
333
|
+
process.stderr.write(`no such task: ${positionals[0]}\n`);
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
const next = touch({
|
|
337
|
+
...task,
|
|
338
|
+
status: "cancelled",
|
|
339
|
+
owner,
|
|
340
|
+
completedAt: task.completedAt ?? nowIso(),
|
|
341
|
+
evidence: [...(task.evidence ?? []), `cancelled: ${reason}`],
|
|
342
|
+
});
|
|
343
|
+
await writeTask(p, next);
|
|
344
|
+
await release(p, positionals[0], owner);
|
|
345
|
+
await refreshIndex(p);
|
|
346
|
+
process.stdout.write(`${next.id}\n`);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
async function cmdTaskRelease(args) {
|
|
350
|
+
const { positionals, flags } = parseArgs(args);
|
|
351
|
+
if (positionals.length !== 1) {
|
|
352
|
+
process.stderr.write("usage: ok task release <id> --owner X\n");
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
const owner = flagString(flags, "owner");
|
|
356
|
+
if (!owner) {
|
|
357
|
+
process.stderr.write("--owner is required\n");
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
const p = await paths();
|
|
361
|
+
const removed = await release(p, positionals[0], owner);
|
|
362
|
+
process.stdout.write(removed ? `${positionals[0]}\n` : `no lock for ${positionals[0]}\n`);
|
|
363
|
+
return removed ? 0 : 1;
|
|
364
|
+
}
|
|
365
|
+
async function refreshIndex(p) {
|
|
366
|
+
try {
|
|
367
|
+
await rebuildIndex(p);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
process.stderr.write(`warning: index rebuild failed: ${e.message}\n`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// `node --experimental-strip-types ok/commands/task.ts [args]` invocation
|
|
374
|
+
// (used by bin/ok.ts shell wrapper for per-subprocess isolation).
|
|
375
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
376
|
+
await runMain(async () => runTask(process.argv.slice(2)));
|
|
377
|
+
}
|