u-foo 3.0.7 → 3.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.7",
3
+ "version": "3.0.8",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -27,6 +27,7 @@ function emptyExecutionState() {
27
27
  pendingUserPrompts: [],
28
28
  planGraph: require("./planGraphService").emptyPlanGraphState(),
29
29
  graphs: {},
30
+ archivedPlans: [],
30
31
  taskRuns: require("../runtime/taskRun").emptyTaskRunStore(),
31
32
  agentMailbox: require("../runtime/loopMailbox").emptyMailbox(),
32
33
  taskMailboxes: {},
@@ -48,6 +48,102 @@ function cloneJson(value) {
48
48
  }
49
49
  }
50
50
 
51
+ const MAX_ARCHIVED_PLANS = 5;
52
+
53
+ /**
54
+ * Snapshot a finished/replaced plan out of the live primary slot so the next
55
+ * create starts clean instead of overlaying the previous graph.
56
+ */
57
+ function archivePlanGraph(executionState = null, planGraph = null, {
58
+ reason = "",
59
+ } = {}) {
60
+ if (!executionState || typeof executionState !== "object") return null;
61
+ const live = planGraph && typeof planGraph === "object" ? planGraph : null;
62
+ const graphId = String((live && live.graphId) || "").trim();
63
+ if (!live || !graphId) return null;
64
+
65
+ if (!Array.isArray(executionState.archivedPlans)) {
66
+ executionState.archivedPlans = [];
67
+ }
68
+ const archived = {
69
+ ...cloneJson(live),
70
+ archivedAt: new Date().toISOString(),
71
+ archiveReason: String(reason || "").trim() || "archived",
72
+ };
73
+ executionState.archivedPlans.push(archived);
74
+ if (executionState.archivedPlans.length > MAX_ARCHIVED_PLANS) {
75
+ executionState.archivedPlans = executionState.archivedPlans.slice(-MAX_ARCHIVED_PLANS);
76
+ }
77
+ if (executionState.graphs && typeof executionState.graphs === "object") {
78
+ delete executionState.graphs[graphId];
79
+ }
80
+ return archived;
81
+ }
82
+
83
+ function buildPlanCompletionSummary(planGraph = null, advance = null) {
84
+ const pg = planGraph && typeof planGraph === "object" ? planGraph : {};
85
+ const nodes = Array.isArray(pg.nodes) ? pg.nodes : [];
86
+ const tasks = nodes.filter((node) => node && (node.type === "task" || !node.generated));
87
+ const succeeded = tasks.filter((n) => n.status === "succeeded").length;
88
+ const failed = tasks.filter((n) => (
89
+ n.status === "failed" || n.status === "blocked" || n.status === "cancelled"
90
+ )).length;
91
+ const lines = [
92
+ `Plan completed${pg.graphId ? ` (${pg.graphId})` : ""}.`,
93
+ ];
94
+ if (pg.objective) lines.push(`Objective: ${String(pg.objective).slice(0, 240)}`);
95
+ lines.push(`Tasks: ${succeeded} succeeded, ${failed} failed/blocked, ${tasks.length} total.`);
96
+ for (const node of tasks.slice(0, 12)) {
97
+ const bit = (node.result && node.result.summary)
98
+ || node.error
99
+ || node.status
100
+ || "";
101
+ lines.push(
102
+ `- ${node.id} [${node.status || "pending"}]`
103
+ + (bit ? `: ${String(bit).slice(0, 160)}` : ""),
104
+ );
105
+ }
106
+ const executed = advance && Array.isArray(advance.executedNodes) ? advance.executedNodes : [];
107
+ if (executed.length > 0 && tasks.length === 0) {
108
+ for (const entry of executed.slice(0, 8)) {
109
+ lines.push(`- ${entry.id}: ${entry.summary || entry.status || "done"}`);
110
+ }
111
+ }
112
+ lines.push("Summarize the outcome for the user. The active plan has been cleared; use plan_graph create for a new objective.");
113
+ return lines.join("\n");
114
+ }
115
+
116
+ /**
117
+ * On graph_terminal for the primary Agent Loop plan: archive, clear primary
118
+ * slot, and exit auto Plan Mode. Child TaskLoop graphs stay in graphs[].
119
+ */
120
+ function finalizeTerminalPrimaryPlan(executionState = null, planGraph = null, advance = null) {
121
+ const pg = planGraph && typeof planGraph === "object" ? planGraph : null;
122
+ if (!pg || !pg.graphId) return null;
123
+ if (!advance || String(advance.yieldReason || "") !== "graph_terminal") return null;
124
+
125
+ const completionSummary = buildPlanCompletionSummary(pg, advance);
126
+ const archived = archivePlanGraph(executionState, pg, { reason: "graph_terminal" });
127
+ executionState.planGraph = emptyPlanGraphState();
128
+ executionState.mode = "single_action";
129
+
130
+ try {
131
+ const { getPlanModeSource, setPlanMode } = require("./planMode");
132
+ if (getPlanModeSource(executionState) === "auto") {
133
+ setPlanMode(executionState, false, { reason: "plan completed" });
134
+ }
135
+ } catch {
136
+ // planMode is optional for pure unit callers
137
+ }
138
+
139
+ return {
140
+ planCompleted: true,
141
+ archivedGraphId: (archived && archived.graphId) || pg.graphId,
142
+ completionSummary,
143
+ planMode: Boolean(executionState.planMode),
144
+ };
145
+ }
146
+
51
147
  function ensurePlanGraphState(executionState = null) {
52
148
  const state = executionState && typeof executionState === "object"
53
149
  ? executionState
@@ -614,42 +710,73 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
614
710
  }
