@pi-unipi/kanboard 3.0.0-alpha.8 → 3.0.0-alpha.9
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/index.ts +1 -0
- package/package.json +7 -7
- package/src/commands.ts +53 -19
- package/src/guard.ts +41 -18
- package/src/runner.ts +86 -11
- package/src/settings.ts +5 -0
package/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
71
|
return status?.phase === "running" ? status.taskId : null;
|
|
72
72
|
},
|
|
73
73
|
() => readKanboardSettings().turnAddLimit,
|
|
74
|
+
() => readKanboardSettings().doCredits,
|
|
74
75
|
);
|
|
75
76
|
const sessionId = (): string => process.env.UNIPI_KANBOARD_SESSION ?? `pi-${process.pid}`;
|
|
76
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/kanboard",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.9",
|
|
4
4
|
"description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@pi-unipi/core": "3.0.0-alpha.
|
|
34
|
+
"@pi-unipi/core": "3.0.0-alpha.9"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"themes": []
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
|
-
"@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.
|
|
52
|
-
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.
|
|
53
|
-
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.
|
|
54
|
-
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.
|
|
55
|
-
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.
|
|
51
|
+
"@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.9",
|
|
52
|
+
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.9",
|
|
53
|
+
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.9",
|
|
54
|
+
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.9",
|
|
55
|
+
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.9"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/commands.ts
CHANGED
|
@@ -40,9 +40,9 @@ export const HELP = `Kanboard commands
|
|
|
40
40
|
/unipi:kanboard-autowork start|stop work ready tasks chain by chain; stop finishes the current one first
|
|
41
41
|
Priority -p: 1 none · 2 low · 3 medium · 4 high · 5 urgent`;
|
|
42
42
|
|
|
43
|
-
export function doText(slug: string, cli: string, request: string, queueMax = 10): string {
|
|
43
|
+
export function doText(slug: string, cli: string, request: string, queueMax = 10, credits = 0): string {
|
|
44
44
|
const limit = queueMax === 0 ? "Todo tasks" : `Todo tasks, at most ${queueMax}`;
|
|
45
|
-
return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`).
|
|
45
|
+
return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`). You have ${credits} write credit${credits === 1 ? "" : "s"} remaining this session — each board write (add, move, edit, note, link, order, queue) costs 1, reads are free; /unipi:kanboard-do tops the credits back up. You can add tasks, move them between backlog and todo, link, order, note, and edit tasks you created. To have tasks worked, queue them in order with \`queue <IDs>\` (${limit}); the runner starts them one by one after this turn, so do not start the work yourself. Read a task with \`show <ID>\` before editing, linking or queueing it — \`list\` only shows titles and a one-line excerpt. To choose how a task is worked, label it with \`edit <ID> --strategy none|goal|ralph|swarm|graph\` and \`--plan yes|no\`; leave it unset to let the runner decide. If the request is unclear, ask me instead of guessing.
|
|
46
46
|
|
|
47
47
|
Request: ${request}`;
|
|
48
48
|
}
|
|
@@ -1093,7 +1093,21 @@ export function kanboardCompletions(prefix: string): CompletionItem[] | null {
|
|
|
1093
1093
|
/** Live task list for `--after` / `-do` completions, cached ~5s per client. */
|
|
1094
1094
|
const taskCache = new WeakMap<KanboardCli, { at: number; tasks: ShowTask[] }>();
|
|
1095
1095
|
|
|
1096
|
-
export
|
|
1096
|
+
export interface TaskCompletionOptions {
|
|
1097
|
+
/** Also match task TITLES (default true); false = id prefixes only. */
|
|
1098
|
+
titleSearch?: boolean;
|
|
1099
|
+
/** Row style: "status" = label id, description "<status> — <title>";
|
|
1100
|
+
* "id-title" = label "<id> <title>", description "<status>". */
|
|
1101
|
+
rowStyle?: "status" | "id-title";
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export async function taskCompletions(
|
|
1105
|
+
deps: CommandDeps,
|
|
1106
|
+
prefix: string,
|
|
1107
|
+
before: string,
|
|
1108
|
+
opts: TaskCompletionOptions = {},
|
|
1109
|
+
): Promise<CompletionItem[] | null> {
|
|
1110
|
+
const { titleSearch = true, rowStyle = "status" } = opts;
|
|
1097
1111
|
const client = deps.cli;
|
|
1098
1112
|
if (!client) return null;
|
|
1099
1113
|
let cache = taskCache.get(client);
|
|
@@ -1109,13 +1123,21 @@ export async function taskCompletions(deps: CommandDeps, prefix: string, before:
|
|
|
1109
1123
|
const needle = prefix.toLowerCase();
|
|
1110
1124
|
const items = cache.tasks
|
|
1111
1125
|
.filter((task) => !["cancelled", "archived"].includes(task.status))
|
|
1112
|
-
.filter((task) => task.id.toLowerCase().startsWith(needle) || task.title.toLowerCase().includes(needle))
|
|
1126
|
+
.filter((task) => task.id.toLowerCase().startsWith(needle) || (titleSearch && task.title.toLowerCase().includes(needle)))
|
|
1113
1127
|
.slice(0, 12)
|
|
1114
|
-
.map((task) =>
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1128
|
+
.map((task) =>
|
|
1129
|
+
rowStyle === "id-title"
|
|
1130
|
+
? {
|
|
1131
|
+
value: fullArgs(before, task.id),
|
|
1132
|
+
label: `${task.id} ${task.title.slice(0, 64)}`,
|
|
1133
|
+
description: task.status,
|
|
1134
|
+
}
|
|
1135
|
+
: {
|
|
1136
|
+
value: fullArgs(before, task.id),
|
|
1137
|
+
label: task.id,
|
|
1138
|
+
description: `${task.status} — ${task.title}`,
|
|
1139
|
+
},
|
|
1140
|
+
);
|
|
1119
1141
|
return items.length > 0 ? items : null;
|
|
1120
1142
|
}
|
|
1121
1143
|
|
|
@@ -1191,15 +1213,19 @@ export async function kanboardAddCompletions(deps: CommandDeps, prefix: string):
|
|
|
1191
1213
|
return null;
|
|
1192
1214
|
}
|
|
1193
1215
|
|
|
1194
|
-
/** /unipi:kanboard-do … — complete a trailing task-id
|
|
1216
|
+
/** /unipi:kanboard-do … — complete a trailing task-id token; plain prose never matches. */
|
|
1195
1217
|
export async function kanboardDoCompletions(deps: CommandDeps, prefix: string): Promise<CompletionItem[] | null> {
|
|
1196
1218
|
const raw = prefix ?? "";
|
|
1197
|
-
const
|
|
1198
|
-
const before =
|
|
1199
|
-
const token =
|
|
1200
|
-
// Only
|
|
1201
|
-
|
|
1202
|
-
|
|
1219
|
+
const at = raw.lastIndexOf(" ");
|
|
1220
|
+
const before = at === -1 ? "" : raw.slice(0, at + 1);
|
|
1221
|
+
const token = raw.slice(at + 1);
|
|
1222
|
+
// Only a task-id-like tail ("UNI-1", "uni", "UNI-") completes; words that
|
|
1223
|
+
// match no id shape or prefix ("note", "set") yield nothing.
|
|
1224
|
+
if (!/^[A-Za-z]{2,10}-?\d*$/.test(token)) return null;
|
|
1225
|
+
// Title search only when the token looks like a real id prefix (has a digit
|
|
1226
|
+
// or ends with the dash); otherwise id prefixes only.
|
|
1227
|
+
const idLike = /\d/.test(token) || token.endsWith("-");
|
|
1228
|
+
return taskCompletions(deps, token, before, { titleSearch: idLike, rowStyle: "id-title" });
|
|
1203
1229
|
}
|
|
1204
1230
|
|
|
1205
1231
|
export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): void {
|
|
@@ -1308,10 +1334,15 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
|
|
|
1308
1334
|
});
|
|
1309
1335
|
|
|
1310
1336
|
pi.registerCommand("unipi:kanboard-do", {
|
|
1311
|
-
description: "
|
|
1337
|
+
description: "Grant kanboard write credits to the agent (/unipi:kanboard-do <request>; off revokes)",
|
|
1312
1338
|
getArgumentCompletions: (prefix: string) => kanboardDoCompletions(deps, prefix ?? ""),
|
|
1313
1339
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
1314
1340
|
const request = (args ?? "").trim();
|
|
1341
|
+
if (request.toLowerCase() === "off") {
|
|
1342
|
+
deps.guard.revoke();
|
|
1343
|
+
ctx.ui.notify("kanboard: write credits revoked — reads stay free", "info");
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1315
1346
|
if (!request) {
|
|
1316
1347
|
ctx.ui.notify("kanboard: -do needs a request — /unipi:kanboard-do <what the agent may do on the board>", "warning");
|
|
1317
1348
|
return;
|
|
@@ -1328,9 +1359,12 @@ export function registerKanboardCommands(pi: ExtensionAPI, deps: CommandDeps): v
|
|
|
1328
1359
|
const busy = typeof (ctx as unknown as { isIdle?: () => boolean }).isIdle === "function"
|
|
1329
1360
|
? !(ctx as unknown as { isIdle: () => boolean }).isIdle!()
|
|
1330
1361
|
: false;
|
|
1331
|
-
pi.sendUserMessage(
|
|
1362
|
+
pi.sendUserMessage(
|
|
1363
|
+
doText(slug, client.binary.path, request, deps.settings().queueMax, deps.guard.remaining()),
|
|
1364
|
+
busy ? { deliverAs: "followUp" } : undefined,
|
|
1365
|
+
);
|
|
1332
1366
|
deps.guard.noteSent();
|
|
1333
|
-
deps.debug(`do
|
|
1367
|
+
deps.debug(`do credits open (${slug}): ${deps.guard.remaining()} remaining`);
|
|
1334
1368
|
},
|
|
1335
1369
|
});
|
|
1336
1370
|
|
package/src/guard.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { tokenizeArgs } from "./commands.js";
|
|
11
11
|
|
|
12
|
-
export const
|
|
13
|
-
|
|
12
|
+
export const DEFAULT_DO_CREDITS = 10;
|
|
13
|
+
export const WRITE_CREDITS_USED_UP = "kanboard write credits used up — run /unipi:kanboard-do to reload";
|
|
14
14
|
export const addCapReason = (limit: number): string => `at most ${limit} new tasks per turn`;
|
|
15
15
|
/** @deprecated tests should read the limit through the guard's getter instead. */
|
|
16
16
|
export const ADD_CAP = 20;
|
|
@@ -80,35 +80,55 @@ export function isReadonly(invocation: KanboardInvocation): boolean {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface WriteGuard {
|
|
83
|
-
/**
|
|
83
|
+
/** Grant / top up the session's write credits to N (kanboard.doCredits). */
|
|
84
84
|
open(): void;
|
|
85
|
+
/** Revoke remaining credits (/unipi:kanboard-do off). */
|
|
86
|
+
revoke(): void;
|
|
87
|
+
/** Credits left this session. */
|
|
88
|
+
remaining(): number;
|
|
85
89
|
/** Arm the agent_end closer right after the -do prompt was sent. */
|
|
86
90
|
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
|
+
/** Credits persist across turns; this only closes the -do window label. */
|
|
91
92
|
onAgentEnd(): boolean;
|
|
92
93
|
/** null when the command is allowed; otherwise the block reason. */
|
|
93
94
|
check(command: string): string | null;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* Writes cost session credits (one per write subcommand in the command line);
|
|
99
|
+
* reads are free and never blocked. A runner task keeps unlimited access
|
|
100
|
+
* (still add-capped). Credits persist across turns and follow-up questions
|
|
101
|
+
* until spent; /unipi:kanboard-do tops up to N without stacking past N.
|
|
101
102
|
*/
|
|
102
|
-
export function createWriteGuard(
|
|
103
|
+
export function createWriteGuard(
|
|
104
|
+
runnerTask: () => string | null,
|
|
105
|
+
addLimit: () => number = () => ADD_CAP,
|
|
106
|
+
doCredits: () => number = () => DEFAULT_DO_CREDITS,
|
|
107
|
+
): WriteGuard {
|
|
108
|
+
let credits = 0;
|
|
103
109
|
let doOpen = false;
|
|
104
110
|
let sentAt = 0;
|
|
105
111
|
let adds = 0;
|
|
106
112
|
let lastTask: string | null = null;
|
|
113
|
+
const countAdd = (invocation: KanboardInvocation): string | null => {
|
|
114
|
+
if (invocation.sub !== "add") return null;
|
|
115
|
+
adds += 1;
|
|
116
|
+
const limit = addLimit();
|
|
117
|
+
return limit > 0 && adds > limit ? addCapReason(limit) : null;
|
|
118
|
+
};
|
|
107
119
|
return {
|
|
108
120
|
open() {
|
|
109
|
-
|
|
121
|
+
credits = Math.max(credits, Math.max(0, doCredits()));
|
|
110
122
|
adds = 0;
|
|
111
123
|
lastTask = null;
|
|
124
|
+
doOpen = true;
|
|
125
|
+
},
|
|
126
|
+
revoke() {
|
|
127
|
+
credits = 0;
|
|
128
|
+
doOpen = false;
|
|
129
|
+
},
|
|
130
|
+
remaining() {
|
|
131
|
+
return credits;
|
|
112
132
|
},
|
|
113
133
|
noteSent() {
|
|
114
134
|
sentAt = Date.now();
|
|
@@ -130,12 +150,15 @@ export function createWriteGuard(runnerTask: () => string | null, addLimit: () =
|
|
|
130
150
|
}
|
|
131
151
|
for (const invocation of invocations) {
|
|
132
152
|
if (isReadonly(invocation)) continue;
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (limit > 0 && adds > limit) return addCapReason(limit);
|
|
153
|
+
if (task !== null) {
|
|
154
|
+
const cap = countAdd(invocation);
|
|
155
|
+
if (cap) return cap;
|
|
156
|
+
continue;
|
|
138
157
|
}
|
|
158
|
+
if (credits <= 0) return WRITE_CREDITS_USED_UP;
|
|
159
|
+
credits -= 1;
|
|
160
|
+
const cap = countAdd(invocation);
|
|
161
|
+
if (cap) return cap;
|
|
139
162
|
}
|
|
140
163
|
return null;
|
|
141
164
|
},
|
package/src/runner.ts
CHANGED
|
@@ -436,6 +436,22 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
436
436
|
return true;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/** Latest [parked goal: …] marker from the task's release comments, if any. */
|
|
440
|
+
async function parkedGoalFromNotes(taskId: string): Promise<string | null> {
|
|
441
|
+
try {
|
|
442
|
+
const shown = asTask(
|
|
443
|
+
"show",
|
|
444
|
+
await runCli<unknown>(["show", taskId], { extraEnv: { UNIPI_KANBOARD_PROJECT: project() } }),
|
|
445
|
+
);
|
|
446
|
+
const matches = (shown.activity ?? [])
|
|
447
|
+
.map((entry) => entry.text.match(/\[parked goal: ([\w-]+)\]/)?.[1])
|
|
448
|
+
.filter((goal): goal is string => typeof goal === "string");
|
|
449
|
+
return matches.at(-1) ?? null;
|
|
450
|
+
} catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
439
455
|
/** Wire the chosen strategy for the work turn; falls back toward "none". */
|
|
440
456
|
async function applyStrategy(ctx: ExtensionContext, task: KanboardTask, strategy: Strategy): Promise<void> {
|
|
441
457
|
const setRun = (mode: RunMode, goal?: string): void => {
|
|
@@ -447,6 +463,22 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
447
463
|
.catch((error) => deps.debug(`set-run failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
448
464
|
};
|
|
449
465
|
const goalStart = async (): Promise<boolean> => {
|
|
466
|
+
// A goal parked for THIS task (interrupted/released earlier) resumes
|
|
467
|
+
// instead of starting fresh; a stale id falls through to goal-start.
|
|
468
|
+
const priorGoal = task.run?.goal ?? (await parkedGoalFromNotes(task.id));
|
|
469
|
+
if (priorGoal) {
|
|
470
|
+
const resumed = await callCommandRunner<{ ok?: boolean; reason?: string }>(
|
|
471
|
+
"unipi:goal-resume",
|
|
472
|
+
ctx,
|
|
473
|
+
{ goalId: priorGoal },
|
|
474
|
+
);
|
|
475
|
+
if (resumed.found && resumed.result?.ok) {
|
|
476
|
+
state.goalId = priorGoal;
|
|
477
|
+
state.mode = "goal";
|
|
478
|
+
setRun("goal", priorGoal);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
450
482
|
const started = await callCommandRunner<{ ok?: boolean; goalId?: string; reason?: string }>(
|
|
451
483
|
"unipi:goal-start",
|
|
452
484
|
ctx,
|
|
@@ -530,14 +562,14 @@ ${body}`.trim() },
|
|
|
530
562
|
}
|
|
531
563
|
|
|
532
564
|
/** Ask the goal engine whether the goal reached a terminal state. */
|
|
533
|
-
async function goalStatus(): Promise<{ status: string; objective?: string } | null> {
|
|
534
|
-
const looked = await callCommandRunner<{ found?: boolean; status?: string; objective?: string }>(
|
|
565
|
+
async function goalStatus(): Promise<{ status: string; reason?: string; objective?: string } | null> {
|
|
566
|
+
const looked = await callCommandRunner<{ found?: boolean; status?: string; reason?: string; objective?: string }>(
|
|
535
567
|
"unipi:goal-status",
|
|
536
568
|
undefined,
|
|
537
569
|
{ goalId: state.goalId },
|
|
538
570
|
);
|
|
539
571
|
if (!looked.found || !looked.result?.found || !looked.result.status) return null;
|
|
540
|
-
return { status: looked.result.status, objective: looked.result.objective };
|
|
572
|
+
return { status: looked.result.status, reason: looked.result.reason, objective: looked.result.objective };
|
|
541
573
|
}
|
|
542
574
|
|
|
543
575
|
function planStillActive(): boolean {
|
|
@@ -553,21 +585,57 @@ ${body}`.trim() },
|
|
|
553
585
|
"show",
|
|
554
586
|
await runCli<unknown>(["show", task.id], { extraEnv: { UNIPI_KANBOARD_PROJECT: project() } }),
|
|
555
587
|
);
|
|
556
|
-
|
|
588
|
+
// The user moved the task out from under the runner (done/cancelled/
|
|
589
|
+
// backlog): stop its goal, leave the board alone.
|
|
590
|
+
const movedAway = ["done", "cancelled", "backlog"].includes(value.status);
|
|
591
|
+
if (movedAway && state.goalId) {
|
|
592
|
+
await callCommandRunner("unipi:goal-stop", ctx, { goalId: state.goalId });
|
|
593
|
+
}
|
|
594
|
+
// A still-drivable goal is parked so re-claiming this task resumes it.
|
|
595
|
+
// The id rides the release comment (activity survives; the run block
|
|
596
|
+
// does not — release clears it).
|
|
597
|
+
let parkedMarker = "";
|
|
598
|
+
if (!movedAway && state.mode === "goal" && state.goalId) {
|
|
599
|
+
const paused = await callCommandRunner<{ ok?: boolean; reason?: string }>(
|
|
600
|
+
"unipi:goal-pause",
|
|
601
|
+
ctx,
|
|
602
|
+
{ goalId: state.goalId },
|
|
603
|
+
);
|
|
604
|
+
if (paused.found && paused.result?.ok) {
|
|
605
|
+
deps.debug(`goal ${state.goalId} parked for ${task.id}`);
|
|
606
|
+
parkedMarker = ` [parked goal: ${state.goalId}]`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (movedAway) {
|
|
610
|
+
// The user's move stands — no release, just reset the runner.
|
|
611
|
+
ctx.ui.notify(`▣ ${task.id} was moved to ${value.status} outside the runner — goal stopped`, "warning");
|
|
612
|
+
} else if (value.status === "blocked") {
|
|
557
613
|
const note = (value.activity ?? []).slice(-1)[0]?.text ?? "blocked";
|
|
558
614
|
ctx.ui.notify(`▣ ${task.id} blocked: ${note}`, "warning");
|
|
559
615
|
} else if (outcome === "in_review") {
|
|
560
|
-
await runCli(["release", task.id, "--to", "in_review", "--comment", comment], {
|
|
616
|
+
await runCli(["release", task.id, "--to", "in_review", "--comment", comment + parkedMarker], {
|
|
561
617
|
extraEnv: { UNIPI_KANBOARD_PROJECT: project() },
|
|
562
618
|
});
|
|
563
619
|
const first = comment.split("\n")[0]?.slice(0, 120) ?? "";
|
|
564
620
|
ctx.ui.notify(`✓ ${task.id} → In Review: ${first}`, "info");
|
|
565
621
|
} else {
|
|
566
|
-
await runCli(["release", task.id, "--to", outcome, "--comment", comment], {
|
|
622
|
+
await runCli(["release", task.id, "--to", outcome, "--comment", comment + parkedMarker], {
|
|
567
623
|
extraEnv: { UNIPI_KANBOARD_PROJECT: project() },
|
|
568
624
|
});
|
|
569
625
|
ctx.ui.notify(`↩ ${task.id} → ${outcome === "todo" ? "Todo" : "Blocked"}: ${comment.slice(0, 120)}`, "warning");
|
|
570
626
|
}
|
|
627
|
+
if (movedAway) {
|
|
628
|
+
state.consumedText = state.lastText.trim();
|
|
629
|
+
state.task = null;
|
|
630
|
+
state.goalId = null;
|
|
631
|
+
state.endsSinceSend = 0;
|
|
632
|
+
state.phase = "idle";
|
|
633
|
+
persist("idle");
|
|
634
|
+
setStatus(ctx, state.stopAfterCurrent ? undefined : describe());
|
|
635
|
+
await postBoardProgress();
|
|
636
|
+
await continueLoop(ctx);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
571
639
|
} catch (error) {
|
|
572
640
|
ctx.ui.notify(
|
|
573
641
|
`kanboard: could not release ${task.id} — ${error instanceof KanboardCliError ? error.message : String(error)}`,
|
|
@@ -878,13 +946,20 @@ ${body}`.trim() },
|
|
|
878
946
|
}
|
|
879
947
|
if (state.mode === "goal" && state.goalId) {
|
|
880
948
|
const goal = await goalStatus();
|
|
881
|
-
if (goal
|
|
882
|
-
|
|
883
|
-
|
|
949
|
+
if (goal) {
|
|
950
|
+
// A user-stopped goal is NOT success: the task goes back to Todo.
|
|
951
|
+
if (goal.status === "complete" && goal.reason === "complete(user_requested)") {
|
|
952
|
+
await finishTask(ctx, "todo", "goal stopped by user");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (goal.status !== "complete") {
|
|
956
|
+
if (["failed", "blocked", "stalled", "abandoned", "budget_exhausted"].includes(goal.status)) {
|
|
957
|
+
await finishTask(ctx, "todo", `goal ${goal.status}`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
deps.debug(`settle deferred: goal ${goal.status}`);
|
|
884
961
|
return;
|
|
885
962
|
}
|
|
886
|
-
deps.debug(`settle deferred: goal ${goal.status}`);
|
|
887
|
-
return;
|
|
888
963
|
}
|
|
889
964
|
}
|
|
890
965
|
|
package/src/settings.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface KanboardSettings {
|
|
|
31
31
|
maxSessions: number;
|
|
32
32
|
/** `add` calls allowed per -do/runner window (0 = unlimited). */
|
|
33
33
|
turnAddLimit: number;
|
|
34
|
+
/** Write credits a /unipi:kanboard-do grants (each board write costs 1). */
|
|
35
|
+
doCredits: number;
|
|
34
36
|
/** Strategy for unlabelled tasks ("auto" = jev decides). */
|
|
35
37
|
defaultStrategy: "auto" | "none" | "goal" | "ralph" | "swarm" | "graph";
|
|
36
38
|
/** Plan-first for unlabelled tasks (jev is never asked about plan). */
|
|
@@ -53,6 +55,7 @@ export const DEFAULT_SETTINGS: KanboardSettings = {
|
|
|
53
55
|
queueMax: 10,
|
|
54
56
|
maxSessions: 2,
|
|
55
57
|
turnAddLimit: 20,
|
|
58
|
+
doCredits: 10,
|
|
56
59
|
defaultStrategy: "auto",
|
|
57
60
|
defaultPlan: false,
|
|
58
61
|
blocking: "avoid",
|
|
@@ -113,6 +116,7 @@ export function registerKanboardSettings(): void {
|
|
|
113
116
|
{ key: "queueMax", type: "number", label: "-do queue limit", min: 0, zeroLabel: "unlimited", description: "Tasks a session may queue for the runner" },
|
|
114
117
|
{ key: "maxSessions", type: "number", label: "Sessions working at once (per project)", min: 1, description: "Distinct sessions holding in_progress tasks" },
|
|
115
118
|
{ key: "turnAddLimit", type: "number", label: "New tasks per turn", min: 0, zeroLabel: "unlimited", description: "`add` calls allowed per -do turn or runner task" },
|
|
119
|
+
{ key: "doCredits", type: "number", label: "-do write credits", min: 0, zeroLabel: "off", description: "Write credits a /unipi:kanboard-do grants; each board write costs 1, reads are free" },
|
|
116
120
|
],
|
|
117
121
|
},
|
|
118
122
|
{
|
|
@@ -183,6 +187,7 @@ export function readKanboardSettings(cwd: string = process.cwd()): KanboardSetti
|
|
|
183
187
|
queueMax: typeof raw.queueMax === "number" && raw.queueMax >= 0 ? raw.queueMax : DEFAULT_SETTINGS.queueMax,
|
|
184
188
|
maxSessions: typeof raw.maxSessions === "number" && raw.maxSessions >= 1 ? raw.maxSessions : DEFAULT_SETTINGS.maxSessions,
|
|
185
189
|
turnAddLimit: typeof raw.turnAddLimit === "number" && raw.turnAddLimit >= 0 ? raw.turnAddLimit : DEFAULT_SETTINGS.turnAddLimit,
|
|
190
|
+
doCredits: typeof raw.doCredits === "number" && raw.doCredits >= 0 ? raw.doCredits : DEFAULT_SETTINGS.doCredits,
|
|
186
191
|
defaultStrategy:
|
|
187
192
|
typeof raw.defaultStrategy === "string" &&
|
|
188
193
|
["auto", "none", "goal", "ralph", "swarm", "graph"].includes(raw.defaultStrategy)
|