@pi-unipi/kanboard 2.20.4 → 3.0.0-alpha.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 +184 -66
- package/index.ts +206 -63
- package/package.json +18 -23
- package/skills/kanboard/SKILL.md +106 -0
- package/src/bin.ts +210 -0
- package/src/commands.ts +1371 -0
- package/src/guard.ts +143 -0
- package/src/runner.ts +817 -0
- package/src/settings.ts +158 -0
- package/src/shapes.ts +220 -0
- package/commands.ts +0 -83
- package/parser/checkbox-parser.ts +0 -165
- package/parser/frontmatter.ts +0 -26
- package/parser/index.ts +0 -94
- package/parser/milestones.ts +0 -110
- package/parser/plans.ts +0 -111
- package/server/index.ts +0 -266
- package/server/routes/milestone.ts +0 -38
- package/server/routes/workflow.ts +0 -41
- package/skills/kanboard-doctor/SKILL.md +0 -71
- package/types.ts +0 -67
- package/ui/components/checklist.ts +0 -58
- package/ui/components/copy-button.ts +0 -23
- package/ui/components/status-badge.ts +0 -21
- package/ui/layouts/base.ts +0 -68
- package/ui/milestone/page.ts +0 -140
- package/ui/static/app.js +0 -73
- package/ui/static/style.css +0 -729
- package/ui/workflow/page.ts +0 -182
package/src/guard.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/kanboard — the write window.
|
|
3
|
+
*
|
|
4
|
+
* Bash calls into `unipi-kanboard` are split into always-allowed reads and
|
|
5
|
+
* writes that are only allowed while a `/unipi:kanboard-do` turn is open or the
|
|
6
|
+
* runner has a task in flight. The window is a `tool_call` gate: it never edits
|
|
7
|
+
* the command, it just returns a block reason or lets the call through.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { tokenizeArgs } from "./commands.js";
|
|
11
|
+
|
|
12
|
+
export const WRITE_BLOCK_REASON =
|
|
13
|
+
"kanboard board writes are only allowed during /unipi:kanboard-do or a runner task";
|
|
14
|
+
export const addCapReason = (limit: number): string => `at most ${limit} new tasks per turn`;
|
|
15
|
+
/** @deprecated tests should read the limit through the guard's getter instead. */
|
|
16
|
+
export const ADD_CAP = 20;
|
|
17
|
+
export const ADD_CAP_REASON = addCapReason(ADD_CAP);
|
|
18
|
+
|
|
19
|
+
/** Subcommands that never write to the board. */
|
|
20
|
+
const READONLY = new Set(["list", "show", "attachments", "next", "chain", "search", "status"]);
|
|
21
|
+
|
|
22
|
+
/** Global flags that take a value; `--json` is the only valueless one. */
|
|
23
|
+
const GLOBAL_VALUE_FLAGS = new Set(["--actor", "--project", "--gate", "--session"]);
|
|
24
|
+
|
|
25
|
+
export interface KanboardInvocation {
|
|
26
|
+
/** First positional after the binary name ("" when absent). */
|
|
27
|
+
sub: string;
|
|
28
|
+
/** Everything after the subcommand. */
|
|
29
|
+
args: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every `unipi-kanboard` invocation inside a shell command line. */
|
|
33
|
+
export function kanboardInvocations(command: string): KanboardInvocation[] {
|
|
34
|
+
const tokens = tokenizeArgs(command);
|
|
35
|
+
const out: KanboardInvocation[] = [];
|
|
36
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
37
|
+
const token = tokens[index]!;
|
|
38
|
+
// The binary may be a bare name or an absolute path (and .exe on Windows).
|
|
39
|
+
if (!/unipi-kanboard(\.exe)?$/.test(token)) continue;
|
|
40
|
+
const rest = tokens.slice(index + 1);
|
|
41
|
+
let cursor = 0;
|
|
42
|
+
while (cursor < rest.length) {
|
|
43
|
+
const arg = rest[cursor]!;
|
|
44
|
+
if (arg === "--json") {
|
|
45
|
+
cursor += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (GLOBAL_VALUE_FLAGS.has(arg)) {
|
|
49
|
+
cursor += 2;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if ([...GLOBAL_VALUE_FLAGS].some((flag) => arg.startsWith(`${flag}=`))) {
|
|
53
|
+
cursor += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
out.push({ sub: rest[cursor] ?? "", args: rest.slice(cursor + 1) });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Read-only means: no writes, and the board does not change. */
|
|
64
|
+
export function isReadonly(invocation: KanboardInvocation): boolean {
|
|
65
|
+
if (READONLY.has(invocation.sub)) return true;
|
|
66
|
+
if (invocation.sub === "queue") {
|
|
67
|
+
// `queue --list` (or bare `queue`, which lists) is read-only; ids write.
|
|
68
|
+
return invocation.args.every((arg) => arg === "--list" || arg === "--json") || invocation.args.length === 0;
|
|
69
|
+
}
|
|
70
|
+
if (invocation.sub === "project") {
|
|
71
|
+
return invocation.args[0] === "list" || invocation.args[0] === "show";
|
|
72
|
+
}
|
|
73
|
+
if (invocation.sub === "settings") {
|
|
74
|
+
return invocation.args[0] !== "set"; // bare/`show` reads; `set` writes
|
|
75
|
+
}
|
|
76
|
+
if (invocation.sub === "validate") {
|
|
77
|
+
return !invocation.args.includes("--fix");
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface WriteGuard {
|
|
83
|
+
/** Open the window for a `/unipi:kanboard-do` turn. */
|
|
84
|
+
open(): void;
|
|
85
|
+
/** Arm the agent_end closer right after the -do prompt was sent. */
|
|
86
|
+
noteSent(): void;
|
|
87
|
+
/**
|
|
88
|
+
* A window opened by -do closes on the first agent_end that isn't the
|
|
89
|
+
* pre-send echo (>150ms after noteSent). Returns true when it just closed.
|
|
90
|
+
*/
|
|
91
|
+
onAgentEnd(): boolean;
|
|
92
|
+
/** null when the command is allowed; otherwise the block reason. */
|
|
93
|
+
check(command: string): string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The window is open during a -do turn (`doOpen`) or while the runner has a
|
|
98
|
+
* task in phase `running`. `runnerTask` returns that task's id (null when the
|
|
99
|
+
* runner is not running one) — the `add` counter resets whenever the running
|
|
100
|
+
* task changes, so autowork/queue drains get a fresh 20 per task.
|
|
101
|
+
*/
|
|
102
|
+
export function createWriteGuard(runnerTask: () => string | null, addLimit: () => number = () => ADD_CAP): WriteGuard {
|
|
103
|
+
let doOpen = false;
|
|
104
|
+
let sentAt = 0;
|
|
105
|
+
let adds = 0;
|
|
106
|
+
let lastTask: string | null = null;
|
|
107
|
+
return {
|
|
108
|
+
open() {
|
|
109
|
+
doOpen = true;
|
|
110
|
+
adds = 0;
|
|
111
|
+
lastTask = null;
|
|
112
|
+
},
|
|
113
|
+
noteSent() {
|
|
114
|
+
sentAt = Date.now();
|
|
115
|
+
},
|
|
116
|
+
onAgentEnd() {
|
|
117
|
+
if (!doOpen) return false;
|
|
118
|
+
if (Date.now() - sentAt < 150) return false; // late end from the previous turn
|
|
119
|
+
doOpen = false;
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
check(command: string) {
|
|
123
|
+
const invocations = kanboardInvocations(command);
|
|
124
|
+
if (invocations.length === 0) return null;
|
|
125
|
+
const task = runnerTask();
|
|
126
|
+
// A new runner task is a new window for the cap (a -do open resets too).
|
|
127
|
+
if (task !== null && task !== lastTask) {
|
|
128
|
+
adds = 0;
|
|
129
|
+
lastTask = task;
|
|
130
|
+
}
|
|
131
|
+
for (const invocation of invocations) {
|
|
132
|
+
if (isReadonly(invocation)) continue;
|
|
133
|
+
if (!(doOpen || task !== null)) return WRITE_BLOCK_REASON;
|
|
134
|
+
if (invocation.sub === "add") {
|
|
135
|
+
adds += 1;
|
|
136
|
+
const limit = addLimit();
|
|
137
|
+
if (limit > 0 && adds > limit) return addCapReason(limit);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|