615
711
  if (advanced.advance) advance = advanced.advance;
616
712
  }
617
- const live = executionState.planGraph;
618
- const { readyNodes, waitingNodes } = summarizeReadyWaiting(live.nodes);
713
+ const completedGraph = executionState.planGraph;
714
+ const terminal = selected.isPrimary
715
+ ? finalizeTerminalPrimaryPlan(executionState, completedGraph, advance)
716
+ : null;
717
+ const live = terminal
718
+ ? completedGraph
719
+ : executionState.planGraph;
720
+ const { readyNodes, waitingNodes } = summarizeReadyWaiting(
721
+ terminal ? [] : (live.nodes || []),
722
+ );
619
723
  const payload = controlResult.ok
620
724
  ? accepted({
621
- graphId: live.graphId || "",
622
- commandRevision: Number(live.specRevision) || 0,
623
- stateRevision: Number(live.stateRevision) || 0,
624
- revision: Number(live.specRevision) || 0,
725
+ graphId: terminal ? "" : (live.graphId || ""),
726
+ commandRevision: terminal ? 0 : (Number(live.specRevision) || 0),
727
+ stateRevision: terminal ? 0 : (Number(live.stateRevision) || 0),
728
+ revision: terminal ? 0 : (Number(live.specRevision) || 0),
625
729
  control: controlResult,
626
- summary: advance.waitingFor
627
- ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
628
- : (advance.yieldReason || "control actions applied"),
629
- changes: { nodesAdded: [], nodesUpdated: [] },
730
+ summary: terminal
731
+ ? terminal.completionSummary
732
+ : (advance.waitingFor
733
+ ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
734
+ : (advance.yieldReason || "control actions applied")),
735
+ changes: {
736
+ nodesAdded: [],
737
+ nodesUpdated: [],
738
+ ...(terminal ? { archivedGraphId: terminal.archivedGraphId } : {}),
739
+ },
630
740
  nodesAdded: [],
631
741
  nodesUpdated: [],
632
742
  readyNodes,
633
743
  waitingNodes,
634
- waitingFor: live.waitingFor || null,
744
+ waitingFor: terminal ? null : (live.waitingFor || null),
635
745
  advance,
636
- planView: projectPlanView(live),
746
+ planView: terminal ? [] : projectPlanView(live),
637
747
  validationWarnings: [],
748
+ ...(terminal ? {
749
+ planCompleted: true,
750
+ archivedGraphId: terminal.archivedGraphId,
751
+ completionSummary: terminal.completionSummary,
752
+ } : {}),
638
753
  })
639
754
  : rejected(controlResult.errors || [{
640
755
  code: "CONTROL_REJECTED",
641
756
  message: "one or more control actions rejected",
642
757
  }], { control: controlResult });
