@pi-archimedes/todo 2.7.3 → 2.9.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/package.json +5 -5
- package/src/prepare-args.test.ts +58 -0
- package/src/prepare-args.ts +55 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/todo",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"./prepare-args": "./src/prepare-args.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@pi-archimedes/core": "2.
|
|
22
|
+
"@pi-archimedes/core": "2.9.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@earendil-works/pi-ai": ">=0.1.0",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@earendil-works/pi-tui": ">=0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@earendil-works/pi-ai": "^0.
|
|
31
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
32
|
-
"@earendil-works/pi-tui": "^0.
|
|
30
|
+
"@earendil-works/pi-ai": "^0.87.0",
|
|
31
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
32
|
+
"@earendil-works/pi-tui": "^0.87.0",
|
|
33
33
|
"typescript": "^6.0.0"
|
|
34
34
|
},
|
|
35
35
|
"pi": {
|
package/src/prepare-args.test.ts
CHANGED
|
@@ -168,6 +168,64 @@ describe("prepareTodoArguments", () => {
|
|
|
168
168
|
expect(Object.hasOwn(out, "todoList")).toBe(false);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it("recovers a list sent under the snake_case key todo_list (stringified JSON)", () => {
|
|
172
|
+
const out = prepareTodoArguments({
|
|
173
|
+
operation: "write",
|
|
174
|
+
todo_list: '[{"content": "Fix auth", "status": "pending"}]',
|
|
175
|
+
});
|
|
176
|
+
expect(out).toEqual({ operation: "write", todoList: [{ content: "Fix auth", status: "pending" }] });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("recovers a list sent under the alias key todos (array, legacy item shape)", () => {
|
|
180
|
+
const out = prepareTodoArguments({ operation: "write", todos: [LEGACY_ITEM] });
|
|
181
|
+
expect(out).toEqual({ operation: "write", todoList: [{ content: "Fix auth", description: "Y", status: "pending" }] });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("prefers the canonical todoList key over fallback keys", () => {
|
|
185
|
+
const out = prepareTodoArguments({
|
|
186
|
+
operation: "write",
|
|
187
|
+
todoList: [{ content: "canonical", status: "pending" }],
|
|
188
|
+
todo_list: [{ content: "ignored", status: "pending" }],
|
|
189
|
+
});
|
|
190
|
+
expect(out.todoList).toEqual([{ content: "canonical", status: "pending" }]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("skips a null fallback key and uses the next present one", () => {
|
|
194
|
+
const out = prepareTodoArguments({
|
|
195
|
+
operation: "write",
|
|
196
|
+
todo_list: null,
|
|
197
|
+
todos: [{ content: "X", status: "pending" }],
|
|
198
|
+
});
|
|
199
|
+
expect(out.todoList).toEqual([{ content: "X", status: "pending" }]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("drops an explicit null todoList even when a fallback key is present", () => {
|
|
203
|
+
const out = prepareTodoArguments({
|
|
204
|
+
operation: "write",
|
|
205
|
+
todoList: null,
|
|
206
|
+
todos: [{ content: "X", status: "pending" }],
|
|
207
|
+
});
|
|
208
|
+
expect(out).toEqual({ operation: "write" });
|
|
209
|
+
expect(Object.hasOwn(out, "todoList")).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("infers operation=write from a fallback-key list", () => {
|
|
213
|
+
expect(prepareTodoArguments({ todos: ["x"] }).operation).toBe("write");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("recovers a double-encoded (stringified) arguments object", () => {
|
|
217
|
+
const out = prepareTodoArguments('{"operation":"write","todo_list":"[\\"a\\"]"}');
|
|
218
|
+
expect(out).toEqual({ operation: "write", todoList: [{ content: "a", status: "pending" }] });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("passes an unparseable top-level string through untouched", () => {
|
|
222
|
+
expect(prepareTodoArguments("not json at all")).toBe("not json at all");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("keeps a top-level string that parses to a non-object untouched", () => {
|
|
226
|
+
expect(prepareTodoArguments("[1, 2]")).toBe("[1, 2]");
|
|
227
|
+
});
|
|
228
|
+
|
|
171
229
|
it("wraps a single bare object in an array and drops extra keys (item + top level)", () => {
|
|
172
230
|
const out = prepareTodoArguments({
|
|
173
231
|
operation: "write",
|
package/src/prepare-args.ts
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* - `todoList` is a stringified JSON array instead of an array (sometimes
|
|
10
10
|
* with mangled escaping, e.g. `"id": 1"` — an extra quote after the
|
|
11
11
|
* number)
|
|
12
|
+
* - `todoList` is sent under a different top-level key — `todo_list`
|
|
13
|
+
* (snake_case), `todos` (the parameter name of other harnesses' todo
|
|
14
|
+
* tools) — observed in real sessions; the payload is dropped entirely
|
|
15
|
+
* and the write fails with "todoList required" without this repair
|
|
16
|
+
* - the whole arguments object is double-encoded as a JSON string
|
|
12
17
|
* - items put the task text under `title`/`step` (shapes borrowed from
|
|
13
18
|
* other harnesses) instead of the canonical `content`
|
|
14
19
|
* - `id` is invented, omitted, or nulled — the canonical schema has NO
|
|
@@ -73,6 +78,24 @@ const STATUS_ALIASES: ReadonlyMap<string, TodoStatus> = new Map<string, TodoStat
|
|
|
73
78
|
const CONTENT_FALLBACK_KEYS = ["title", "step", "task", "text", "name", "label", "activeForm"] as const;
|
|
74
79
|
const DESCRIPTION_FALLBACK_KEYS = ["details", "notes", "summary", "context"] as const;
|
|
75
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Keys the model sometimes uses for the top-level list argument instead of
|
|
83
|
+
* `todoList` (shapes borrowed from other harnesses: `todo_list` is the
|
|
84
|
+
* snake_case form, `todos` is the parameter of other harnesses' todo tools).
|
|
85
|
+
* Checked only when `todoList` itself is absent; first present wins.
|
|
86
|
+
*/
|
|
87
|
+
const TODO_LIST_FALLBACK_KEYS = [
|
|
88
|
+
"todo_list",
|
|
89
|
+
"todos",
|
|
90
|
+
"todo",
|
|
91
|
+
"todoItems",
|
|
92
|
+
"todo_items",
|
|
93
|
+
"todo-items",
|
|
94
|
+
"task_list",
|
|
95
|
+
"taskList",
|
|
96
|
+
"tasks",
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
76
99
|
/** Reserved keys the last-resort text scan must never pick up. */
|
|
77
100
|
const LAST_RESORT_SKIP_KEYS = new Set<string>([
|
|
78
101
|
"content",
|
|
@@ -263,6 +286,23 @@ function normalizeTodoList(raw: unknown): { value: unknown; kept: boolean } {
|
|
|
263
286
|
return { value, kept: true }; // Unmendable — original validation error stands
|
|
264
287
|
}
|
|
265
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Pick the raw list value out of a record of tool arguments: the canonical
|
|
291
|
+
* `todoList` key wins when present (even null — an explicit null is honored
|
|
292
|
+
* over a stray alias); otherwise the first non-nullish fallback key. Returns
|
|
293
|
+
* undefined when no key is present. Shared with the subagent stream, which
|
|
294
|
+
* reads raw (pre-prepareArguments) child tool-call args.
|
|
295
|
+
*/
|
|
296
|
+
export function pickTodoListRaw(args: Record_ | undefined | null): unknown {
|
|
297
|
+
if (!args) return undefined;
|
|
298
|
+
if (args.todoList !== undefined) return args.todoList;
|
|
299
|
+
for (const key of TODO_LIST_FALLBACK_KEYS) {
|
|
300
|
+
const value = args[key];
|
|
301
|
+
if (value !== undefined && value !== null) return value;
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
function normalizeOperation(raw: unknown, listPresent: boolean): "write" | "read" {
|
|
267
307
|
if (raw === "write" || raw === "read") return raw;
|
|
268
308
|
if (typeof raw === "string") {
|
|
@@ -278,11 +318,23 @@ function normalizeOperation(raw: unknown, listPresent: boolean): "write" | "read
|
|
|
278
318
|
* Never throws, never mutates its input, and passes through anything it
|
|
279
319
|
* cannot recover.
|
|
280
320
|
*/
|
|
281
|
-
export function prepareTodoArguments(
|
|
321
|
+
export function prepareTodoArguments(rawArgs: unknown): ManageTodoListInput {
|
|
322
|
+
let args: unknown = rawArgs;
|
|
323
|
+
if (typeof args === "string") {
|
|
324
|
+
// The model double-encoded the whole arguments object as a JSON string.
|
|
325
|
+
// Unparseable, or parsed to a scalar/array — keep the string so the
|
|
326
|
+
// schema error stays meaningful.
|
|
327
|
+
try {
|
|
328
|
+
const parsed: unknown = JSON.parse(args);
|
|
329
|
+
if (isRecord(parsed)) args = parsed;
|
|
330
|
+
} catch {
|
|
331
|
+
// passthrough
|
|
332
|
+
}
|
|
333
|
+
}
|
|
282
334
|
if (!isRecord(args)) return args as ManageTodoListInput;
|
|
283
335
|
|
|
284
|
-
const
|
|
285
|
-
|
|
336
|
+
const rawList = pickTodoListRaw(args);
|
|
337
|
+
const normalized = rawList === undefined ? undefined : normalizeTodoList(rawList);
|
|
286
338
|
const present =
|
|
287
339
|
normalized !== undefined && normalized.kept && normalized.value !== undefined;
|
|
288
340
|
|