@xingular/harness 1.4.0-unstable → 1.5.0-unstable

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE.md +9 -7
  3. package/dist/cli.js +160 -456
  4. package/dist/sdk/prompt/compile.d.ts +4 -2
  5. package/dist/sdk/prompt/compile.js +10 -8
  6. package/dist/sdk/prompt/reporting.d.ts +2 -2
  7. package/dist/sdk/prompt/reporting.js +40 -250
  8. package/dist/sdk/prompt/standard.d.ts +4 -1
  9. package/dist/sdk/prompt/standard.js +6 -2
  10. package/dist/sdk/sessions/completionCheck.d.ts +4 -2
  11. package/dist/sdk/sessions/completionCheck.js +19 -1
  12. package/dist/sdk/sessions/contracts.js +2 -2
  13. package/dist/sdk/sessions/goalStop.d.ts +27 -0
  14. package/dist/sdk/sessions/goalStop.js +41 -0
  15. package/dist/sdk/sessions/runner.d.ts +0 -3
  16. package/dist/sdk/sessions/runner.js +16 -9
  17. package/dist/sdk/sessions/sessions.js +6 -36
  18. package/dist/sdk/sessions/workSlice.js +1 -1
  19. package/dist/sdk/terminal/piExtension.js +18 -31
  20. package/dist/sdk/terminal/terminals.js +1 -1
  21. package/dist/sdk/tools/askUser.js +1 -1
  22. package/dist/sdk/tools/todo.js +4 -3
  23. package/dist/terminal-opentui.js +108 -200
  24. package/dist/terminal-pi.js +262 -338
  25. package/docs/architecture/02-sessions-and-runs.md +13 -15
  26. package/docs/architecture/04-prompt.md +41 -35
  27. package/docs/architecture/07-tools-and-gate.md +2 -2
  28. package/docs/architecture/10-server-protocol.md +6 -5
  29. package/docs/architecture/11-terminal.md +7 -7
  30. package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +2 -2
  31. package/docs/decisions/ADR-0069-stop-gate-and-finish.md +8 -2
  32. package/docs/decisions/ADR-0070-terminal-slice-continuation.md +4 -1
  33. package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +3 -0
  34. package/docs/decisions/ADR-0073-large-reporting-prompt.md +6 -1
  35. package/docs/decisions/ADR-0074-report-content-not-report-blocks.md +90 -0
  36. package/docs/decisions/ADR-0075-no-todo-gate.md +84 -0
  37. package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +93 -0
  38. package/docs/releases/1.4.0-unstable.md +9 -2
  39. package/docs/releases/1.5.0-unstable.md +23 -0
  40. package/docs/study/STUDY-0004-turn-end-controls.md +7 -0
  41. package/docs/study/STUDY-0006-reporting-prompts.md +48 -10
  42. package/package.json +1 -1
  43. package/dist/sdk/sessions/stopGate.d.ts +0 -78
  44. package/dist/sdk/sessions/stopGate.js +0 -143