643
- if (commandId && payload.status === "accepted") {
758
+ if (commandId && payload.status === "accepted" && !terminal) {
644
759
  cacheCommand(executionState.planGraph, commandId, payload);
645
760
  }
761
+ restorePrimaryGraph();
646
762
  return { ...payload, executionState, modelPayload: payload, ok: payload.status === "accepted" };
647
763
  }
648
764
 
649
765
  if (operation === "cancel_graph" || operation === "clear") {
650
766
  const previousId = planGraph.graphId || "";
767
+ if (previousId) {
768
+ archivePlanGraph(executionState, planGraph, { reason: "cancel_graph" });
769
+ }
651
770
  executionState.planGraph = emptyPlanGraphState();
652
771
  executionState.mode = "single_action";
772
+ try {
773
+ const { getPlanModeSource, setPlanMode } = require("./planMode");
774
+ if (getPlanModeSource(executionState) === "auto") {
775
+ setPlanMode(executionState, false, { reason: "plan cancelled" });
776
+ }
777
+ } catch {
778
+ // ignore
779
+ }
653
780
  const payload = accepted({
654
781
  graphId: "",
655
782
  commandRevision: 0,
@@ -662,6 +789,9 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
662
789
  waitingNodes: [],
663
790
  advance: { status: "completed", yieldReason: "cancelled", executedNodes: [], failedNodes: [] },
664
791
  validationWarnings: [],
792
+ summary: previousId
793
+ ? `Plan ${previousId} cancelled and cleared.`
794
+ : "No active plan to cancel.",
665
795
  });
666
796
  restorePrimaryGraph();
667
797
  return { ...payload, executionState, modelPayload: payload };
@@ -697,15 +827,22 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
697
827
  return rejectWorking(payload);
698
828
  }
699
829
 
700
- const beforeIds = new Set(listNodeIds(planGraph.nodes));
830
+ const beforeIds = operation === "create"
831
+ ? new Set()
832
+ : new Set(listNodeIds(planGraph.nodes));
701
833
  let nextPlan = {
702
834
  id: planGraph.graphId || createPlanId("plan"),
703
835
  objective: planGraph.objective || "",
704
836
  nodes: snapshotNodes(planGraph),
705
837
  };
706
838
  let nodesUpdated = [];
839
+ let replacedGraphId = "";
707
840
 
708
841
  if (operation === "create") {
842
+ if (selected.isPrimary && planGraph.graphId) {
843
+ replacedGraphId = String(planGraph.graphId);
844
+ archivePlanGraph(executionState, planGraph, { reason: "replaced_by_create" });
845
+ }
709
846
  const graphSource = command.graph || {};
710
847
  if (Array.isArray(graphSource.nodes)) {
711
848
  nextPlan = normalizePlanGraph({
@@ -717,6 +854,11 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
717
854
  nextPlan.nodes = stripModelStatuses(nextPlan.nodes);
718
855
  }
719
856
  if (!nextPlan.id) nextPlan.id = createPlanId("plan");
857
+ // Prefer a fresh id when replacing a live plan so the new graph does not
858
+ // collide with the archived one in graphs[].
859
+ if (replacedGraphId && nextPlan.id === replacedGraphId) {
860
+ nextPlan.id = createPlanId("plan");
861
+ }
720
862
  nodesUpdated = listNodeIds(nextPlan.nodes);
721
863
  // Parent graphs are owned by the agent loop.
722
864
  const { agentLoopOwner } = require("../runtime/graphOwner");
@@ -798,9 +940,10 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
798
940
  }
799
941
 
800
942
  // Prefer the patched node list (includes aggregate expand status).
943
+ // create starts clean — do not inherit statuses from a replaced graph.
801
944
  const preferredNodes = applyStatusesFromStore(
802
945
  (Array.isArray(nextPlan.nodes) ? nextPlan.nodes : []).map((node) => normalizePlanNode(node, node.id)),
803
- planGraph.nodes,
946
+ operation === "create" ? [] : planGraph.nodes,
804
947
  { preferSourceStatus: operation === "patch" },
805
948
  );
806
949
 
@@ -831,28 +974,53 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
831
974
  }
832
975
  }
833
976
 
