@bumpyclock/pi-tasque 0.1.0 → 0.2.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 +168 -140
- package/package.json +24 -6
- package/src/bridge/bridge-tool.ts +10 -13
- package/src/bridge/types.ts +11 -12
- package/src/durable-tasks/bulk-contract.ts +176 -0
- package/src/durable-tasks/handoff-guard.ts +329 -0
- package/src/durable-tasks/project.ts +71 -0
- package/src/durable-tasks/status.ts +21 -3
- package/src/durable-tasks/task-tool.ts +764 -0
- package/src/durable-tasks/tools-bulk.ts +144 -0
- package/src/durable-tasks/tools-change.ts +107 -51
- package/src/durable-tasks/tools-claim.ts +10 -13
- package/src/durable-tasks/tools-query.ts +57 -25
- package/src/durable-tasks/tools-spec.ts +230 -0
- package/src/durable-tasks/tools-tree-create.ts +227 -0
- package/src/guidelines/internal-tools.ts +33 -0
- package/src/guidelines/task.ts +5 -0
- package/src/guidelines/todo.ts +5 -0
- package/src/index.ts +2 -13
- package/src/session-todos/state/replay.ts +10 -13
- package/src/session-todos/todo-overlay.ts +1 -1
- package/src/session-todos/todo.ts +10 -11
- package/src/session-todos/tool/types.ts +7 -7
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { errorToolDetails, textToolResult } from "../shared/tool-result.js";
|
|
7
|
+
import { runQueuedMutation } from "./mutation-queue.js";
|
|
8
|
+
import { runTsqJson } from "./runner.js";
|
|
9
|
+
|
|
10
|
+
export type SpecMode = "show" | "check" | "set" | "update";
|
|
11
|
+
|
|
12
|
+
export const SPEC_READ_MODES: readonly SpecMode[] = ["show", "check"];
|
|
13
|
+
export const SPEC_WRITE_MODES: readonly SpecMode[] = ["set", "update"];
|
|
14
|
+
|
|
15
|
+
export interface SpecParams {
|
|
16
|
+
readonly id: string | undefined;
|
|
17
|
+
readonly mode: SpecMode;
|
|
18
|
+
readonly text?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SpecSuccessDetails {
|
|
22
|
+
readonly ok: true;
|
|
23
|
+
readonly action: "spec";
|
|
24
|
+
readonly mode: SpecMode;
|
|
25
|
+
readonly argv: readonly string[];
|
|
26
|
+
readonly data: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SpecCheckFailedDetails {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly error: {
|
|
32
|
+
readonly code: "spec_check_failed";
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly details: unknown;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SpecDetails = SpecSuccessDetails | SpecCheckFailedDetails;
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SPEC_TIMEOUT_MS = 10_000;
|
|
41
|
+
|
|
42
|
+
export async function executeTsqSpec(
|
|
43
|
+
pi: ExtensionAPI,
|
|
44
|
+
params: SpecParams,
|
|
45
|
+
signal: AbortSignal | undefined,
|
|
46
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
47
|
+
): Promise<AgentToolResult<SpecDetails>> {
|
|
48
|
+
const validated = validateSpecParams(params);
|
|
49
|
+
if (!validated.ok) {
|
|
50
|
+
return textToolResult(
|
|
51
|
+
`Error: ${validated.message}`,
|
|
52
|
+
errorToolDetails({
|
|
53
|
+
code: "validation_error",
|
|
54
|
+
message: validated.message,
|
|
55
|
+
}) as unknown as SpecDetails,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { argv, mode } = validated;
|
|
60
|
+
const isWrite = (SPEC_WRITE_MODES as readonly string[]).includes(mode);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const data = isWrite
|
|
64
|
+
? await runQueuedMutation(ctx.cwd, () =>
|
|
65
|
+
runTsqJson(pi, { cwd: ctx.cwd }, argv, {
|
|
66
|
+
timeout: DEFAULT_SPEC_TIMEOUT_MS,
|
|
67
|
+
...(signal === undefined ? {} : { signal }),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
: await runTsqJson(pi, { cwd: ctx.cwd }, argv, {
|
|
71
|
+
timeout: DEFAULT_SPEC_TIMEOUT_MS,
|
|
72
|
+
...(signal === undefined ? {} : { signal }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (mode === "check" && isSpecCheckFailed(data)) {
|
|
76
|
+
return buildCheckFailedResult(data, argv);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return textToolResult(formatSpecSuccess(mode, data), {
|
|
80
|
+
ok: true,
|
|
81
|
+
action: "spec",
|
|
82
|
+
mode,
|
|
83
|
+
argv,
|
|
84
|
+
data,
|
|
85
|
+
} as SpecSuccessDetails);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
const code = getErrorCode(error);
|
|
89
|
+
return textToolResult(
|
|
90
|
+
`Error: ${message}`,
|
|
91
|
+
errorToolDetails({
|
|
92
|
+
code,
|
|
93
|
+
message,
|
|
94
|
+
details: { action: "spec", mode, argv },
|
|
95
|
+
}) as unknown as SpecDetails,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type ValidationSuccess = {
|
|
101
|
+
readonly ok: true;
|
|
102
|
+
readonly mode: SpecMode;
|
|
103
|
+
readonly argv: string[];
|
|
104
|
+
};
|
|
105
|
+
type ValidationFailure = { readonly ok: false; readonly message: string };
|
|
106
|
+
type ValidationResult = ValidationSuccess | ValidationFailure;
|
|
107
|
+
|
|
108
|
+
export function validateSpecParams(params: SpecParams): ValidationResult {
|
|
109
|
+
const id = params.id?.trim();
|
|
110
|
+
if (id === undefined || id.length === 0) {
|
|
111
|
+
return { ok: false, message: "spec action requires id" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { mode } = params;
|
|
115
|
+
const isRead = (SPEC_READ_MODES as readonly string[]).includes(mode);
|
|
116
|
+
const isWrite = (SPEC_WRITE_MODES as readonly string[]).includes(mode);
|
|
117
|
+
|
|
118
|
+
if (!isRead && !isWrite) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
message: `spec mode must be show, check, set, or update`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isRead && params.text !== undefined) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
message: `spec ${mode} does not accept text`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isWrite) {
|
|
133
|
+
const text = params.text?.trim();
|
|
134
|
+
if (text === undefined || text.length === 0) {
|
|
135
|
+
return { ok: false, message: `spec ${mode} requires text` };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ok: true, mode, argv: buildSpecArgv(id, mode, params.text) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildSpecArgv(
|
|
143
|
+
id: string,
|
|
144
|
+
mode: SpecMode,
|
|
145
|
+
text: string | undefined,
|
|
146
|
+
): string[] {
|
|
147
|
+
switch (mode) {
|
|
148
|
+
case "show":
|
|
149
|
+
return ["spec", id, "--show"];
|
|
150
|
+
case "check":
|
|
151
|
+
return ["spec", id, "--check"];
|
|
152
|
+
case "set":
|
|
153
|
+
return ["spec", id, "--force", `--text=${text!}`];
|
|
154
|
+
case "update":
|
|
155
|
+
return ["spec", id, "--update", `--text=${text!}`];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isSpecCheckFailed(data: unknown): boolean {
|
|
160
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return (data as Record<string, unknown>).ok === false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildCheckFailedResult(
|
|
167
|
+
data: unknown,
|
|
168
|
+
argv: readonly string[],
|
|
169
|
+
): AgentToolResult<SpecDetails> {
|
|
170
|
+
const record = data as Record<string, unknown>;
|
|
171
|
+
const diagnostics = record.diagnostics ?? record.issues ?? record;
|
|
172
|
+
const message =
|
|
173
|
+
typeof record.message === "string" ? record.message : "spec check failed";
|
|
174
|
+
|
|
175
|
+
return textToolResult(`Spec check failed: ${message}`, {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: "spec_check_failed",
|
|
179
|
+
message,
|
|
180
|
+
details: { argv, diagnostics },
|
|
181
|
+
},
|
|
182
|
+
} as SpecCheckFailedDetails);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatSpecSuccess(mode: SpecMode, data: unknown): string {
|
|
186
|
+
switch (mode) {
|
|
187
|
+
case "show": {
|
|
188
|
+
const record = data as Record<string, unknown> | null;
|
|
189
|
+
const spec =
|
|
190
|
+
typeof record?.spec === "object" &&
|
|
191
|
+
record.spec !== null &&
|
|
192
|
+
!Array.isArray(record.spec)
|
|
193
|
+
? (record.spec as Record<string, unknown>)
|
|
194
|
+
: undefined;
|
|
195
|
+
const content =
|
|
196
|
+
typeof spec?.content === "string"
|
|
197
|
+
? spec.content
|
|
198
|
+
: typeof record?.content === "string"
|
|
199
|
+
? record.content
|
|
200
|
+
: undefined;
|
|
201
|
+
const path =
|
|
202
|
+
typeof spec?.path === "string"
|
|
203
|
+
? spec.path
|
|
204
|
+
: typeof record?.path === "string"
|
|
205
|
+
? record.path
|
|
206
|
+
: undefined;
|
|
207
|
+
if (content !== undefined) {
|
|
208
|
+
const header = path !== undefined ? `Spec (${path}):\n` : "Spec:\n";
|
|
209
|
+
return `${header}${content}`;
|
|
210
|
+
}
|
|
211
|
+
return "Spec: no content returned";
|
|
212
|
+
}
|
|
213
|
+
case "check":
|
|
214
|
+
return "Spec check passed";
|
|
215
|
+
case "set":
|
|
216
|
+
return "Spec attached";
|
|
217
|
+
case "update":
|
|
218
|
+
return "Spec updated";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getErrorCode(error: unknown): string {
|
|
223
|
+
if (typeof error === "object" && error !== null && !Array.isArray(error)) {
|
|
224
|
+
const code = (error as Record<string, unknown>).code;
|
|
225
|
+
if (typeof code === "string") {
|
|
226
|
+
return code;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return "tsq_error";
|
|
230
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nested create_tree executor for the `task` tool.
|
|
3
|
+
*
|
|
4
|
+
* Walks a validated `CreateTreeNode` tree depth-first, creating each parent
|
|
5
|
+
* before its children and passing the generated parent id via `--parent`.
|
|
6
|
+
*
|
|
7
|
+
* Fail-fast: on the first creation failure the entire remaining tree is
|
|
8
|
+
* skipped — no orphan children are created. No rollback of already-created
|
|
9
|
+
* nodes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AgentToolResult,
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import type { CreateTreeNode, CreateTreeResult } from "./bulk-contract.js";
|
|
18
|
+
import { runQueuedMutation } from "./mutation-queue.js";
|
|
19
|
+
import { runTsqJson } from "./runner.js";
|
|
20
|
+
import {
|
|
21
|
+
okToolDetails,
|
|
22
|
+
textToolResult,
|
|
23
|
+
type StandardToolDetails,
|
|
24
|
+
} from "../shared/tool-result.js";
|
|
25
|
+
|
|
26
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
interface CreatedEntry {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly title: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FailedEntry {
|
|
34
|
+
readonly title: string;
|
|
35
|
+
readonly error: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SkippedEntry {
|
|
39
|
+
readonly title: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Mutable accumulator threaded through the walk. */
|
|
43
|
+
interface TreeWalkState {
|
|
44
|
+
readonly created: CreatedEntry[];
|
|
45
|
+
failed?: FailedEntry;
|
|
46
|
+
readonly skipped: SkippedEntry[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type CreateTreeDetails = ReturnType<
|
|
50
|
+
typeof okToolDetails<CreateTreeResult>
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export async function executeCreateTree(
|
|
56
|
+
pi: ExtensionAPI,
|
|
57
|
+
root: CreateTreeNode,
|
|
58
|
+
signal: AbortSignal | undefined,
|
|
59
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
60
|
+
): Promise<AgentToolResult<StandardToolDetails<CreateTreeResult>>> {
|
|
61
|
+
const state: TreeWalkState = { created: [], skipped: [] };
|
|
62
|
+
|
|
63
|
+
await walkAndCreate(pi, ctx, signal, root, undefined, state);
|
|
64
|
+
|
|
65
|
+
const result: CreateTreeResult = {
|
|
66
|
+
created: state.created,
|
|
67
|
+
...(state.failed ? { failed: state.failed } : {}),
|
|
68
|
+
skipped: state.skipped,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return textToolResult(formatResultText(result), okToolDetails(result));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Tree walk ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async function walkAndCreate(
|
|
77
|
+
pi: ExtensionAPI,
|
|
78
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
79
|
+
signal: AbortSignal | undefined,
|
|
80
|
+
node: CreateTreeNode,
|
|
81
|
+
parentId: string | undefined,
|
|
82
|
+
state: TreeWalkState,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
// If a prior node already failed, skip this entire subtree.
|
|
85
|
+
if (state.failed) {
|
|
86
|
+
collectSkipped(node, state.skipped);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const argv = buildCreateArgv(node, parentId);
|
|
91
|
+
|
|
92
|
+
let createdId: string;
|
|
93
|
+
try {
|
|
94
|
+
const result = await runMutation(pi, ctx, argv, signal);
|
|
95
|
+
const extracted = extractCreatedId(result, node.title);
|
|
96
|
+
createdId = extracted.id;
|
|
97
|
+
state.created.push({ id: extracted.id, title: extracted.title });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
state.failed = { title: node.title, error: getErrorMessage(error) };
|
|
100
|
+
// Skip all children of this failed node
|
|
101
|
+
if (node.children) {
|
|
102
|
+
for (const child of node.children) {
|
|
103
|
+
collectSkipped(child, state.skipped);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recurse into children with the newly created parent id.
|
|
110
|
+
if (node.children) {
|
|
111
|
+
for (const child of node.children) {
|
|
112
|
+
await walkAndCreate(pi, ctx, signal, child, createdId, state);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── CLI argv builder ───────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function buildCreateArgv(
|
|
120
|
+
node: CreateTreeNode,
|
|
121
|
+
parentId: string | undefined,
|
|
122
|
+
): string[] {
|
|
123
|
+
const argv = [
|
|
124
|
+
"create",
|
|
125
|
+
`--kind=${node.kind}`,
|
|
126
|
+
"-p",
|
|
127
|
+
String(node.priority),
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (node.description) {
|
|
131
|
+
argv.push(`--description=${node.description}`);
|
|
132
|
+
}
|
|
133
|
+
if (parentId) {
|
|
134
|
+
argv.push(`--parent=${parentId}`);
|
|
135
|
+
}
|
|
136
|
+
if (node.planned === true) {
|
|
137
|
+
argv.push("--planned");
|
|
138
|
+
} else if (node.needsPlan === true) {
|
|
139
|
+
argv.push("--needs-plan");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
argv.push("--", node.title);
|
|
143
|
+
return argv;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Mutation runner ────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function runMutation(
|
|
149
|
+
pi: ExtensionAPI,
|
|
150
|
+
ctx: Pick<ExtensionContext, "cwd">,
|
|
151
|
+
argv: readonly string[],
|
|
152
|
+
signal: AbortSignal | undefined,
|
|
153
|
+
): Promise<unknown> {
|
|
154
|
+
const options = signal === undefined ? {} : { signal };
|
|
155
|
+
return runQueuedMutation(ctx.cwd, () =>
|
|
156
|
+
runTsqJson(pi, { cwd: ctx.cwd }, argv, options),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Result extraction ──────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
function extractCreatedId(
|
|
163
|
+
result: unknown,
|
|
164
|
+
fallbackTitle: string,
|
|
165
|
+
): { readonly id: string; readonly title: string } {
|
|
166
|
+
const root = asRecord(result);
|
|
167
|
+
const task = asRecord(root?.task) ?? root;
|
|
168
|
+
|
|
169
|
+
const id = typeof task?.id === "string" ? task.id : undefined;
|
|
170
|
+
if (!id) {
|
|
171
|
+
throw new Error("tsq create did not return a task id");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const title =
|
|
175
|
+
typeof task?.title === "string" ? task.title : fallbackTitle;
|
|
176
|
+
return { id, title };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Skipped collector ──────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function collectSkipped(
|
|
182
|
+
node: CreateTreeNode,
|
|
183
|
+
skipped: SkippedEntry[],
|
|
184
|
+
): void {
|
|
185
|
+
skipped.push({ title: node.title });
|
|
186
|
+
if (node.children) {
|
|
187
|
+
for (const child of node.children) {
|
|
188
|
+
collectSkipped(child, skipped);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Text formatting ────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function formatResultText(result: CreateTreeResult): string {
|
|
196
|
+
const lines: string[] = [];
|
|
197
|
+
|
|
198
|
+
if (result.created.length > 0) {
|
|
199
|
+
const noun = result.created.length === 1 ? "task" : "tasks";
|
|
200
|
+
lines.push(
|
|
201
|
+
`Created ${result.created.length} ${noun}: ${result.created.map((c) => c.id).join(", ")}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.failed) {
|
|
206
|
+
lines.push(`Failed: "${result.failed.title}" — ${result.failed.error}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (result.skipped.length > 0) {
|
|
210
|
+
lines.push(`${result.skipped.length} skipped`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Utilities ──────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function getErrorMessage(error: unknown): string {
|
|
219
|
+
return error instanceof Error ? error.message : String(error);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
223
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
return value as Record<string, unknown>;
|
|
227
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const READ_TASKS_PROMPT_SNIPPET = "Read durable task state.";
|
|
2
|
+
|
|
3
|
+
export const READ_TASKS_PROMPT_GUIDELINES = [
|
|
4
|
+
"Use task read actions for fresh durable task state; read actions do not mutate tasks.",
|
|
5
|
+
"Include spec content only when needed; regular task details are more concise.",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export const CHANGE_TASKS_PROMPT_SNIPPET = "Mutate durable tasks.";
|
|
9
|
+
|
|
10
|
+
export const CHANGE_TASKS_PROMPT_GUIDELINES = [
|
|
11
|
+
"Use task mutations for explicit durable task changes; use `todo` for current-session checklist steps.",
|
|
12
|
+
"Use block/unblock for hard blockers and order/unorder for task sequencing.",
|
|
13
|
+
"Inspect task details or dependencies before and after graph changes when the relationship is not obvious.",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const CLAIM_TASK_PROMPT_SNIPPET = "Claim durable task ownership.";
|
|
17
|
+
|
|
18
|
+
export const CLAIM_TASK_PROMPT_GUIDELINES = [
|
|
19
|
+
"Pass your own role/name as assignee when available, e.g. developer, worker, oracle.",
|
|
20
|
+
"Create a linked todo only when you want one session todo for the claimed task.",
|
|
21
|
+
"Completing a linked todo does not mark the durable task done; durable completion must be explicit.",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const TASK_TODO_BRIDGE_PROMPT_SNIPPET =
|
|
25
|
+
"Link session todos and durable tasks.";
|
|
26
|
+
|
|
27
|
+
export const TASK_TODO_BRIDGE_PROMPT_GUIDELINES = [
|
|
28
|
+
"Use link to associate an existing todo with an existing durable task via todo metadata.",
|
|
29
|
+
"Use list links to inspect current todo ↔ durable task associations.",
|
|
30
|
+
"Use promote to create a durable task from a todo and link the promoted todo explicitly.",
|
|
31
|
+
"Use import to create or reuse session todos from durable task state and link them explicitly.",
|
|
32
|
+
"Todo completion does not mark a durable task done; durable completion stays explicit.",
|
|
33
|
+
];
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { importTsqHandler } from "./bridge/import-tsq.js";
|
|
4
|
-
import { promoteTodoHandler } from "./bridge/promote-todo.js";
|
|
2
|
+
import { registerTaskTool } from "./durable-tasks/task-tool.js";
|
|
5
3
|
import { registerTasqueStatusLifecycle } from "./durable-tasks/status.js";
|
|
6
|
-
import { registerTsqChangeTool } from "./durable-tasks/tools-change.js";
|
|
7
|
-
import { registerTsqClaimTool } from "./durable-tasks/tools-claim.js";
|
|
8
|
-
import { registerTsqQueryTool } from "./durable-tasks/tools-query.js";
|
|
9
4
|
import { registerSessionTodoModule } from "./session-todos/todo.js";
|
|
10
5
|
|
|
11
6
|
export default function piTasqueExtension(pi: ExtensionAPI): void {
|
|
12
7
|
registerSessionTodoModule(pi);
|
|
13
|
-
|
|
14
|
-
registerTsqChangeTool(pi);
|
|
15
|
-
registerTsqClaimTool(pi);
|
|
16
|
-
registerTaskBridgeTool(pi, {
|
|
17
|
-
promote_todo: promoteTodoHandler,
|
|
18
|
-
import_tsq: importTsqHandler,
|
|
19
|
-
});
|
|
8
|
+
registerTaskTool(pi);
|
|
20
9
|
registerTasqueStatusLifecycle(pi);
|
|
21
10
|
}
|
|
@@ -33,8 +33,7 @@ const VALID_STATUSES = new Set<TaskStatus>([
|
|
|
33
33
|
"deleted",
|
|
34
34
|
]);
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
const TSQ_CLAIM_REPLAY_TOOL_NAME = "tsq_claim";
|
|
36
|
+
const DURABLE_TASK_REPLAY_TOOL_NAME = "task";
|
|
38
37
|
const TASK_BRIDGE_MUTATION_ACTIONS = new Set(["promote_todo", "import_tsq"]);
|
|
39
38
|
|
|
40
39
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -226,11 +225,10 @@ function applyReplayableClaimTodo(state: TaskState, todo: Task): TaskState {
|
|
|
226
225
|
|
|
227
226
|
/**
|
|
228
227
|
* Rebuild todo state from the current session branch. The latest compatible
|
|
229
|
-
* `todo` tool result wins; malformed snapshots are skipped. Successful
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* imports/promotions, and claim-created todos survive reload/branch replay.
|
|
228
|
+
* `todo` tool result wins; malformed snapshots are skipped. Successful durable
|
|
229
|
+
* `task` results replay todo snapshots, links, and claim-created todos so
|
|
230
|
+
* bridge metadata, imports/promotions, and claim-created todos survive
|
|
231
|
+
* reload/branch replay.
|
|
234
232
|
*/
|
|
235
233
|
export function replayFromBranch(ctx: BranchContext): TaskState {
|
|
236
234
|
let result = emptyState();
|
|
@@ -248,7 +246,7 @@ export function replayFromBranch(ctx: BranchContext): TaskState {
|
|
|
248
246
|
continue;
|
|
249
247
|
}
|
|
250
248
|
|
|
251
|
-
if (message.toolName ===
|
|
249
|
+
if (message.toolName === DURABLE_TASK_REPLAY_TOOL_NAME) {
|
|
252
250
|
const snapshot = getReplayableBridgeTodoSnapshot(message.details);
|
|
253
251
|
if (snapshot !== undefined) {
|
|
254
252
|
result = snapshot;
|
|
@@ -256,12 +254,11 @@ export function replayFromBranch(ctx: BranchContext): TaskState {
|
|
|
256
254
|
}
|
|
257
255
|
|
|
258
256
|
const link = getReplayableBridgeLink(message.details);
|
|
259
|
-
if (link
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
if (link !== undefined) {
|
|
258
|
+
result = applyReplayableBridgeLink(result, link);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
263
261
|
|
|
264
|
-
if (message.toolName === TSQ_CLAIM_REPLAY_TOOL_NAME) {
|
|
265
262
|
const todo = getReplayableClaimTodo(message.details);
|
|
266
263
|
if (todo === undefined) continue;
|
|
267
264
|
result = applyReplayableClaimTodo(result, todo);
|
|
@@ -21,7 +21,7 @@ import type { TaskState } from "./state/state.js";
|
|
|
21
21
|
import { getState } from "./state/store.js";
|
|
22
22
|
import { formatOverlayTaskLine, formatStatusLabel } from "./view/format.js";
|
|
23
23
|
|
|
24
|
-
const WIDGET_KEY = "
|
|
24
|
+
const WIDGET_KEY = "pi-tasque-todos";
|
|
25
25
|
const MAX_WIDGET_LINES = 12;
|
|
26
26
|
const OVERLAY_HEADING = "Todos";
|
|
27
27
|
const OVERLAY_MORE = "more";
|
|
@@ -2,6 +2,10 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
TODO_PROMPT_GUIDELINES,
|
|
7
|
+
TODO_PROMPT_SNIPPET,
|
|
8
|
+
} from "../guidelines/todo.js";
|
|
5
9
|
import {
|
|
6
10
|
selectTasksByStatus,
|
|
7
11
|
selectTodoCounts,
|
|
@@ -32,17 +36,12 @@ import {
|
|
|
32
36
|
const SECTION_PENDING = "── Pending ──";
|
|
33
37
|
const SECTION_IN_PROGRESS = "── In Progress ──";
|
|
34
38
|
const SECTION_COMPLETED = "── Completed ──";
|
|
35
|
-
const TODO_AFFECTING_TOOLS = new Set(["todo", "
|
|
36
|
-
|
|
37
|
-
export const TODO_PROMPT_SNIPPET =
|
|
38
|
-
"Manage current-session tactical todos for multi-step execution.";
|
|
39
|
+
const TODO_AFFECTING_TOOLS = new Set(["todo", "task"]);
|
|
39
40
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"Use blockedBy for session-local dependencies; list hides deleted tombstones unless includeDeleted is true.",
|
|
45
|
-
];
|
|
41
|
+
export {
|
|
42
|
+
TODO_PROMPT_GUIDELINES,
|
|
43
|
+
TODO_PROMPT_SNIPPET,
|
|
44
|
+
} from "../guidelines/todo.js";
|
|
46
45
|
|
|
47
46
|
export { isTransitionValid } from "./state/invariants.js";
|
|
48
47
|
export { applyTaskMutation } from "./state/state-reducer.js";
|
|
@@ -75,7 +74,7 @@ export function registerTodoTool(pi: ExtensionAPI): void {
|
|
|
75
74
|
name: TOOL_NAME,
|
|
76
75
|
label: TOOL_LABEL,
|
|
77
76
|
description:
|
|
78
|
-
"Manage current-session todos for tactical execution. Actions: create, update, list, get, delete, clear. Use for this session's
|
|
77
|
+
"Manage current-session todos for tactical execution. Actions: create, update, list, get, delete, clear. Use for this session's checklist; use task for durable project work.",
|
|
79
78
|
promptSnippet: TODO_PROMPT_SNIPPET,
|
|
80
79
|
promptGuidelines: TODO_PROMPT_GUIDELINES,
|
|
81
80
|
parameters: TodoParamsSchema,
|
|
@@ -79,7 +79,7 @@ export interface TaskMutationParams {
|
|
|
79
79
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
// TypeBox parameter schema — every `description` doubles as LLM-facing prompt
|
|
82
|
-
// copy.
|
|
82
|
+
// copy. Keep field order and wording stable for replay and agent ergonomics.
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
84
|
|
|
85
85
|
export const TodoParamsSchema = Type.Object({
|
|
@@ -92,10 +92,10 @@ export const TodoParamsSchema = Type.Object({
|
|
|
92
92
|
"clear",
|
|
93
93
|
] as const),
|
|
94
94
|
subject: Type.Optional(
|
|
95
|
-
Type.String({ description: "
|
|
95
|
+
Type.String({ description: "Todo subject line (required for create)" }),
|
|
96
96
|
),
|
|
97
97
|
description: Type.Optional(
|
|
98
|
-
Type.String({ description: "Long-form
|
|
98
|
+
Type.String({ description: "Long-form todo description" }),
|
|
99
99
|
),
|
|
100
100
|
activeForm: Type.Optional(
|
|
101
101
|
Type.String({
|
|
@@ -115,17 +115,17 @@ export const TodoParamsSchema = Type.Object({
|
|
|
115
115
|
),
|
|
116
116
|
addBlockedBy: Type.Optional(
|
|
117
117
|
Type.Array(Type.Number(), {
|
|
118
|
-
description: "
|
|
118
|
+
description: "Todo ids to add to blockedBy (update only, additive merge)",
|
|
119
119
|
}),
|
|
120
120
|
),
|
|
121
121
|
removeBlockedBy: Type.Optional(
|
|
122
122
|
Type.Array(Type.Number(), {
|
|
123
123
|
description:
|
|
124
|
-
"
|
|
124
|
+
"Todo ids to remove from blockedBy (update only, additive merge)",
|
|
125
125
|
}),
|
|
126
126
|
),
|
|
127
127
|
owner: Type.Optional(
|
|
128
|
-
Type.String({ description: "Agent/owner assigned to this
|
|
128
|
+
Type.String({ description: "Agent/owner assigned to this todo" }),
|
|
129
129
|
),
|
|
130
130
|
metadata: Type.Optional(
|
|
131
131
|
Type.Record(Type.String(), Type.Unknown(), {
|
|
@@ -135,7 +135,7 @@ export const TodoParamsSchema = Type.Object({
|
|
|
135
135
|
),
|
|
136
136
|
id: Type.Optional(
|
|
137
137
|
Type.Number({
|
|
138
|
-
description: "
|
|
138
|
+
description: "Todo id (required for update, get, delete)",
|
|
139
139
|
}),
|
|
140
140
|
),
|
|
141
141
|
includeDeleted: Type.Optional(
|