@@ -1429,7 +1429,7 @@ function isActiveStatus(status) {
1429
1429
 
1430
1430
  // src/tools/todo.ts
1431
1431
  var TODO_WRITE = "todo_write";
1432
- var TODO_GUIDANCE = "If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. " + 'The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, list the steps of a handoff or plan instead of folding them into one "remaining work" item, and never stop because the list is short or done. ' + "Keep the list true at all times: mark a step completed only once it is done and checked, and never mark unfinished work completed. An open step is work to do in this reply, not something to report. Before you end the turn, compare the request, not the list, with what is done and checked, and do whatever is still yours to do. " + "The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.";
1432
+ var TODO_GUIDANCE = "If the work has several distinct steps, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. " + 'The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, and list the steps of a handoff or plan instead of folding them into one "remaining work" item. ' + "Keep the list true: mark a step completed only once it is done and checked, and never mark unfinished work completed. Before you end the turn, compare the request, not the list, with what is done and checked; if a step stays open, say why in the reply. " + "When a login blocks a step, leave it open and give the exact command the person must run in the reply. " + "The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.";
1433
1433
  var TODO_EVENTS = new Set(["todo.updated", "checklist.updated"]);
1434
1434
  var MARK = { pending: "[ ]", in_progress: "[~]", completed: "[x]" };
1435
1435
  function describeTodo(items) {
@@ -1666,7 +1666,7 @@ function askUserTool(run) {
1666
1666
  return {
1667
1667
  name: ASK_USER,
1668
1668
  label: "Ask the user",
1669
- description: "Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1\u20132 word label and aim to keep each question to 25 words or fewer. " + "Every question must offer 1\u20136 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text \u201CSomething else\u201D answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel\u2019s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked.",
1669
+ description: "Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1\u20132 word label and aim to keep each question to 25 words or fewer. " + "Every question must offer 1\u20136 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text \u201CSomething else\u201D answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel\u2019s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked. Never use it to ask for a login, a secret or a credential: give the exact command in the reply instead.",
1670
1670
  parameters: (Type) => Type.Object({
1671
1671
  questions: Type.Array(Type.Object({
1672
1672
  summary: Type.String({ description: "A concise label of one or two words" }),
@@ -17361,73 +17361,36 @@ import { readFileSync as readFileSync9 } from "fs";
17361
17361
  import { createBashTool, createEditTool, createEditToolDefinition, createPowerShellTool, createWriteTool, createWriteToolDefinition } from "@earendil-works/pi-coding-agent";
17362
17362
  import { Type as Type2 } from "typebox";
17363
17363
 
17364
- // src/sessions/compactionCheckpoint.ts
17365
- var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
17366
- function validatedTodo(value) {
17367
- if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS)
17368
- return null;
17369
- const items = [];
17370
- for (const valueItem of value) {
17371
- const item = valueItem;
17372
- const parsed = todoItemSchema.safeParse({
17373
- text: typeof item?.text === "string" ? item.text.trim() : item?.text,
17374
- status: item?.status
17375
- });
17376
- if (!parsed.success)
17377
- return null;
17378
- items.push(parsed.data);
17364
+ // src/sessions/goalStop.ts
17365
+ function openItems(todo) {
17366
+ return todo.filter((item) => item.status !== "completed");
17367
+ }
17368
+ function goalStopOutcome(todo, goal) {
17369
+ const open = openItems(todo);
17370
+ if (goal?.status === "blocked")
17371
+ return { code: "needs_person", reason: goal.lastReason || "The goal is blocked.", open };
17372
+ if (goal && (goal.status === "budget_limited" || goal.status === "usage_limited" || goal.status === "paused")) {
17373
+ return { code: "unfinished", reason: goal.lastReason || `The goal is ${goal.status.replace("_", " ")}.`, open };
17379
17374
  }
17380
- return items;
17375
+ return null;
17381
17376
  }
17382
- function compactionCheckpoint(options = {}) {
17383
- return (pi) => {
17384
- let goal = "";
17385
- let todo = validatedTodo(options.initialTodo ?? []) ?? [];
17386
- let baseline;
17387
- let checkpoint;
17388
- const reset = (prompt = "") => {
17389
- goal = prompt;
17390
- todo = validatedTodo(options.initialTodo ?? []) ?? [];
17391
- baseline = undefined;
17392
- checkpoint = undefined;
17393
- };
17394
- pi.on("before_agent_start", (event) => {
17395
- if (event.messageType !== "user")
17396
- return;
17397
- reset(event.prompt);
17398
- });
17399
- pi.on("tool_result", (event) => {
17400
- if (event.toolName !== TODO_WRITE || event.isError)
17401
- return;
17402
- const items = validatedTodo(event.input.items);
17403
- if (items)
17404
- todo = items;
17405
- });
17406
- pi.on("context", (event) => {
17407
- const index = event.messages.findLastIndex((message) => message.role === "compactionSummary");
17408
- const summary = index >= 0 ? event.messages[index] : undefined;
17409
- const key = summary ? `${summary.timestamp}:${summary.tokensBefore}` : null;
17410
- if (!baseline) {
17411
- baseline = { key };
17412
- return;
17413
- }
17414
- if (!key || key === baseline.key)
17415
- return;
17416
- if (checkpoint?.key !== key) {
17417
- const content = renderCompactionCheckpoint({ goal, todo, planInstructions: options.planInstructions?.() ?? null });
17418
- checkpoint = {
17419
- key,
17420
- message: content ? { role: "custom", customType: COMPACTION_CHECKPOINT_EXTENSION, content, display: false, timestamp: Date.now() } : null
17421
- };
17422
- }
17423
- if (!checkpoint.message)
17424
- return;
17425
- return { messages: [...event.messages.slice(0, index + 1), checkpoint.message, ...event.messages.slice(index + 1)] };
17426
- });
17427
- pi.on("agent_settled", () => reset());
17428
- pi.on("session_tree", () => reset());
17377
+ function handoffFields(todo, goal) {
17378
+ const next = openItems(todo)[0]?.text || "";
17379
+ const blocked = goal?.status === "blocked" ? (goal.lastReason || "The goal is blocked.").slice(0, 500) : "";
17380
+ return {
17381
+ done: todo.filter((item) => item.status === "completed").slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
17382
+ next: next.slice(0, 500),
17383
+ ...blocked ? { blocked } : {}
17429
17384
  };
17430
17385
  }
17386
+ var stopCheckpointReason = (outcome) => outcome.code === "needs_person" ? "requested" : "stuck";
17387
+ function stopSummary(outcome) {
17388
+ if (outcome.code === "needs_person")
17389
+ return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1200);
17390
+ if (outcome.reason)
17391
+ return `The reply ended with work left: ${outcome.reason}`.slice(0, 1200);
17392
+ return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? "" : "s"}.`;
17393
+ }
17431
17394
 
17432
17395
  // src/sessions/completionCheck.ts
17433
17396
  var MAX_CHECK_TRANSCRIPT = 24000;
@@ -17555,121 +17518,6 @@ async function runCompletionCheck(ask, input, options = {}) {
17555
17518
  function answerText(message) {
17556
17519
  return textOf(message?.content);
17557
17520
  }
17558
- function launchSwitchOff(name) {
17559
- const value = process.env[name]?.trim().toLowerCase();
17560
- return value === "off" || value === "0" || value === "false";
17561
- }
17562
-
17563
- // src/sessions/stopGate.ts
17564
- var STOP_GATE_EXTENSION = "stop-gate";
17565
- var STOP_GATE_CUSTOM_TYPE = "xingular-stop-gate";
17566
- var MAX_STOP_GATE_NUDGES = 3;
17567
- var BOOKKEEPING_TOOLS = new Set([TODO_WRITE, "session_name"]);
17568
- function createStopGateState(initialTodo = []) {
17569
- return { todo: validatedTodo(initialTodo) ?? [], request: "", nudges: 0, callsSinceNudge: 0, callsInReply: 0 };
17570
- }
17571
- function resetStopGateState(state, initialTodo = [], request = "") {
17572
- state.todo = validatedTodo(initialTodo) ?? [];
17573
- state.request = request;
17574
- state.nudges = 0;
17575
- state.callsSinceNudge = 0;
17576
- state.callsInReply = 0;
17577
- }
17578
- function openItems(todo) {
17579
- return todo.filter((item) => item.status !== "completed");
17580
- }
17581
- function stopOutcome(state, goal) {
17582
- const open = openItems(state.todo);
17583
- if (goal?.status === "blocked")
17584
- return { code: "needs_person", reason: goal.lastReason || "The goal is blocked.", open };
17585
- if (goal && (goal.status === "budget_limited" || goal.status === "usage_limited" || goal.status === "paused")) {
17586
- return { code: "unfinished", reason: goal.lastReason || `The goal is ${goal.status.replace("_", " ")}.`, open };
17587
- }
17588
- if (open.length)
17589
- return { code: "unfinished", reason: "", open };
17590
- return null;
17591
- }
17592
- function handoffFields(state, goal) {
17593
- const todo = state?.todo ?? [];
17594
- const next = openItems(todo)[0]?.text || "";
17595
- const blocked = goal?.status === "blocked" ? (goal.lastReason || "The goal is blocked.").slice(0, 500) : "";
17596
- return {
17597
- done: todo.filter((item) => item.status === "completed").slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
17598
- next: next.slice(0, 500),
17599
- ...blocked ? { blocked } : {}
17600
- };
17601
- }
17602
- var stopCheckpointReason = (outcome) => outcome.code === "needs_person" ? "requested" : "stuck";
17603
- function stopSummary(outcome) {
17604
- if (outcome.code === "needs_person")
17605
- return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1200);
17606
- if (outcome.reason)
17607
- return `The reply ended with work left: ${outcome.reason}`.slice(0, 1200);
17608
- return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? "" : "s"}.`;
17609
- }
17610
- function nudgeText(count, max, open) {
17611
- const list = open.slice(0, 12).map((item) => `- ${item.text}`).join(`
17612
- `);
17613
- const more = open.length > 12 ? `
17614
- - \u2026and ${open.length - 12} more` : "";
17615
- return [
17616
- `Harness stop gate (${count}/${max}): your reply ended with ${open.length} open todo item${open.length === 1 ? "" : "s"}:`,
17617
- `${list}${more}`,
17618
- "Continue with the next open item now. Mark an item completed only after it is done and checked.",
17619
- "The list shows progress; it is not the limit of the work. If the request needs more than the list says, add it and do it.",
17620
- "Decisions inside the request are yours. Stop only for something only the person can do (a deploy, a secret, remote data, money, missing access), and then say exactly what it is."
17621
- ].join(`
17622
- `);
17623
- }
17624
- function stopGateNotice(event) {
17625
- return `Stop gate ${event.count}: the reply ended with ${event.open.length} open todo item${event.open.length === 1 ? "" : "s"}; continuing.`;
17626
- }
17627
- function stopGate(options) {
17628
- const { state } = options;
17629
- const max = options.max ?? MAX_STOP_GATE_NUDGES;
17630
- return (pi) => {
17631
- pi.on("before_agent_start", (event) => {
17632
- if (event.messageType !== undefined && event.messageType !== "user")
17633
- return;
17634
- resetStopGateState(state, options.initialTodo, options.request?.() || (typeof event.prompt === "string" ? event.prompt : ""));
17635
- });
17636
- pi.on("tool_call", (event) => {
17637
- if (BOOKKEEPING_TOOLS.has(event.toolName))
17638
- return;
17639
- state.callsSinceNudge += 1;
17640
- state.callsInReply += 1;
17641
- });
17642
- pi.on("tool_result", (event) => {
17643
- if (event.toolName !== TODO_WRITE || event.isError)
17644
- return;
17645
- const items = validatedTodo(event.input?.items);
17646
- if (items)
17647
- state.todo = items;
17648
- });
17649
- pi.on("agent_before_settle", (event, ctx) => {
17650
- if (event.outcome !== "completed" || event.continue || ctx?.signal?.aborted)
17651
- return;
17652
- if (options.active && !options.active())
17653
- return;
17654
- if (options.sliceEnded?.() || options.failed?.())
17655
- return;
17656
- if (state.nudges >= max)
17657
- return;
17658
- if (state.nudges > 0 && state.callsSinceNudge === 0)
17659
- return;
17660
- const open = openItems(state.todo);
17661
- if (!open.length)
17662
- return;
17663
- state.nudges += 1;
17664
- state.callsSinceNudge = 0;
17665
- options.onEvent?.({ kind: "todo", count: state.nudges, open });
17666
- return {
17667
- entries: [...event.entries, { type: "custom_message", customType: STOP_GATE_CUSTOM_TYPE, content: nudgeText(state.nudges, max, open), display: true }],
17668
- continue: true
17669
- };
17670
- });
17671
- };
17672
- }
17673
17521
  function sessionCheckModel(ctx) {
17674
17522
  const session = ctx ?? {};
17675
17523
  const model = session.model;
@@ -17686,6 +17534,10 @@ function sessionCheckModel(ctx) {
17686
17534
  return { text: answerText(message), message };
17687
17535
  };
17688
17536
  }
17537
+ function launchSwitchOff(name) {
17538
+ const value = process.env[name]?.trim().toLowerCase();
17539
+ return value === "off" || value === "0" || value === "false";
17540
+ }
17689
17541
 
17690
17542
  // src/sessions/goalLoop.ts
17691
17543
  var GOAL_EXTENSION = "goal";
@@ -17813,6 +17665,7 @@ function goalNotice(event) {
17813
17665
  }
17814
17666
 
17815
17667
  // src/sessions/workSlice.ts
17668
+ var WORK_SLICE_EXTENSION = "work-slice";
17816
17669
  var DEFAULT_SLICE_POLICY = {
17817
17670
  enabled: true,
17818
17671
  maxToolCalls: 320,
@@ -18056,6 +17909,74 @@ async function withCommandBudget(run, abort, seconds) {
18056
17909
  }
18057
17910
  }
18058
17911
 
17912
+ // src/sessions/compactionCheckpoint.ts
17913
+ var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
17914
+ function validatedTodo(value) {
17915
+ if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS)
17916
+ return null;
17917
+ const items = [];
17918
+ for (const valueItem of value) {
17919
+ const item = valueItem;
17920
+ const parsed = todoItemSchema.safeParse({
17921
+ text: typeof item?.text === "string" ? item.text.trim() : item?.text,
17922
+ status: item?.status
17923
+ });
17924
+ if (!parsed.success)
17925
+ return null;
17926
+ items.push(parsed.data);
17927
+ }
17928
+ return items;
17929
+ }
17930
+ function compactionCheckpoint(options = {}) {
17931
+ return (pi) => {
17932
+ let goal = "";
17933
+ let todo = validatedTodo(options.initialTodo ?? []) ?? [];
17934
+ let baseline;
17935
+ let checkpoint;
17936
+ const reset = (prompt = "") => {
17937
+ goal = prompt;
17938
+ todo = validatedTodo(options.initialTodo ?? []) ?? [];
17939
+ baseline = undefined;
17940
+ checkpoint = undefined;
17941
+ };
17942
+ pi.on("before_agent_start", (event) => {
17943
+ if (event.messageType !== "user")
17944
+ return;
17945
+ reset(event.prompt);
17946
+ });
17947
+ pi.on("tool_result", (event) => {
17948
+ if (event.toolName !== TODO_WRITE || event.isError)
17949
+ return;
17950
+ const items = validatedTodo(event.input.items);
17951
+ if (items)
17952
+ todo = items;
17953
+ });
17954
+ pi.on("context", (event) => {
17955
+ const index = event.messages.findLastIndex((message) => message.role === "compactionSummary");
17956
+ const summary = index >= 0 ? event.messages[index] : undefined;
17957
+ const key = summary ? `${summary.timestamp}:${summary.tokensBefore}` : null;
17958
+ if (!baseline) {
17959
+ baseline = { key };
17960
+ return;
17961
+ }
17962
+ if (!key || key === baseline.key)
17963
+ return;
17964
+ if (checkpoint?.key !== key) {
17965
+ const content = renderCompactionCheckpoint({ goal, todo, planInstructions: options.planInstructions?.() ?? null });
17966
+ checkpoint = {
17967
+ key,
17968
+ message: content ? { role: "custom", customType: COMPACTION_CHECKPOINT_EXTENSION, content, display: false, timestamp: Date.now() } : null
17969
+ };
17970
+ }
17971
+ if (!checkpoint.message)
17972
+ return;
17973
+ return { messages: [...event.messages.slice(0, index + 1), checkpoint.message, ...event.messages.slice(index + 1)] };
17974
+ });
17975
+ pi.on("agent_settled", () => reset());
17976
+ pi.on("session_tree", () => reset());
17977
+ };
17978
+ }
17979
+
18059
17980
  // src/world/images.ts
18060
17981
  var KEEP_IMAGES = 6;
18061
17982
  var PRUNE_STEP = 4;
@@ -19025,7 +18946,6 @@ async function piExtension_default(pi, planRun, getSession, goal) {
19025
18946
  console.error(`${error instanceof Error ? error.message : String(error)} Using the default work slices.`);
19026
18947
  }
19027
18948
  const sliceState = createSliceState(slicePolicy);
19028
- const gateState = createStopGateState();
19029
18949
  const recordCheckUsage = (raw) => {
19030
18950
  const message = raw;
19031
18951
  const reported = message.usage;
@@ -19305,18 +19225,6 @@ async function piExtension_default(pi, planRun, getSession, goal) {
19305
19225
  continue: true
19306
19226
  };
19307
19227
  });
19308
- if (!launchSwitchOff("XINGULAR_STOP_GATE"))
19309
- stopGate({
19310
- state: gateState,
19311
- active: () => !planRun?.active,
19312
- sliceEnded: () => sliceState.endedBy !== null,
19313
- failed: () => Boolean(failure),
19314
- onEvent: (event) => {
19315
- terminalCall("notice", { kind: "notify", level: "info", extension: STOP_GATE_EXTENSION, text: stopGateNotice(event) }).catch(() => {
19316
- return;
19317
- });
19318
- }
19319
- })(pi);
19320
19228
  if (goal) {
19321
19229
  for (const spec of goalTools(() => goal))
19322
19230
  pi.registerTool({
@@ -19375,13 +19283,13 @@ async function piExtension_default(pi, planRun, getSession, goal) {
19375
19283
  failed: () => Boolean(failure),
19376
19284
  blocked: () => goal?.get()?.status === "blocked",
19377
19285
  goalActive: () => goal?.active() === true,
19378
- todo: () => gateState.todo,
19379
- next: () => handoffFields(gateState, goal?.get() ?? null).next,
19286
+ todo: () => lastTodo,
19287
+ next: () => handoffFields(lastTodo, goal?.get() ?? null).next,
19380
19288
  onContinue: (ended) => {
19381
- terminalCall("checkpoint", { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: ended.slice, reason: "budget" }).catch(() => {
19289
+ terminalCall("checkpoint", { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: ended.slice, reason: "budget" }).catch(() => {
19382
19290
  return;
19383
19291
  });
19384
- terminalCall("notice", { kind: "notify", level: "info", extension: STOP_GATE_EXTENSION, text: `Work slice ${ended.slice} reached ${ended.limit ?? "its limit"}; continuing with slice ${ended.slice + 1} of up to ${slicePolicy.maxSlices}.` }).catch(() => {
19292
+ terminalCall("notice", { kind: "notify", level: "info", extension: WORK_SLICE_EXTENSION, text: `Work slice ${ended.slice} reached ${ended.limit ?? "its limit"}; continuing with slice ${ended.slice + 1} of up to ${slicePolicy.maxSlices}.` }).catch(() => {
19385
19293
  return;
19386
19294
  });
19387
19295
  }
@@ -19585,13 +19493,13 @@ async function piExtension_default(pi, planRun, getSession, goal) {
19585
19493
  }
19586
19494
  subagentParent = false;
19587
19495
  const aborted = event.aborted || messageAborted;
19588
- const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ? stopOutcome(gateState, goal?.get() ?? null) : null;
19496
+ const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ? goalStopOutcome(lastTodo, goal?.get() ?? null) : null;
19589
19497
  if (stop) {
19590
- await terminalCall("checkpoint", { summary: stopSummary(stop), ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: stopCheckpointReason(stop) }).catch(() => {
19498
+ await terminalCall("checkpoint", { summary: stopSummary(stop), ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: stopCheckpointReason(stop) }).catch(() => {
19591
19499
  return;
19592
19500
  });
19593
19501
  } else if (sliceState.endedBy && !failure && !aborted) {
19594
- await terminalCall("checkpoint", { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(gateState, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: sliceState.endedBy }).catch(() => {
19502
+ await terminalCall("checkpoint", { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: sliceState.endedBy }).catch(() => {
19595
19503
  return;
19596
19504
  });
19597
19505
  }