834
- const specRevision = (Number(planGraph.specRevision) || 0) + 1;
835
- executionState.planGraph = {
836
- ...planGraph,
837
- graphId: preferredCompile.planId || nextPlan.id,
838
- specRevision,
839
- stateRevision: Number(planGraph.stateRevision) || 0,
840
- revision: specRevision,
841
- objective: preferredCompile.objective || nextPlan.objective || "",
842
- failurePolicy: preferredCompile.failurePolicy
843
- || nextPlan.failurePolicy
844
- || planGraph.failurePolicy
845
- || "continue_independent",
846
- nodes: mergedNodes,
847
- outputs: operation === "create" ? {} : { ...(planGraph.outputs || {}) },
848
- waitingFor: null,
849
- lastStoppedAt: "",
850
- lastYieldReason: "",
851
- commandLog: planGraph.commandLog || {},
852
- owner: nextPlan.owner || planGraph.owner || null,
853
- parentGraphId: planGraph.parentGraphId || "",
854
- parentNodeId: planGraph.parentNodeId || "",
855
- };
977
+ const specRevision = operation === "create"
978
+ ? 1
979
+ : (Number(planGraph.specRevision) || 0) + 1;
980
+ if (operation === "create") {
981
+ executionState.planGraph = {
982
+ ...emptyPlanGraphState(),
983
+ graphId: preferredCompile.planId || nextPlan.id,
984
+ specRevision,
985
+ stateRevision: 0,
986
+ revision: specRevision,
987
+ objective: preferredCompile.objective || nextPlan.objective || "",
988
+ failurePolicy: preferredCompile.failurePolicy
989
+ || nextPlan.failurePolicy
990
+ || "continue_independent",
991
+ nodes: mergedNodes,
992
+ outputs: {},
993
+ waitingFor: null,
994
+ lastStoppedAt: "",
995
+ lastYieldReason: "",
996
+ commandLog: {},
997
+ owner: nextPlan.owner || null,
998
+ parentGraphId: "",
999
+ parentNodeId: "",
1000
+ };
1001
+ } else {
1002
+ executionState.planGraph = {
1003
+ ...planGraph,
1004
+ graphId: preferredCompile.planId || nextPlan.id,
1005
+ specRevision,
1006
+ stateRevision: Number(planGraph.stateRevision) || 0,
1007
+ revision: specRevision,
1008
+ objective: preferredCompile.objective || nextPlan.objective || "",
1009
+ failurePolicy: preferredCompile.failurePolicy
1010
+ || nextPlan.failurePolicy
1011
+ || planGraph.failurePolicy
1012
+ || "continue_independent",
1013
+ nodes: mergedNodes,
1014
+ outputs: { ...(planGraph.outputs || {}) },
1015
+ waitingFor: null,
1016
+ lastStoppedAt: "",
1017
+ lastYieldReason: "",
1018
+ commandLog: planGraph.commandLog || {},
1019
+ owner: nextPlan.owner || planGraph.owner || null,
1020
+ parentGraphId: planGraph.parentGraphId || "",
1021
+ parentNodeId: planGraph.parentNodeId || "",
1022
+ };
1023
+ }
856
1024
  if (!executionState.graphs || typeof executionState.graphs !== "object") {
857
1025
  executionState.graphs = {};
858
1026
  }
@@ -890,45 +1058,80 @@ function runPlanGraphCommand(commandInput = {}, options = {}) {
890
1058
  ...advanced.planGraph,
891
1059
  specRevision,
892
1060
  revision: specRevision,
893
- commandLog: planGraph.commandLog || {},
894
- owner: advanced.planGraph.owner || planGraph.owner || null,
895
- parentGraphId: advanced.planGraph.parentGraphId || planGraph.parentGraphId || "",
896
- parentNodeId: advanced.planGraph.parentNodeId || planGraph.parentNodeId || "",
1061
+ commandLog: operation === "create" ? {} : (planGraph.commandLog || {}),
1062
+ owner: advanced.planGraph.owner || (operation === "create" ? nextPlan.owner : planGraph.owner) || null,
1063
+ parentGraphId: operation === "create"
1064
+ ? ""
1065
+ : (advanced.planGraph.parentGraphId || planGraph.parentGraphId || ""),
1066
+ parentNodeId: operation === "create"
1067
+ ? ""
1068
+ : (advanced.planGraph.parentNodeId || planGraph.parentNodeId || ""),
897
1069
  };
