@pi-unipi/kanboard 3.0.0-alpha.2 → 3.0.0-alpha.3
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 +16 -8
- package/index.ts +4 -0
- package/package.json +7 -7
- package/skills/kanboard/SKILL.md +10 -4
- package/src/bin.ts +4 -2
- package/src/commands.ts +6 -1
- package/src/progress.ts +27 -0
- package/src/runner.ts +197 -62
- package/src/settings.ts +59 -0
package/README.md
CHANGED
|
@@ -39,20 +39,28 @@ not allowed to write:
|
|
|
39
39
|
|
|
40
40
|
1. `claim-next --session <sid> --pid <ppid> --host <host> --gate <chainGate>` —
|
|
41
41
|
nothing ready → `Nothing ready (N waiting on deps, M blocked)`.
|
|
42
|
-
2. **
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
2. **The task's labels decide how it runs** (`edit <ID> --strategy
|
|
43
|
+
none|goal|ralph|swarm|graph` and `--plan yes|no`; `auto`/unset = jev picks).
|
|
44
|
+
jev gets one `choice` call with only the unset questions (title + body
|
|
45
|
+
≤2000 chars): strategy criteria none/goal/ralph/swarm/graph + plan yes/no.
|
|
46
|
+
jev null → `none` + no plan; the decision is logged to
|
|
47
|
+
`~/.unipi/logs/kanboard.log` with `UNIPI_DEBUG_KANBOARD=1`.
|
|
46
48
|
3. `set-run --mode`, then the task goes to the agent as a user message: title,
|
|
47
49
|
body, last 10 activity entries, each dependency with its status and last note,
|
|
48
50
|
and the rules (block with a comment to ask a question; never write
|
|
49
51
|
`in_review`/`done`/`cancelled`; work only on this task).
|
|
50
|
-
- **plan** → plan mode is entered through workflow's `unipi:plan-enter`
|
|
51
|
-
first; approval stays interactive; a discarded plan releases the
|
|
52
|
-
with `plan discarded`.
|
|
52
|
+
- **plan=yes** → plan mode is entered through workflow's `unipi:plan-enter`
|
|
53
|
+
runner first; approval stays interactive; a discarded plan releases the
|
|
54
|
+
task to Todo with `plan discarded`. The chosen strategy applies to the
|
|
55
|
+
work turn after approval (deferred until `planModeChanged`).
|
|
53
56
|
- **goal** → long-horizon's `unipi:goal-start` runner starts a goal with the
|
|
54
57
|
task as the objective; the goal id is recorded with `set-run --goal` and
|
|
55
58
|
completion is read back with `unipi:goal-status`.
|
|
59
|
+
- **swarm / graph** → long-horizon's `unipi:lh-explicit` runner sets the
|
|
60
|
+
explicit mode for the work turn.
|
|
61
|
+
- **ralph** → `unipi:ralph-start` starts the checklist loop (task title +
|
|
62
|
+
body as the checklist file); with no `- [ ]` items or a failed start it
|
|
63
|
+
falls back to goal, then none.
|
|
56
64
|
4. Run end (a `/plan` settle after the last `agent_end`, once the agent reports
|
|
57
65
|
idle with no queued messages): the task is re-read — if the agent blocked it,
|
|
58
66
|
that is respected and reported (`▣ UNI-12 blocked: <comment>`) and the loop
|
|
@@ -65,7 +73,7 @@ not allowed to write:
|
|
|
65
73
|
stale are dropped with a notice), falls back to `claim-next` while autowork
|
|
66
74
|
is on, and stops when nothing is ready. The event loop is never blocked.
|
|
67
75
|
|
|
68
|
-
Footer: `▣ UNI-12 ·
|
|
76
|
+
Footer: `▣ UNI-12 · <strategy>[ +plan]` while a task runs. The claimed task is persisted with
|
|
69
77
|
`pi.appendEntry("unipi:kanboard-runner", …)`, so `/reload` or a resume offers to
|
|
70
78
|
resume it or releases it to Todo.
|
|
71
79
|
|
package/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
initUnipiDirs,
|
|
23
23
|
registerCommandRunner,
|
|
24
24
|
getSettings,
|
|
25
|
+
appendProgress,
|
|
26
|
+
registerProgressRenderer,
|
|
25
27
|
} from "@pi-unipi/core";
|
|
26
28
|
|
|
27
29
|
import { openCli, type KanboardCli } from "./src/bin.js";
|
|
@@ -101,6 +103,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
103
|
guard,
|
|
102
104
|
session: sessionId,
|
|
103
105
|
debug,
|
|
106
|
+
progress: (data) => appendProgress(pi, data),
|
|
104
107
|
}) as CommandDeps;
|
|
105
108
|
|
|
106
109
|
const revealSkill = (ctx: ExtensionContext | { cwd?: string }): void => {
|
|
@@ -133,6 +136,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
133
136
|
return true;
|
|
134
137
|
};
|
|
135
138
|
|
|
139
|
+
registerProgressRenderer(pi);
|
|
136
140
|
registerKanboardCommands(pi, buildDeps());
|
|
137
141
|
|
|
138
142
|
registerCommandRunner(ACTION_OPEN, async (ctx) => {
|
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.3",
|
|
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.3"
|
|
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.3",
|
|
52
|
+
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.3",
|
|
53
|
+
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.3",
|
|
54
|
+
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.3",
|
|
55
|
+
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.3"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/skills/kanboard/SKILL.md
CHANGED
|
@@ -26,6 +26,8 @@ be on `PATH`):
|
|
|
26
26
|
<binary> --actor agent --project <slug> attach <ID> <file> --note "<what it shows>"
|
|
27
27
|
<binary> --actor agent --project <slug> attachments <ID>
|
|
28
28
|
<binary> --actor agent --project <slug> edit <ID> --title|--body|--labels … # only tasks you created, while in backlog/todo
|
|
29
|
+
<binary> --actor agent --project <slug> edit <ID> --strategy none|goal|ralph|swarm|graph|auto # how the runner works it (auto/unset = jev decides)
|
|
30
|
+
<binary> --actor agent --project <slug> edit <ID> --plan yes|no|auto # plan+approve first (auto/unset = jev decides)
|
|
29
31
|
<binary> --actor agent --project <slug> move <ID> blocked --comment "<what you need>"
|
|
30
32
|
<binary> --actor agent --project <slug> link <ID> --after <DEP>
|
|
31
33
|
<binary> --actor agent --project <slug> unlink <ID> --after <DEP>
|
|
@@ -35,7 +37,10 @@ be on `PATH`):
|
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
`--json` gives machine-readable output for every subcommand (task objects carry
|
|
38
|
-
`ready`, `waitingFor`, `depsStatus` and `
|
|
40
|
+
`ready`, `waitingFor`, `depsStatus`, `staleness`, `strategy` and `plan`).
|
|
41
|
+
|
|
42
|
+
`list`/`search` show a one-line body excerpt; read a task's full description with
|
|
43
|
+
`show <ID>` before editing, linking or queueing it.
|
|
39
44
|
|
|
40
45
|
`settings show` reads the pi runtime and effective limits; `settings set`
|
|
41
46
|
and `rotate-token` are user-only — the agent is refused.
|
|
@@ -69,9 +74,10 @@ lists them — read the files there directly if you need one.
|
|
|
69
74
|
your turn ends; claiming them yourself loses the summary and the review step.
|
|
70
75
|
3. **Never cancel.** If a task should be dropped, block it with
|
|
71
76
|
`move <ID> blocked --comment "suggest cancel: <why>"` and let the user decide.
|
|
72
|
-
4. **
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
4. **Follow the blocking rule in your task prompt**: by default work
|
|
78
|
+
autonomously and record assumptions with `note <ID> "assumed: <what/why>"`;
|
|
79
|
+
block (with a comment saying exactly what you need) only when you truly
|
|
80
|
+
cannot continue — `move <ID> blocked --comment "<what you need>"`.
|
|
75
81
|
5. **Work only on the task you were given.** Follow-up work goes to the board as
|
|
76
82
|
a new task in Backlog (`add "<title>"`), optionally `link <new> --after <ID>`.
|
|
77
83
|
A session holds one claim at a time, and at most `maxSessions` (default 2)
|
package/src/bin.ts
CHANGED
|
@@ -80,10 +80,12 @@ export function resolveBinary(env: NodeJS.ProcessEnv = process.env, from?: strin
|
|
|
80
80
|
if (explicit) {
|
|
81
81
|
return isRunnable(explicit) ? { path: explicit, source: "env" } : null;
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// In a source checkout the dev build wins — the platform package can ship a
|
|
84
|
+
// stale prebuilt binary (seen on coffee: alpha.0 shadowed the debug build).
|
|
85
85
|
const dev = devBuildPath();
|
|
86
86
|
if (dev) return { path: dev, source: "dev-build" };
|
|
87
|
+
const packaged = platformPackagePath(from);
|
|
88
|
+
if (packaged && isRunnable(packaged)) return { path: packaged, source: "platform-package" };
|
|
87
89
|
return null;
|
|
88
90
|
}
|
|
89
91
|
|
package/src/commands.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `/unipi:kanboard-add`, `/unipi:kanboard-do`, `/unipi:kanboard-autowork`.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { boardProgressData } from "./progress.js";
|
|
6
7
|
import { execFile, spawn } from "node:child_process";
|
|
7
8
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { homedir, hostname as osHostname, networkInterfaces, tmpdir, type NetworkInterfaceInfo } from "node:os";
|
|
@@ -41,7 +42,7 @@ Priority -p: 1 none · 2 low · 3 medium · 4 high · 5 urgent`;
|
|
|
41
42
|
|
|
42
43
|
export function doText(slug: string, cli: string, request: string, queueMax = 10): string {
|
|
43
44
|
const limit = queueMax === 0 ? "Todo tasks" : `Todo tasks, at most ${queueMax}`;
|
|
44
|
-
return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`). Board writes are allowed until this turn ends. 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. If the request is unclear, ask me instead of guessing.
|
|
45
|
+
return `[kanboard] For this request you may use the kanboard skill on project ${slug} (CLI: \`${cli} --actor agent --project ${slug} …\`). Board writes are allowed until this turn ends. 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.
|
|
45
46
|
|
|
46
47
|
Request: ${request}`;
|
|
47
48
|
}
|
|
@@ -196,6 +197,8 @@ export interface CommandDeps {
|
|
|
196
197
|
/** This session's id (UNIPI_KANBOARD_SESSION / pi-<pid>). */
|
|
197
198
|
session: () => string;
|
|
198
199
|
debug: (line: string) => void;
|
|
200
|
+
/** Post a user-only progress bar (optional: absent in tests). */
|
|
201
|
+
progress?: (data: import("@pi-unipi/core").ProgressData) => void;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
export interface DaemonInfo {
|
|
@@ -628,6 +631,8 @@ export async function runStatus(deps: CommandDeps, ctx: ExtensionCommandContext
|
|
|
628
631
|
if (problems && problems.length > 0) {
|
|
629
632
|
lines.push(`⚠ ${problems.length} task file(s) need repair — unipi-kanboard validate --fix`);
|
|
630
633
|
}
|
|
634
|
+
const bar = boardProgressData(tasks, slug);
|
|
635
|
+
if (bar) deps.progress?.(bar);
|
|
631
636
|
// Active claims and this session's queue.
|
|
632
637
|
const running = tasks.filter((task) => task.status === "in_progress");
|
|
633
638
|
if (running.length > 0) {
|
package/src/progress.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board progress bar (user-only entry):
|
|
3
|
+
* ▣ Board · unipi ██████████▒▒░░░░░░░░ 5/10 tasks 1 blocked
|
|
4
|
+
* Solid = in review + done, shade = in progress, light = todo + blocked.
|
|
5
|
+
* Backlog, cancelled and archived tasks are outside the bar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProgressData } from "@pi-unipi/core";
|
|
9
|
+
|
|
10
|
+
const COUNTED = new Set(["todo", "in_progress", "in_review", "done", "blocked"]);
|
|
11
|
+
|
|
12
|
+
export function boardProgressData(tasks: ReadonlyArray<{ status: string }>, slug: string): ProgressData | undefined {
|
|
13
|
+
const counted = tasks.filter((t) => COUNTED.has(t.status));
|
|
14
|
+
if (counted.length === 0) return undefined;
|
|
15
|
+
const n = (s: string) => counted.filter((t) => t.status === s).length;
|
|
16
|
+
const blocked = n("blocked");
|
|
17
|
+
return {
|
|
18
|
+
icon: "▣",
|
|
19
|
+
label: slug ? `Board · ${slug}` : "Board",
|
|
20
|
+
done: n("in_review") + n("done"),
|
|
21
|
+
active: n("in_progress"),
|
|
22
|
+
total: counted.length,
|
|
23
|
+
unit: "tasks",
|
|
24
|
+
detail: blocked > 0 ? `${String(blocked)} blocked` : undefined,
|
|
25
|
+
color: "success",
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* (spec principle 2).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { appendProgress } from "@pi-unipi/core";
|
|
11
|
+
import { boardProgressData } from "./progress.js";
|
|
10
12
|
import { hostname as osHostname } from "node:os";
|
|
11
13
|
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
12
14
|
import { callCommandRunner, readJudgeJevSettings, askJev, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
@@ -26,7 +28,10 @@ import { applyLimitEnv, type KanboardSettings } from "./settings.js";
|
|
|
26
28
|
|
|
27
29
|
export const RUNNER_ENTRY = "unipi:kanboard-runner";
|
|
28
30
|
|
|
29
|
-
export type RunMode = "
|
|
31
|
+
export type RunMode = "none" | "plan" | "goal" | "ralph" | "swarm" | "graph";
|
|
32
|
+
|
|
33
|
+
/** Work strategy = the long-horizon modes; `direct` maps to `none`. */
|
|
34
|
+
export type Strategy = "none" | "goal" | "ralph" | "swarm" | "graph";
|
|
30
35
|
|
|
31
36
|
export type { KanboardTask, KanboardActivity, KanboardRun };
|
|
32
37
|
|
|
@@ -35,6 +40,10 @@ interface RunnerState {
|
|
|
35
40
|
task: KanboardTask | null;
|
|
36
41
|
mode: RunMode;
|
|
37
42
|
goalId: string | null;
|
|
43
|
+
/** Plan-first flag for the current task (requested by label or jev). */
|
|
44
|
+
planning: boolean;
|
|
45
|
+
/** Strategy held back until plan approval (set on planModeChanged). */
|
|
46
|
+
pendingStrategy: Strategy | null;
|
|
38
47
|
endsSinceSend: number;
|
|
39
48
|
stopAfterCurrent: boolean;
|
|
40
49
|
/** Autowork: keep claiming ready tasks after each finish (queue first). */
|
|
@@ -47,6 +56,8 @@ interface RunnerState {
|
|
|
47
56
|
consumedText: string;
|
|
48
57
|
/** When the current task's prompt was sent (ms). */
|
|
49
58
|
sentAt: number;
|
|
59
|
+
/** Last ctx seen — needed to apply a deferred strategy on plan approval. */
|
|
60
|
+
lastCtx: ExtensionContext | undefined;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
export interface RunnerDeps {
|
|
@@ -74,23 +85,34 @@ export interface Runner {
|
|
|
74
85
|
|
|
75
86
|
/**
|
|
76
87
|
* The askJev question the runner asks for mode choice, as a standalone export
|
|
77
|
-
* so probes and tests exercise exactly what
|
|
88
|
+
* so probes and tests exercise exactly what chooseStrategy sends.
|
|
78
89
|
*/
|
|
79
|
-
|
|
90
|
+
const STRATEGY_CRITERIA: Record<Strategy, string> = {
|
|
91
|
+
none: "A well-scoped change one pass can finish and check.",
|
|
92
|
+
goal: "One objective that needs iterating until it is verifiably done (tests, checks).",
|
|
93
|
+
ralph: "An enumerable checklist of similar chores worked item by item.",
|
|
94
|
+
swarm: "Several independent parts that can be worked in parallel, then combined.",
|
|
95
|
+
graph: "Dependent steps where later work needs earlier results.",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type StrategyQuestion = { type: "choice"; instructions: string; criteria: Record<string, string> };
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The askJev questions the runner asks per claimed task — only for the fields
|
|
102
|
+
* the task left unset (`strategy`/`plan` labels win; `auto` = jev decides).
|
|
103
|
+
* Exported so probes and tests exercise exactly what chooseStrategy sends.
|
|
104
|
+
*/
|
|
105
|
+
export function strategyQuestion(task: KanboardTask): {
|
|
80
106
|
state: string;
|
|
81
|
-
questions: {
|
|
107
|
+
questions: { strategy: StrategyQuestion };
|
|
82
108
|
} {
|
|
83
109
|
return {
|
|
84
110
|
state: `${task.title}\n\n${(task.body ?? "").slice(0, 2000)}`,
|
|
85
111
|
questions: {
|
|
86
|
-
|
|
112
|
+
strategy: {
|
|
87
113
|
type: "choice",
|
|
88
|
-
instructions: "
|
|
89
|
-
criteria: {
|
|
90
|
-
direct: "A small, clear change — just do it",
|
|
91
|
-
plan: "Multi-file or design choices; needs a plan approved first",
|
|
92
|
-
goal: "A large multi-step objective that needs many turns and verification",
|
|
93
|
-
},
|
|
114
|
+
instructions: "Which work strategy should run this task?",
|
|
115
|
+
criteria: { ...STRATEGY_CRITERIA },
|
|
94
116
|
},
|
|
95
117
|
},
|
|
96
118
|
};
|
|
@@ -102,8 +124,10 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
102
124
|
const state: RunnerState = {
|
|
103
125
|
phase: "idle",
|
|
104
126
|
task: null,
|
|
105
|
-
mode: "
|
|
127
|
+
mode: "none",
|
|
106
128
|
goalId: null,
|
|
129
|
+
planning: false,
|
|
130
|
+
pendingStrategy: null,
|
|
107
131
|
endsSinceSend: 0,
|
|
108
132
|
stopAfterCurrent: false,
|
|
109
133
|
autowork: false,
|
|
@@ -112,6 +136,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
112
136
|
lastText: "",
|
|
113
137
|
consumedText: "",
|
|
114
138
|
sentAt: 0,
|
|
139
|
+
lastCtx: undefined,
|
|
115
140
|
};
|
|
116
141
|
|
|
117
142
|
const setStatus = (ctx: ExtensionContext, text?: string): void => {
|
|
@@ -122,7 +147,8 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
122
147
|
}
|
|
123
148
|
};
|
|
124
149
|
|
|
125
|
-
const describe = (): string =>
|
|
150
|
+
const describe = (): string =>
|
|
151
|
+
state.task ? `▣ ${state.task.id} · ${state.mode}${state.planning ? " +plan" : ""}` : "▣ idle";
|
|
126
152
|
|
|
127
153
|
const persist = (phase: string): void => {
|
|
128
154
|
pi.appendEntry(RUNNER_ENTRY, {
|
|
@@ -220,15 +246,27 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
220
246
|
}
|
|
221
247
|
}
|
|
222
248
|
|
|
223
|
-
// ──
|
|
224
|
-
|
|
225
|
-
async function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
249
|
+
// ── strategy choice (labels win; jev decides the unset) ───────────────────
|
|
250
|
+
|
|
251
|
+
async function chooseStrategy(task: KanboardTask): Promise<{ strategy: Strategy; plan: boolean }> {
|
|
252
|
+
// Labels win; the board defaults fill the gaps; jev only picks a strategy
|
|
253
|
+
// when both leave it on auto. Plan is never a jev question.
|
|
254
|
+
const board = deps.settings();
|
|
255
|
+
const labelled = typeof task.strategy === "string" && (task.strategy as string) !== "auto" ? (task.strategy as Strategy) : undefined;
|
|
256
|
+
const plan: boolean = typeof task.plan === "boolean" ? task.plan : board.defaultPlan === true;
|
|
257
|
+
let strategy: Strategy | undefined = labelled;
|
|
258
|
+
if (strategy === undefined && board.defaultStrategy !== "auto") {
|
|
259
|
+
strategy = board.defaultStrategy as Strategy;
|
|
260
|
+
}
|
|
261
|
+
let jevAnswer: string | null = null;
|
|
262
|
+
if (strategy === undefined) {
|
|
263
|
+
const settings = readJudgeJevSettings(cwd);
|
|
264
|
+
const answers = await askJev({ ...strategyQuestion(task), settings, env: process.env });
|
|
265
|
+
jevAnswer = answers?.strategy?.choice ?? null;
|
|
266
|
+
strategy = jevAnswer !== null && jevAnswer in STRATEGY_CRITERIA ? (jevAnswer as Strategy) : "none";
|
|
267
|
+
}
|
|
268
|
+
deps.debug(`strategy ${task.id}: label=${labelled ?? "-"} board=${board.defaultStrategy}/${board.defaultPlan ? "plan" : "no-plan"} jev answered ${JSON.stringify(jevAnswer)} → ${strategy}${plan ? "+plan" : ""}`);
|
|
269
|
+
return { strategy, plan };
|
|
232
270
|
}
|
|
233
271
|
|
|
234
272
|
// ── prompting ─────────────────────────────────────────────────────────────
|
|
@@ -240,7 +278,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
240
278
|
.map((entry) => `- ${entry.at} [${entry.actor}] ${entry.text}`)
|
|
241
279
|
.join("\n");
|
|
242
280
|
const byId = new Map(all.map((candidate) => [candidate.id, candidate]));
|
|
243
|
-
const
|
|
281
|
+
const depLines = (task.deps ?? [])
|
|
244
282
|
.map((id) => {
|
|
245
283
|
const dep = byId.get(id);
|
|
246
284
|
if (!dep) return `- ${id}: (missing)`;
|
|
@@ -258,16 +296,25 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
258
296
|
activity || "(none)",
|
|
259
297
|
"",
|
|
260
298
|
"## Dependencies",
|
|
261
|
-
|
|
299
|
+
depLines || "(none)",
|
|
262
300
|
"",
|
|
263
301
|
...attachmentSection(task),
|
|
264
302
|
"## Rules",
|
|
265
303
|
`Work only on this task (${task.id}).`,
|
|
266
304
|
`Use the board CLI through its absolute path and always pass the actor and project:`,
|
|
267
305
|
` ${binary} --actor agent --project ${project()} <command>`,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
306
|
+
...(deps.settings().blocking === "ask"
|
|
307
|
+
? [
|
|
308
|
+
`If you need information or a decision from the user, run:`,
|
|
309
|
+
` ${binary} --actor agent --project ${project()} move ${task.id} blocked --comment "<what you need>"`,
|
|
310
|
+
"and stop — the runner will hand the task back when the user answers.",
|
|
311
|
+
]
|
|
312
|
+
: [
|
|
313
|
+
"Work autonomously. If something is unclear, make the most reasonable assumption, record it with",
|
|
314
|
+
` \`note ${task.id} "assumed: <what and why>"\`, and continue. Block only when you truly cannot continue —`,
|
|
315
|
+
"missing credentials or access, a destructive or irreversible decision, or a contradiction in the",
|
|
316
|
+
` task — with \`${binary} --actor agent --project ${project()} move ${task.id} blocked --comment "<what you need>"\`, then stop.`,
|
|
317
|
+
]),
|
|
271
318
|
`Do not move the task to in_review or done and do not cancel it: the runner writes those when your turn ends.`,
|
|
272
319
|
`You may add notes with \`note ${task.id} "<text>"\`, link follow-up work with \`add\`, and reorder with \`order\`.`,
|
|
273
320
|
`To show evidence (a screenshot, a log, a report), attach it: \`attach ${task.id} <file> --note "<what it shows>"\`.`,
|
|
@@ -277,6 +324,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
277
324
|
// ── the loop ──────────────────────────────────────────────────────────────
|
|
278
325
|
|
|
279
326
|
async function startTask(ctx: ExtensionContext): Promise<boolean> {
|
|
327
|
+
state.lastCtx = ctx;
|
|
280
328
|
let claimed: KanboardTask | null;
|
|
281
329
|
let claim: KanboardClaim;
|
|
282
330
|
try {
|
|
@@ -298,6 +346,8 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
298
346
|
state.task = claimed;
|
|
299
347
|
state.endsSinceSend = 0;
|
|
300
348
|
state.planOutcome = null;
|
|
349
|
+
state.planning = false;
|
|
350
|
+
state.pendingStrategy = null;
|
|
301
351
|
state.lastText = "";
|
|
302
352
|
try {
|
|
303
353
|
return await runClaimedTask(ctx, claimed);
|
|
@@ -338,13 +388,16 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
338
388
|
state.goalId = null;
|
|
339
389
|
state.phase = "running";
|
|
340
390
|
|
|
341
|
-
let
|
|
391
|
+
let strategy: Strategy = "none";
|
|
392
|
+
let plan = false;
|
|
342
393
|
try {
|
|
343
|
-
|
|
394
|
+
({ strategy, plan } = await chooseStrategy(claimed));
|
|
344
395
|
} catch (error) {
|
|
345
|
-
deps.debug(`
|
|
396
|
+
deps.debug(`strategy choice failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
346
397
|
}
|
|
347
|
-
|
|
398
|
+
// mode = the strategy that actually runs the work turn (plan rides alongside).
|
|
399
|
+
state.mode = strategy;
|
|
400
|
+
state.planning = plan;
|
|
348
401
|
|
|
349
402
|
const all = await cli
|
|
350
403
|
.run<unknown>(["list"], { extraEnv: { UNIPI_KANBOARD_PROJECT: project() } })
|
|
@@ -354,52 +407,126 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
354
407
|
return [] as KanboardTask[];
|
|
355
408
|
});
|
|
356
409
|
|
|
357
|
-
|
|
358
|
-
await cli
|
|
359
|
-
.run(["set-run", claimed.id, "--mode", mode], { extraEnv: { UNIPI_KANBOARD_PROJECT: project() } })
|
|
360
|
-
.catch((error) => deps.debug(`set-run failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
361
|
-
|
|
362
|
-
if (mode === "plan") {
|
|
410
|
+
if (plan) {
|
|
363
411
|
const started = await callCommandRunner<{ ok?: boolean; reason?: string }>("unipi:plan-enter", ctx);
|
|
364
412
|
if (!started.found || !started.result?.ok) {
|
|
365
413
|
ctx.ui.notify(
|
|
366
|
-
`kanboard: plan mode unavailable (${started.result?.reason ?? "workflow not loaded"}) — running
|
|
414
|
+
`kanboard: plan mode unavailable (${started.result?.reason ?? "workflow not loaded"}) — running without a plan`,
|
|
367
415
|
"warning",
|
|
368
416
|
);
|
|
369
|
-
state.
|
|
417
|
+
state.planning = false;
|
|
370
418
|
}
|
|
371
419
|
}
|
|
372
|
-
if (state.
|
|
420
|
+
if (state.planning) {
|
|
421
|
+
// The strategy applies to the WORK turn after approval — apply it on
|
|
422
|
+
// planModeChanged, not now (setExplicit would be consumed by the plan turn).
|
|
423
|
+
state.pendingStrategy = strategy;
|
|
424
|
+
} else {
|
|
425
|
+
await applyStrategy(ctx, claimed, strategy);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
persist("running");
|
|
429
|
+
setStatus(ctx, describe());
|
|
430
|
+
ctx.ui.notify(`▣ ${claimed.id} · ${state.mode}${state.planning ? " +plan" : ""} — ${claimed.title}`, "info");
|
|
431
|
+
|
|
432
|
+
const prompt = renderPrompt(claimed, all);
|
|
433
|
+
const busy = typeof ctx.isIdle === "function" ? !ctx.isIdle() : false;
|
|
434
|
+
state.sentAt = Date.now();
|
|
435
|
+
pi.sendUserMessage(prompt, busy ? { deliverAs: "followUp" } : undefined);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Wire the chosen strategy for the work turn; falls back toward "none". */
|
|
440
|
+
async function applyStrategy(ctx: ExtensionContext, task: KanboardTask, strategy: Strategy): Promise<void> {
|
|
441
|
+
const setRun = (mode: RunMode, goal?: string): void => {
|
|
442
|
+
void cli
|
|
443
|
+
.run(
|
|
444
|
+
["set-run", task.id, "--mode", mode, ...(goal ? ["--goal", goal] : [])],
|
|
445
|
+
{ extraEnv: { UNIPI_KANBOARD_PROJECT: project() } },
|
|
446
|
+
)
|
|
447
|
+
.catch((error) => deps.debug(`set-run failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
448
|
+
};
|
|
449
|
+
const goalStart = async (): Promise<boolean> => {
|
|
373
450
|
const started = await callCommandRunner<{ ok?: boolean; goalId?: string; reason?: string }>(
|
|
374
451
|
"unipi:goal-start",
|
|
375
452
|
ctx,
|
|
376
|
-
{ objective: `${
|
|
453
|
+
{ objective: `${task.title} — ${(task.body ?? "").slice(0, 400)}`.trim() },
|
|
377
454
|
);
|
|
378
455
|
if (started.found && started.result?.ok && started.result.goalId) {
|
|
379
456
|
state.goalId = started.result.goalId;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
457
|
+
state.mode = "goal";
|
|
458
|
+
setRun("goal", state.goalId);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
};
|
|
463
|
+
switch (strategy) {
|
|
464
|
+
case "none":
|
|
465
|
+
state.mode = "none";
|
|
466
|
+
setRun("none");
|
|
467
|
+
return;
|
|
468
|
+
case "goal":
|
|
469
|
+
if (await goalStart()) return;
|
|
470
|
+
ctx.ui.notify(`kanboard: goal unavailable — running without a strategy`, "warning");
|
|
471
|
+
state.mode = "none";
|
|
472
|
+
setRun("none");
|
|
473
|
+
return;
|
|
474
|
+
case "swarm":
|
|
475
|
+
case "graph": {
|
|
476
|
+
const started = await callCommandRunner<{ ok?: boolean; reason?: string }>(
|
|
477
|
+
"unipi:lh-explicit",
|
|
478
|
+
ctx,
|
|
479
|
+
{ mode: strategy },
|
|
480
|
+
);
|
|
481
|
+
if (started.found && started.result?.ok) {
|
|
482
|
+
state.mode = strategy;
|
|
483
|
+
setRun(strategy);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
386
486
|
ctx.ui.notify(
|
|
387
|
-
`kanboard:
|
|
487
|
+
`kanboard: ${strategy} unavailable (${started.result?.reason ?? "long-horizon not loaded"}) — running without a strategy`,
|
|
388
488
|
"warning",
|
|
389
489
|
);
|
|
390
|
-
state.mode = "
|
|
490
|
+
state.mode = "none";
|
|
491
|
+
setRun("none");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
case "ralph": {
|
|
495
|
+
const body = task.body ?? "";
|
|
496
|
+
const started = await callCommandRunner<{ ok?: boolean; reason?: string }>(
|
|
497
|
+
"unipi:ralph-start",
|
|
498
|
+
ctx,
|
|
499
|
+
{ name: task.id, content: `${task.title}
|
|
500
|
+
|
|
501
|
+
${body}`.trim() },
|
|
502
|
+
);
|
|
503
|
+
if (started.found && started.result?.ok) {
|
|
504
|
+
state.mode = "ralph";
|
|
505
|
+
setRun("ralph");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
ctx.ui.notify(
|
|
509
|
+
`kanboard: ralph unavailable (${started.result?.reason ?? "no checklist items"}) — falling back to goal`,
|
|
510
|
+
"warning",
|
|
511
|
+
);
|
|
512
|
+
if (await goalStart()) return;
|
|
513
|
+
ctx.ui.notify(`kanboard: goal unavailable — running without a strategy`, "warning");
|
|
514
|
+
state.mode = "none";
|
|
515
|
+
setRun("none");
|
|
516
|
+
return;
|
|
391
517
|
}
|
|
392
518
|
}
|
|
519
|
+
}
|
|
393
520
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
521
|
+
/** User-only board bar after each finished task. Best-effort. */
|
|
522
|
+
async function postBoardProgress(): Promise<void> {
|
|
523
|
+
try {
|
|
524
|
+
const { tasks } = asTaskList(await runCli<unknown>(["list"], { extraEnv: { UNIPI_KANBOARD_PROJECT: project() } }));
|
|
525
|
+
const bar = boardProgressData(tasks, project());
|
|
526
|
+
if (bar) appendProgress(pi, bar);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
deps.debug(`board progress failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
529
|
+
}
|
|
403
530
|
}
|
|
404
531
|
|
|
405
532
|
/** Ask the goal engine whether the goal reached a terminal state. */
|
|
@@ -414,7 +541,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
414
541
|
}
|
|
415
542
|
|
|
416
543
|
function planStillActive(): boolean {
|
|
417
|
-
return state.
|
|
544
|
+
return state.planning && state.planOutcome === null;
|
|
418
545
|
}
|
|
419
546
|
|
|
420
547
|
async function finishTask(ctx: ExtensionContext, outcome: "in_review" | "todo" | "blocked", comment: string): Promise<void> {
|
|
@@ -454,6 +581,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
454
581
|
state.phase = "idle";
|
|
455
582
|
persist("idle");
|
|
456
583
|
setStatus(ctx, state.stopAfterCurrent ? undefined : describe());
|
|
584
|
+
await postBoardProgress();
|
|
457
585
|
|
|
458
586
|
await continueLoop(ctx);
|
|
459
587
|
}
|
|
@@ -646,9 +774,14 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
646
774
|
onPlanModeChanged(payload: unknown): void {
|
|
647
775
|
const active = (payload as { active?: boolean } | undefined)?.active;
|
|
648
776
|
const reason = (payload as { reason?: string } | undefined)?.reason;
|
|
649
|
-
if (active === false && state.
|
|
777
|
+
if (active === false && state.planning && state.planOutcome === null) {
|
|
650
778
|
state.planOutcome = reason === "discarded" ? "discarded" : "approved";
|
|
651
779
|
deps.debug(`plan mode left: ${state.planOutcome}`);
|
|
780
|
+
// Strategy applies to the work turn after approval.
|
|
781
|
+
if (state.planOutcome === "approved" && state.pendingStrategy && state.task && state.lastCtx) {
|
|
782
|
+
void applyStrategy(state.lastCtx, state.task, state.pendingStrategy);
|
|
783
|
+
}
|
|
784
|
+
state.pendingStrategy = null;
|
|
652
785
|
}
|
|
653
786
|
},
|
|
654
787
|
|
|
@@ -683,7 +816,9 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
683
816
|
);
|
|
684
817
|
if (resume) {
|
|
685
818
|
state.task = task;
|
|
686
|
-
|
|
819
|
+
// Old session entries may still say "direct" — it maps to "none".
|
|
820
|
+
const restored = (claimed.mode as string | undefined) === "direct" ? "none" : (claimed.mode ?? "none");
|
|
821
|
+
state.mode = restored;
|
|
687
822
|
state.goalId = claimed.goalId ?? null;
|
|
688
823
|
state.phase = "running";
|
|
689
824
|
state.endsSinceSend = 0;
|
|
@@ -729,7 +864,7 @@ export function createRunner(deps: RunnerDeps): Runner {
|
|
|
729
864
|
deps.debug("settle deferred: plan mode still active");
|
|
730
865
|
return;
|
|
731
866
|
}
|
|
732
|
-
if (state.
|
|
867
|
+
if (state.planning && state.planOutcome === "discarded") {
|
|
733
868
|
await finishTask(ctx, "todo", "plan discarded");
|
|
734
869
|
return;
|
|
735
870
|
}
|
package/src/settings.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface KanboardSettings {
|
|
|
31
31
|
maxSessions: number;
|
|
32
32
|
/** `add` calls allowed per -do/runner window (0 = unlimited). */
|
|
33
33
|
turnAddLimit: number;
|
|
34
|
+
/** Strategy for unlabelled tasks ("auto" = jev decides). */
|
|
35
|
+
defaultStrategy: "auto" | "none" | "goal" | "ralph" | "swarm" | "graph";
|
|
36
|
+
/** Plan-first for unlabelled tasks (jev is never asked about plan). */
|
|
37
|
+
defaultPlan: boolean;
|
|
38
|
+
/** Whether a blocked-by-confusion task may ask the user (ask) or must
|
|
39
|
+
* assume-and-note (avoid, the default). */
|
|
40
|
+
blocking: "avoid" | "ask";
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export const DEFAULT_SETTINGS: KanboardSettings = {
|
|
@@ -46,6 +53,9 @@ export const DEFAULT_SETTINGS: KanboardSettings = {
|
|
|
46
53
|
queueMax: 10,
|
|
47
54
|
maxSessions: 2,
|
|
48
55
|
turnAddLimit: 20,
|
|
56
|
+
defaultStrategy: "auto",
|
|
57
|
+
defaultPlan: false,
|
|
58
|
+
blocking: "avoid",
|
|
49
59
|
};
|
|
50
60
|
|
|
51
61
|
export const KANBOARD_NAMESPACE = "kanboard";
|
|
@@ -103,6 +113,48 @@ export function registerKanboardSettings(): void {
|
|
|
103
113
|
{ key: "queueMax", type: "number", label: "-do queue limit", min: 0, zeroLabel: "unlimited", description: "Tasks a session may queue for the runner" },
|
|
104
114
|
{ key: "maxSessions", type: "number", label: "Sessions working at once (per project)", min: 1, description: "Distinct sessions holding in_progress tasks" },
|
|
105
115
|
{ key: "turnAddLimit", type: "number", label: "New tasks per turn", min: 0, zeroLabel: "unlimited", description: "`add` calls allowed per -do turn or runner task" },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: "Task defaults",
|
|
120
|
+
description: "What the runner does when a task carries no label",
|
|
121
|
+
fields: [
|
|
122
|
+
{
|
|
123
|
+
key: "defaultStrategy",
|
|
124
|
+
type: "enum",
|
|
125
|
+
label: "Default strategy",
|
|
126
|
+
options: [
|
|
127
|
+
{ value: "auto", label: "Auto (jev decides)" },
|
|
128
|
+
{ value: "none", label: "none (one-pass change)" },
|
|
129
|
+
{ value: "goal", label: "goal (iterate until verifiably done)" },
|
|
130
|
+
{ value: "ralph", label: "ralph (checklist of similar chores)" },
|
|
131
|
+
{ value: "swarm", label: "swarm (parallel parts)" },
|
|
132
|
+
{ value: "graph", label: "graph (dependent steps)" },
|
|
133
|
+
],
|
|
134
|
+
description: "Strategy for tasks without a --strategy label",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: "defaultPlan",
|
|
138
|
+
type: "boolean",
|
|
139
|
+
label: "Plan first by default",
|
|
140
|
+
description: "Unlabelled tasks get a plan+approval turn before work (jev is never asked)",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "blocking",
|
|
144
|
+
type: "enum",
|
|
145
|
+
label: "Blocking",
|
|
146
|
+
options: [
|
|
147
|
+
{ value: "avoid", label: "Avoid — work autonomously, note assumptions" },
|
|
148
|
+
{ value: "ask", label: "Ask — block the task to ask the user" },
|
|
149
|
+
],
|
|
150
|
+
description: "What a confused runner task may do; avoid = assume-and-note, only block on true impossibles",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
title: "Board",
|
|
156
|
+
description: "Daemon, tokens and links",
|
|
157
|
+
fields: [
|
|
106
158
|
{ key: "open", type: "action", label: "Open board…", description: "Start the daemon and print the URL", command: ACTION_OPEN },
|
|
107
159
|
{ key: "stopDaemon", type: "action", label: "Stop daemon", description: "Terminate the running kanboard daemon", command: ACTION_STOP_DAEMON },
|
|
108
160
|
{ key: "rotateToken", type: "action", label: "Rotate access token", description: "Drop the persistent token; a fresh one mints on the next daemon start", command: ACTION_ROTATE_TOKEN },
|
|
@@ -130,6 +182,13 @@ export function readKanboardSettings(cwd: string = process.cwd()): KanboardSetti
|
|
|
130
182
|
queueMax: typeof raw.queueMax === "number" && raw.queueMax >= 0 ? raw.queueMax : DEFAULT_SETTINGS.queueMax,
|
|
131
183
|
maxSessions: typeof raw.maxSessions === "number" && raw.maxSessions >= 1 ? raw.maxSessions : DEFAULT_SETTINGS.maxSessions,
|
|
132
184
|
turnAddLimit: typeof raw.turnAddLimit === "number" && raw.turnAddLimit >= 0 ? raw.turnAddLimit : DEFAULT_SETTINGS.turnAddLimit,
|
|
185
|
+
defaultStrategy:
|
|
186
|
+
typeof raw.defaultStrategy === "string" &&
|
|
187
|
+
["auto", "none", "goal", "ralph", "swarm", "graph"].includes(raw.defaultStrategy)
|
|
188
|
+
? (raw.defaultStrategy as KanboardSettings["defaultStrategy"])
|
|
189
|
+
: DEFAULT_SETTINGS.defaultStrategy,
|
|
190
|
+
defaultPlan: raw.defaultPlan === true,
|
|
191
|
+
blocking: raw.blocking === "ask" ? "ask" : "avoid",
|
|
133
192
|
};
|
|
134
193
|
}
|
|
135
194
|
|