@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.
@@ -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
+ ];
@@ -0,0 +1,5 @@
1
+ export const TASK_PROMPT_SNIPPET = "Durable project tasks and todo links.";
2
+
3
+ export const TASK_PROMPT_GUIDELINES = [
4
+ "Use `task` for durable project work that should survive compaction and session restarts; use `todo` for current-session checklist steps.",
5
+ ];
@@ -0,0 +1,5 @@
1
+ export const TODO_PROMPT_SNIPPET = "Current-session checklist for execution.";
2
+
3
+ export const TODO_PROMPT_GUIDELINES = [
4
+ "Use `todo` for current-session checklist steps; use `task` for durable project work.",
5
+ ];
package/src/index.ts CHANGED
@@ -1,21 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerTaskBridgeTool } from "./bridge/bridge-tool.js";
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
- registerTsqQueryTool(pi);
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 TASK_BRIDGE_REPLAY_TOOL_NAME = "task_bridge";
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
- * state-mutating `task_bridge` results replay their todo snapshot, while
231
- * successful `task_bridge link` results and successful `tsq_claim` createTodo
232
- * results are replayed onto the current todo snapshot so bridge metadata,
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 === TASK_BRIDGE_REPLAY_TOOL_NAME) {
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 === undefined) continue;
260
- result = applyReplayableBridgeLink(result, link);
261
- continue;
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 = "rpiv-todos";
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", "task_bridge", "tsq_claim"]);
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 const TODO_PROMPT_GUIDELINES: string[] = [
41
- "Use `todo` for current-session tactical work: inspect, edit, verify, and handoff steps for the task in front of you.",
42
- "Use `todo` for session work, not durable backlog; use Tasque/tsq tools for durable ownership, specs, and cross-session work.",
43
- "Mark one task in_progress before starting it, complete it when verified, and keep blocked or partial work out of completed.",
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 working checklist, not durable backlog.",
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. Field order and wording stay compatible with rpiv-todo v1.
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: "Task subject line (required for create)" }),
95
+ Type.String({ description: "Todo subject line (required for create)" }),
96
96
  ),
97
97
  description: Type.Optional(
98
- Type.String({ description: "Long-form task description" }),
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: "Task ids to add to blockedBy (update only, additive merge)",
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
- "Task ids to remove from blockedBy (update only, additive merge)",
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 task" }),
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: "Task id (required for update, get, delete)",
138
+ description: "Todo id (required for update, get, delete)",
139
139
  }),
140
140
  ),
141
141
  includeDeleted: Type.Optional(