898
1070
  }
899
1071
  if (advanced.advance) advance = advanced.advance;
900
1072
  }
901
1073
 
902
- const live = executionState.planGraph;
903
- const { readyNodes, waitingNodes } = summarizeReadyWaiting(live.nodes);
1074
+ const completedGraph = executionState.planGraph;
1075
+ const terminal = selected.isPrimary
1076
+ ? finalizeTerminalPrimaryPlan(executionState, completedGraph, advance)
1077
+ : null;
1078
+ if (!terminal && !selected.isPrimary && advance.yieldReason === "graph_terminal") {
1079
+ // Child TaskLoop graph finished — keep snapshot; parent stays active.
1080
+ const childId = String(completedGraph.graphId || "").trim();
1081
+ if (childId) {
1082
+ executionState.graphs[childId] = {
1083
+ ...completedGraph,
1084
+ completedAt: new Date().toISOString(),
1085
+ lastYieldReason: "graph_terminal",
1086
+ };
1087
+ }
1088
+ }
1089
+
1090
+ const live = terminal ? completedGraph : executionState.planGraph;
1091
+ const { readyNodes, waitingNodes } = summarizeReadyWaiting(
1092
+ terminal ? [] : (live.nodes || []),
1093
+ );
904
1094
  const payload = accepted({
905
- graphId: live.graphId,
906
- commandRevision: live.specRevision,
907
- stateRevision: live.stateRevision,
908
- revision: live.specRevision,
1095
+ graphId: terminal ? "" : live.graphId,
1096
+ commandRevision: terminal ? 0 : live.specRevision,
1097
+ stateRevision: terminal ? 0 : live.stateRevision,
1098
+ revision: terminal ? 0 : live.specRevision,
909
1099
  changes: {
910
1100
  nodesAdded,
911
1101
  nodesUpdated: Array.from(new Set(nodesUpdated)),
1102
+ ...(replacedGraphId ? { replacedGraphId } : {}),
1103
+ ...(terminal ? { archivedGraphId: terminal.archivedGraphId } : {}),
912
1104
  },
913
1105
  nodesAdded,
914
1106
  nodesUpdated: Array.from(new Set(nodesUpdated)),
915
1107
  readyNodes,
916
1108
  waitingNodes,
917
- waitingFor: live.waitingFor || null,
918
- stoppedAt: live.lastStoppedAt || "",
1109
+ waitingFor: terminal ? null : (live.waitingFor || null),
1110
+ stoppedAt: terminal ? "graph_terminal" : (live.lastStoppedAt || ""),
919
1111
  advance,
920
- planView: projectPlanView(live),
1112
+ planView: terminal ? [] : projectPlanView(live),
921
1113
  validationWarnings: preferredCompile.warnings || [],
922
- summary: advance.waitingFor
923
- ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
924
- : (advance.yieldReason || "graph updated"),
1114
+ summary: terminal
1115
+ ? terminal.completionSummary
1116
+ : (advance.waitingFor
1117
+ ? `waiting on ${advance.waitingFor.type}:${advance.waitingFor.id || ""}`
1118
+ : (advance.yieldReason || "graph updated")),
1119
+ ...(replacedGraphId ? { replacedGraphId } : {}),
1120
+ ...(terminal ? {
1121
+ planCompleted: true,
1122
+ archivedGraphId: terminal.archivedGraphId,
1123
+ completionSummary: terminal.completionSummary,
1124
+ } : {}),
925
1125
  });
926
1126
 
927
- cacheCommand(executionState.planGraph, commandId, payload);
1127
+ if (!terminal) {
1128
+ cacheCommand(executionState.planGraph, commandId, payload);
1129
+ }
928
1130
 
929
1131
  // Agent Loop create enables Plan Mode; TaskLoop child graphs must not.
1132
+ // Skip when create immediately ran to terminal (already exited Plan Mode).
930
1133
  let planModeEntered = null;
931
- if (operation === "create" && payload.status === "accepted") {
1134
+ if (operation === "create" && payload.status === "accepted" && !terminal) {
932
1135
  const ownerKind = String(
933
1136
  (executionState.planGraph && executionState.planGraph.owner && executionState.planGraph.owner.kind)
934
1137
  || "agent_loop",
@@ -968,6 +1171,9 @@ function activePlanRequiresExpansion(planGraph = {}) {
968
1171
  module.exports = {
969
1172
  emptyPlanGraphState,
970
1173
  ensurePlanGraphState,
1174
+ archivePlanGraph,
1175
+ buildPlanCompletionSummary,
1176
+ finalizeTerminalPrimaryPlan,
971
1177
  normalizePlanGraphCommand,
972
1178
  normalizeExpandOp,
973
1179
  runPlanGraphCommand,
@@ -54,6 +54,7 @@ function buildImmutablePrefix() {
54
54
  "- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Cancel with task_run (standalone) or plan_graph operation=cancel_graph / control.cancel_task (graph-bound).",
55
55
  "- When the user enables Plan Mode and no active graph exists, create a plan_graph before performing side effects.",
56
56
  "- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
57
+ "- When the last plan node finishes (graph_terminal), Runtime archives and clears the active plan, exits auto Plan Mode, and returns a completion summary — narrate that summary to the user. Start a new objective with plan_graph create (create replaces any leftover graph); do not patch a finished plan for a new goal.",
57
58
  "- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
58
59
  "- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
59
60
  "- TaskLoop start returns childGraphId. While that TaskRun is waiting_model on child root, patch with graphId=<childGraphId> and expand_node nodeId=root (add tool children). Do not ask the user to /plan off.",
@@ -131,7 +131,8 @@ function shouldAutoContinuePlan(executionState = null) {
131
131
 
132
132
  /**
133
133
  * Internal reminder injected by runtime when the model ends a turn while the
134
- * plan is still waiting on a task. Same shape as user nudges.
134
+ * plan is still waiting on a task. Must NOT reuse the User reminder label —
135
+ * that is reserved for real mid-run user text and pollutes transcript/TUI.
135
136
  */
136
137
  function buildPlanAutoContinueReminder(executionState = null) {
137
138
  const waiting = executionState
@@ -140,14 +141,17 @@ function buildPlanAutoContinueReminder(executionState = null) {
140
141
  ? executionState.planGraph.waitingFor
141
142
  : null;
142
143
  if (!waiting || !waiting.id) return "";
143
- return formatUserReminderMessage(
144
- [
145
- "Continue the active plan. Serve the waiting task now via plan_graph "
146
- + "(expand_node, control.start_task, or control.complete_task as appropriate). "
147
- + "Do not end the turn with text only while this node is waiting.",
148
- ],
149
- { waitingFor: waiting },
150
- );
144
+ const label = waiting.title || waiting.objective || waiting.reason || waiting.id;
145
+ const lines = [
146
+ "Runtime wake (not a user message): active plan is still waiting.",
147
+ "Continue serving it now via plan_graph "
148
+ + "(expand_node, control.start_task, or control.complete_task as appropriate). "
149
+ + "Do not end the turn with text only while this node is waiting.",
150
+ `Prefer serving the current waiting ${waiting.type || "node"}: ${waiting.id}`
151
+ + (label && label !== waiting.id ? ` — ${label}` : "")
152
+ + ".",
153
+ ];
154
+ return lines.join("\n");
151
155
  }
152
156
 
153
157
  module.exports = {
@@ -1131,7 +1131,19 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
1131
1131
 
1132
1132
  // While a native task is in flight, queue an additional user reminder
1133
1133
  // for the next LLM turn instead of starting a second NL task.
1134
+ // Slash commands (/model, /plan, …) must still run immediately — same
1135
+ // rule as the REPL path — otherwise they pollute the nudge queue.
1134
1136
  if (pendingTaskRef.current) {
1137
+ if (/^\//.test(trimmed)) {
1138
+ runChainRef.current = runChainRef.current
1139
+ .then(() => executeLine(modelText, {
1140
+ modelText,
1141
+ logText,
1142
+ preserveNewlines: attachments.length > 0,
1143
+ }))
1144
+ .catch((err) => appendLogText(`Error: ${err && err.message ? err.message : err}`, "error"));
1145
+ return;
1146
+ }
1135
1147
  const { enqueueUserPrompt } = require("../../code/context/userNudge");
1136
1148
  const { emptyExecutionState } = require("../../code/context/executionSegment");
1137
1149
  if (!props.state || typeof props.state !== "object") {