u-foo 2.5.15 → 3.0.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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/code/agent.js +333 -243
  3. package/src/code/commands.js +16 -0
  4. package/src/code/context/assembler.js +18 -13
  5. package/src/code/context/executionSegment.js +97 -119
  6. package/src/code/context/index.js +11 -1
  7. package/src/code/context/planGraph.js +1410 -0
  8. package/src/code/context/planGraphService.js +857 -0
  9. package/src/code/context/planMode.js +398 -0
  10. package/src/code/context/planProjection.js +432 -0
  11. package/src/code/context/promptLayers.js +21 -5
  12. package/src/code/context/stateCommit.js +2 -0
  13. package/src/code/context/toolRuntime.js +172 -0
  14. package/src/code/context/userInteraction.js +457 -0
  15. package/src/code/context/userNudge.js +116 -0
  16. package/src/code/dispatch.js +17 -1
  17. package/src/code/index.js +2 -0
  18. package/src/code/nativeRunner.js +518 -37
  19. package/src/code/repl.js +160 -18
  20. package/src/code/runtime/agentWakeup.js +58 -0
  21. package/src/code/runtime/graphOwner.js +41 -0
  22. package/src/code/runtime/graphYieldRouter.js +42 -0
  23. package/src/code/runtime/index.js +15 -0
  24. package/src/code/runtime/loopMailbox.js +124 -0
  25. package/src/code/runtime/runtimeEvents.js +39 -0
  26. package/src/code/runtime/taskControl.js +565 -0
  27. package/src/code/runtime/taskFocus.js +165 -0
  28. package/src/code/runtime/taskLoop.js +383 -0
  29. package/src/code/runtime/taskRun.js +187 -0
  30. package/src/code/runtime/toolProvenance.js +70 -0
  31. package/src/code/runtime/workspaceLease.js +208 -0
  32. package/src/code/sessionStore.js +0 -10
  33. package/src/code/skills/injection.js +1 -0
  34. package/src/code/taskDecomposer.js +32 -8
  35. package/src/code/tools/askUser.js +11 -0
  36. package/src/code/tools/planGraph.js +29 -0
  37. package/src/ui/format/index.js +25 -1
  38. package/src/ui/format/markdownRenderer.js +224 -2
  39. package/src/ui/ink/UcodeApp.js +285 -22
  40. package/src/code/context/featureFlag.js +0 -13
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+
3
+ const { randomUUID } = require("crypto");
4
+
5
+ /**
6
+ * TaskRun registry — parent Task node identity vs runnable attempt.
7
+ */
8
+
9
+ const TASK_RUN_STATUSES = Object.freeze([
10
+ "queued",
11
+ "running",
12
+ "cancelling",
13
+ "succeeded",
14
+ "failed",
15
+ "cancelled",
16
+ ]);
17
+
18
+ const TASK_RUN_PHASES = Object.freeze([
19
+ "initializing",
20
+ "planning",
21
+ "waiting_model",
22
+ "executing_tools",
23
+ "finalizing",
24
+ ]);
25
+
26
+ const TERMINAL_TASK_RUN = new Set(["succeeded", "failed", "cancelled"]);
27
+
28
+ function createTaskRunId() {
29
+ return `trun_${Date.now().toString(36)}_${randomUUID().slice(0, 6)}`;
30
+ }
31
+
32
+ function emptyTaskRunStore() {
33
+ return {
34
+ byId: {},
35
+ commandLog: {},
36
+ };
37
+ }
38
+
39
+ function ensureTaskRunStore(executionState = null) {
40
+ const state = executionState && typeof executionState === "object" ? executionState : {};
41
+ if (!state.taskRuns || typeof state.taskRuns !== "object") {
42
+ state.taskRuns = emptyTaskRunStore();
43
+ }
44
+ if (!state.taskRuns.byId || typeof state.taskRuns.byId !== "object") {
45
+ state.taskRuns.byId = {};
46
+ }
47
+ if (!state.taskRuns.commandLog || typeof state.taskRuns.commandLog !== "object") {
48
+ state.taskRuns.commandLog = {};
49
+ }
50
+ return state.taskRuns;
51
+ }
52
+
53
+ function createTaskRun({
54
+ parentGraphId = "",
55
+ parentNodeId = "",
56
+ childGraphId = "",
57
+ attempt = 1,
58
+ } = {}) {
59
+ const now = new Date().toISOString();
60
+ return {
61
+ id: createTaskRunId(),
62
+ parentGraphId: String(parentGraphId || "").trim(),
63
+ parentNodeId: String(parentNodeId || "").trim(),
64
+ childGraphId: String(childGraphId || "").trim(),
65
+ status: "queued",
66
+ phase: "initializing",
67
+ attempt: Number.isFinite(attempt) ? Math.max(1, Math.floor(attempt)) : 1,
68
+ ignoreUserPrompts: true,
69
+ cancelRequested: false,
70
+ result: null,
71
+ error: null,
72
+ changedFiles: [],
73
+ createdAt: now,
74
+ startedAt: "",
75
+ completedAt: "",
76
+ };
77
+ }
78
+
79
+ function getTaskRun(executionState = null, taskRunId = "") {
80
+ const store = ensureTaskRunStore(executionState);
81
+ const id = String(taskRunId || "").trim();
82
+ return id && store.byId[id] ? store.byId[id] : null;
83
+ }
84
+
85
+ function putTaskRun(executionState = null, taskRun = null) {
86
+ if (!taskRun || !taskRun.id) return null;
87
+ const store = ensureTaskRunStore(executionState);
88
+ store.byId[taskRun.id] = taskRun;
89
+ return taskRun;
90
+ }
91
+
92
+ function findActiveTaskRunForNode(executionState = null, parentNodeId = "") {
93
+ const store = ensureTaskRunStore(executionState);
94
+ const nodeId = String(parentNodeId || "").trim();
95
+ for (const run of Object.values(store.byId)) {
96
+ if (!run || run.parentNodeId !== nodeId) continue;
97
+ if (run.status === "queued" || run.status === "running" || run.status === "cancelling") {
98
+ return run;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function listActiveWritingTaskRuns(executionState = null) {
105
+ const store = ensureTaskRunStore(executionState);
106
+ return Object.values(store.byId).filter((run) => (
107
+ run
108
+ && (run.status === "queued" || run.status === "running" || run.status === "cancelling")
109
+ ));
110
+ }
111
+
112
+ function isTerminalTaskRun(run = null) {
113
+ return Boolean(run && TERMINAL_TASK_RUN.has(String(run.status || "")));
114
+ }
115
+
116
+ /**
117
+ * Compare-and-set status transition. Returns { ok, run }.
118
+ */
119
+ function casTaskRunStatus(executionState = null, taskRunId = "", {
120
+ expectedStatus = "",
121
+ nextStatus = "",
122
+ phase = "",
123
+ result = null,
124
+ error = null,
125
+ changedFiles = null,
126
+ } = {}) {
127
+ const run = getTaskRun(executionState, taskRunId);
128
+ if (!run) return { ok: false, code: "TASK_RUN_NOT_FOUND", run: null };
129
+ const expected = String(expectedStatus || "").trim();
130
+ if (expected && run.status !== expected) {
131
+ return {
132
+ ok: false,
133
+ code: "TASK_STATUS_CAS_FAILED",
134
+ run,
135
+ currentStatus: run.status,
136
+ };
137
+ }
138
+ const next = String(nextStatus || "").trim();
139
+ if (!TASK_RUN_STATUSES.includes(next)) {
140
+ return { ok: false, code: "INVALID_TASK_STATUS", run };
141
+ }
142
+ run.status = next;
143
+ if (phase && TASK_RUN_PHASES.includes(phase)) run.phase = phase;
144
+ if (result !== null) run.result = result;
145
+ if (error !== null) run.error = error;
146
+ if (Array.isArray(changedFiles)) run.changedFiles = changedFiles.map(String);
147
+ if (next === "running" && !run.startedAt) run.startedAt = new Date().toISOString();
148
+ if (TERMINAL_TASK_RUN.has(next)) {
149
+ run.completedAt = new Date().toISOString();
150
+ run.phase = "finalizing";
151
+ }
152
+ if (next === "cancelling") run.cancelRequested = true;
153
+ putTaskRun(executionState, run);
154
+ return { ok: true, run };
155
+ }
156
+
157
+ function cacheControlCommand(executionState = null, commandId = "", payload = {}) {
158
+ const id = String(commandId || "").trim();
159
+ if (!id) return;
160
+ const store = ensureTaskRunStore(executionState);
161
+ store.commandLog[id] = JSON.parse(JSON.stringify(payload));
162
+ }
163
+
164
+ function getCachedControlCommand(executionState = null, commandId = "") {
165
+ const id = String(commandId || "").trim();
166
+ if (!id) return null;
167
+ const store = ensureTaskRunStore(executionState);
168
+ return store.commandLog[id] ? JSON.parse(JSON.stringify(store.commandLog[id])) : null;
169
+ }
170
+
171
+ module.exports = {
172
+ TASK_RUN_STATUSES,
173
+ TASK_RUN_PHASES,
174
+ TERMINAL_TASK_RUN,
175
+ createTaskRunId,
176
+ emptyTaskRunStore,
177
+ ensureTaskRunStore,
178
+ createTaskRun,
179
+ getTaskRun,
180
+ putTaskRun,
181
+ findActiveTaskRunForNode,
182
+ listActiveWritingTaskRuns,
183
+ isTerminalTaskRun,
184
+ casTaskRunStatus,
185
+ cacheControlCommand,
186
+ getCachedControlCommand,
187
+ };
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Per-TaskRun tool provenance for changedFiles (not git diff attribution).
5
+ */
6
+
7
+ function ensureProvenanceStore(executionState = null) {
8
+ const state = executionState && typeof executionState === "object" ? executionState : {};
9
+ if (!state.toolProvenance || typeof state.toolProvenance !== "object") {
10
+ state.toolProvenance = { byTaskRunId: {} };
11
+ }
12
+ if (!state.toolProvenance.byTaskRunId || typeof state.toolProvenance.byTaskRunId !== "object") {
13
+ state.toolProvenance.byTaskRunId = {};
14
+ }
15
+ return state.toolProvenance;
16
+ }
17
+
18
+ function touchedPathsFromTool(tool = "", args = {}) {
19
+ const name = String(tool || "").trim().toLowerCase();
20
+ const paths = [];
21
+ if (name === "write" || name === "edit") {
22
+ const path = String(args && args.path || "").trim();
23
+ if (path) paths.push(path);
24
+ }
25
+ return paths;
26
+ }
27
+
28
+ function recordToolProvenance(executionState = null, {
29
+ taskRunId = "",
30
+ tool = "",
31
+ args = {},
32
+ graphId = "",
33
+ nodeId = "",
34
+ } = {}) {
35
+ const id = String(taskRunId || "").trim();
36
+ if (!id) return [];
37
+ const store = ensureProvenanceStore(executionState);
38
+ if (!store.byTaskRunId[id]) {
39
+ store.byTaskRunId[id] = { paths: [], events: [] };
40
+ }
41
+ const bucket = store.byTaskRunId[id];
42
+ const paths = touchedPathsFromTool(tool, args);
43
+ for (const path of paths) {
44
+ if (!bucket.paths.includes(path)) bucket.paths.push(path);
45
+ }
46
+ bucket.events.push({
47
+ at: new Date().toISOString(),
48
+ tool: String(tool || ""),
49
+ graphId: String(graphId || ""),
50
+ nodeId: String(nodeId || ""),
51
+ paths,
52
+ });
53
+ // Cap event log
54
+ if (bucket.events.length > 200) bucket.events = bucket.events.slice(-200);
55
+ return paths;
56
+ }
57
+
58
+ function getProvenanceChangedFiles(executionState = null, taskRunId = "") {
59
+ const store = ensureProvenanceStore(executionState);
60
+ const id = String(taskRunId || "").trim();
61
+ const bucket = store.byTaskRunId[id];
62
+ return bucket && Array.isArray(bucket.paths) ? bucket.paths.slice() : [];
63
+ }
64
+
65
+ module.exports = {
66
+ ensureProvenanceStore,
67
+ touchedPathsFromTool,
68
+ recordToolProvenance,
69
+ getProvenanceChangedFiles,
70
+ };
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Workspace write lease — up to MAX_CONCURRENT_WRITE_LEASES writing TaskRuns.
5
+ * Agent write/edit/side-effect bash rejected while any Task holds a write lease.
6
+ */
7
+
8
+ const WRITE_TOOLS = new Set(["write", "edit", "bash"]);
9
+ const MAX_CONCURRENT_WRITE_LEASES = 6;
10
+
11
+ function emptyWorkspaceLease() {
12
+ return {
13
+ holders: [], // [{ kind: "task_run", taskRunId, acquiredAt }]
14
+ mode: "write",
15
+ // Legacy single-holder field; migrated in ensureWorkspaceLease.
16
+ holder: null,
17
+ acquiredAt: "",
18
+ };
19
+ }
20
+
21
+ function ensureWorkspaceLease(executionState = null) {
22
+ const state = executionState && typeof executionState === "object" ? executionState : {};
23
+ if (!state.workspaceLease || typeof state.workspaceLease !== "object") {
24
+ state.workspaceLease = emptyWorkspaceLease();
25
+ }
26
+ normalizeHolders(state.workspaceLease);
27
+ return state.workspaceLease;
28
+ }
29
+
30
+ function normalizeHolders(lease = null) {
31
+ const next = lease && typeof lease === "object" ? lease : emptyWorkspaceLease();
32
+ if (!Array.isArray(next.holders)) next.holders = [];
33
+
34
+ // Migrate V1 single-holder shape.
35
+ if (next.holder && typeof next.holder === "object" && next.holder.taskRunId) {
36
+ const id = String(next.holder.taskRunId || "").trim();
37
+ if (id && !next.holders.some((h) => h && h.taskRunId === id)) {
38
+ next.holders.push({
39
+ kind: "task_run",
40
+ taskRunId: id,
41
+ acquiredAt: String(next.acquiredAt || new Date().toISOString()),
42
+ });
43
+ }
44
+ next.holder = null;
45
+ }
46
+
47
+ next.holders = next.holders
48
+ .filter((h) => h && h.kind === "task_run" && String(h.taskRunId || "").trim())
49
+ .map((h) => ({
50
+ kind: "task_run",
51
+ taskRunId: String(h.taskRunId).trim(),
52
+ acquiredAt: String(h.acquiredAt || ""),
53
+ }));
54
+
55
+ // Cap defensive (should not happen if acquire gates correctly).
56
+ if (next.holders.length > MAX_CONCURRENT_WRITE_LEASES) {
57
+ next.holders = next.holders.slice(0, MAX_CONCURRENT_WRITE_LEASES);
58
+ }
59
+ return next.holders;
60
+ }
61
+
62
+ function listWriteLeaseHolders(executionState = null) {
63
+ return normalizeHolders(ensureWorkspaceLease(executionState)).slice();
64
+ }
65
+
66
+ function countWriteLeases(executionState = null) {
67
+ return listWriteLeaseHolders(executionState).length;
68
+ }
69
+
70
+ function findWriteLeaseHolder(executionState = null, taskRunId = "") {
71
+ const id = String(taskRunId || "").trim();
72
+ if (!id) return null;
73
+ return listWriteLeaseHolders(executionState).find((h) => h.taskRunId === id) || null;
74
+ }
75
+
76
+ function canAcquireWriteLease(executionState = null, taskRunId = "") {
77
+ const id = String(taskRunId || "").trim();
78
+ if (!id) return { ok: false, code: "MISSING_TASK_RUN_ID" };
79
+ if (findWriteLeaseHolder(executionState, id)) {
80
+ return { ok: true, idempotent: true };
81
+ }
82
+ const count = countWriteLeases(executionState);
83
+ if (count >= MAX_CONCURRENT_WRITE_LEASES) {
84
+ return {
85
+ ok: false,
86
+ code: "MAX_CONCURRENT_TASKS",
87
+ max: MAX_CONCURRENT_WRITE_LEASES,
88
+ current: count,
89
+ holders: listWriteLeaseHolders(executionState),
90
+ };
91
+ }
92
+ return { ok: true, current: count, max: MAX_CONCURRENT_WRITE_LEASES };
93
+ }
94
+
95
+ function acquireTaskWriteLease(executionState = null, taskRunId = "") {
96
+ const lease = ensureWorkspaceLease(executionState);
97
+ const holders = normalizeHolders(lease);
98
+ const id = String(taskRunId || "").trim();
99
+ if (!id) {
100
+ return { ok: false, code: "MISSING_TASK_RUN_ID" };
101
+ }
102
+
103
+ const existing = holders.find((h) => h.taskRunId === id);
104
+ if (existing) {
105
+ return { ok: true, lease, holder: existing, idempotent: true };
106
+ }
107
+
108
+ if (holders.length >= MAX_CONCURRENT_WRITE_LEASES) {
109
+ return {
110
+ ok: false,
111
+ code: "MAX_CONCURRENT_TASKS",
112
+ max: MAX_CONCURRENT_WRITE_LEASES,
113
+ current: holders.length,
114
+ holders: holders.slice(),
115
+ message: `At most ${MAX_CONCURRENT_WRITE_LEASES} concurrent writing TaskRuns`,
116
+ };
117
+ }
118
+
119
+ const holder = {
120
+ kind: "task_run",
121
+ taskRunId: id,
122
+ acquiredAt: new Date().toISOString(),
123
+ };
124
+ holders.push(holder);
125
+ lease.holders = holders;
126
+ lease.mode = "write";
127
+ return { ok: true, lease, holder };
128
+ }
129
+
130
+ function releaseTaskWriteLease(executionState = null, taskRunId = "") {
131
+ const lease = ensureWorkspaceLease(executionState);
132
+ const holders = normalizeHolders(lease);
133
+ const id = String(taskRunId || "").trim();
134
+ if (!id) return { ok: true, lease };
135
+ lease.holders = holders.filter((h) => h.taskRunId !== id);
136
+ if (lease.holders.length === 0) {
137
+ lease.acquiredAt = "";
138
+ }
139
+ return { ok: true, lease };
140
+ }
141
+
142
+ function clearWorkspaceLease(executionState = null) {
143
+ const lease = ensureWorkspaceLease(executionState);
144
+ lease.holders = [];
145
+ lease.holder = null;
146
+ lease.acquiredAt = "";
147
+ return lease;
148
+ }
149
+
150
+ /**
151
+ * @param {string} tool
152
+ * @param {"agent_loop"|"task_loop"} originKind
153
+ * @param {string} [taskRunId]
154
+ */
155
+ function checkWriteAllowed(executionState = null, {
156
+ tool = "",
157
+ originKind = "agent_loop",
158
+ taskRunId = "",
159
+ } = {}) {
160
+ const name = String(tool || "").trim().toLowerCase();
161
+ if (!WRITE_TOOLS.has(name)) return { ok: true };
162
+
163
+ const holders = listWriteLeaseHolders(executionState);
164
+ if (holders.length === 0) return { ok: true };
165
+
166
+ if (originKind === "task_loop") {
167
+ const id = String(taskRunId || "").trim();
168
+ if (holders.some((h) => h.taskRunId === id)) {
169
+ return { ok: true };
170
+ }
171
+ return {
172
+ ok: false,
173
+ code: "WORKSPACE_WRITE_LEASE_HELD",
174
+ holders,
175
+ message: "This TaskRun does not hold a workspace write lease.",
176
+ };
177
+ }
178
+
179
+ // Agent loop cannot write while any task holds a lease.
180
+ return {
181
+ ok: false,
182
+ code: "WORKSPACE_WRITE_LEASE_HELD",
183
+ holders,
184
+ owner: holders[0] || null,
185
+ message: `${holders.length} active TaskRun(s) hold workspace write lease(s); cancel or wait before writing.`,
186
+ };
187
+ }
188
+
189
+ function hasActiveWriteLease(executionState = null) {
190
+ return countWriteLeases(executionState) > 0;
191
+ }
192
+
193
+ module.exports = {
194
+ WRITE_TOOLS,
195
+ MAX_CONCURRENT_WRITE_LEASES,
196
+ emptyWorkspaceLease,
197
+ ensureWorkspaceLease,
198
+ normalizeHolders,
199
+ listWriteLeaseHolders,
200
+ countWriteLeases,
201
+ findWriteLeaseHolder,
202
+ canAcquireWriteLease,
203
+ acquireTaskWriteLease,
204
+ releaseTaskWriteLease,
205
+ clearWorkspaceLease,
206
+ checkWriteAllowed,
207
+ hasActiveWriteLease,
208
+ };
@@ -1,7 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { randomUUID } = require("crypto");
4
- const { isContextV2Enabled } = require("./context/featureFlag");
5
4
  const {
6
5
  getTranscriptsDir,
7
6
  getTranscriptFilePath,
@@ -72,7 +71,6 @@ function buildSessionSnapshot(input = {}) {
72
71
  const source = input && typeof input === "object" ? input : {};
73
72
  const sessionId = resolveSessionId(source.sessionId);
74
73
  const createdAt = String(source.createdAt || "").trim() || toIsoNow();
75
- const useV2 = isContextV2Enabled() || Number(source.version) >= 2;
76
74
 
77
75
  const base = {
78
76
  sessionId,
@@ -84,14 +82,6 @@ function buildSessionSnapshot(input = {}) {
84
82
  updatedAt: toIsoNow(),
85
83
  };
86
84
 
87
- if (!useV2) {
88
- return {
89
- version: 1,
90
- ...base,
91
- nlMessages: cloneMessages(source.nlMessages),
92
- };
93
- }
94
-
95
85
  return {
96
86
  version: 2,
97
87
  ...base,
@@ -195,4 +195,5 @@ module.exports = {
195
195
  readSkillBlock,
196
196
  persistSkillBodyArtifact,
197
197
  buildSkillInjections,
198
+ sanitizeSkillContent,
198
199
  };
@@ -4,8 +4,34 @@
4
4
  */
5
5
 
6
6
  const { runNativeAgentTask } = require("./nativeRunner");
7
- const { isContextV2Enabled } = require("./context/featureFlag");
8
7
  const { assembleModelContext, recordToolCallInSession } = require("./context/assembler");
8
+ const fs = require("fs");
9
+ const {
10
+ buildSkillManifest,
11
+ renderActiveSkillBlock,
12
+ } = require("./skills");
13
+ const { sanitizeSkillContent } = require("./skills/injection");
14
+
15
+ function renderActiveSkillBodiesFromState(state = {}) {
16
+ const skills = Array.isArray(state.activeSkills) ? state.activeSkills : [];
17
+ const blocks = [];
18
+ for (const skill of skills) {
19
+ const skillPath = String(skill && skill.path || "").trim();
20
+ if (!skillPath) continue;
21
+ try {
22
+ const raw = fs.readFileSync(skillPath, "utf8");
23
+ const content = sanitizeSkillContent(raw);
24
+ const manifest = buildSkillManifest({
25
+ name: skill.name || "",
26
+ path: skillPath,
27
+ }, { bodyArtifactId: skill.bodyArtifactId || "" });
28
+ blocks.push(renderActiveSkillBlock(manifest, content));
29
+ } catch {
30
+ // ignore missing skill bodies
31
+ }
32
+ }
33
+ return blocks;
34
+ }
9
35
 
10
36
  /**
11
37
  * Decompose a bug fix task into manageable steps
@@ -135,13 +161,11 @@ async function runDecomposedTask({
135
161
  messages = [],
136
162
  sessionId = "",
137
163
  state = null,
138
- contextV2 = false,
139
164
  systemBlocks = null,
140
165
  }) {
141
166
  const steps = decomposeBugFixTask(task);
142
167
  const results = [];
143
168
  let aborted = false;
144
- const useV2 = Boolean(contextV2 || isContextV2Enabled());
145
169
 
146
170
  // Check if already aborted
147
171
  if (signal && signal.aborted) {
@@ -174,12 +198,13 @@ async function runDecomposedTask({
174
198
  let stepMessages = messages;
175
199
  let stepSystemPrompt = systemPrompt;
176
200
  let stepSystemBlocks = systemBlocks;
177
- if (useV2 && state) {
201
+ if (state) {
202
+ const skillBodies = renderActiveSkillBodiesFromState(state);
178
203
  const assembled = assembleModelContext(state, {
179
204
  workspaceRoot,
180
205
  model,
181
206
  provider,
182
- turnDynamic: stepPrompt,
207
+ turnDynamic: [...skillBodies, stepPrompt].filter(Boolean).join("\n\n"),
183
208
  });
184
209
  stepMessages = assembled.messages;
185
210
  stepSystemPrompt = assembled.systemPrompt;
@@ -197,13 +222,12 @@ async function runDecomposedTask({
197
222
  timeoutMs: step.timeoutMs,
198
223
  onToolEvent,
199
224
  signal,
200
- contextV2: useV2,
201
- onArtifactPersisted: useV2 && state
225
+ onArtifactPersisted: state
202
226
  ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
203
227
  : null,
204
228
  });
205
229
 
206
- if (useV2 && state && stepResult && Array.isArray(stepResult.messages)) {
230
+ if (state && stepResult && Array.isArray(stepResult.messages)) {
207
231
  const { syncMessagesToTranscript } = require("./context/assembler");
208
232
  syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
209
233
  }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ const { runAskUserTool } = require("../context/userInteraction");
4
+
5
+ function runAskUserToolDispatch(args = {}, options = {}) {
6
+ return runAskUserTool(args, options);
7
+ }
8
+
9
+ module.exports = {
10
+ runAskUserTool: runAskUserToolDispatch,
11
+ };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ const {
4
+ runPlanGraphCommand,
5
+ normalizePlanGraphCommand,
6
+ } = require("../context/planGraphService");
7
+
8
+ function runPlanGraphTool(args = {}, options = {}) {
9
+ const command = normalizePlanGraphCommand(args) || args;
10
+ const result = runPlanGraphCommand(command, {
11
+ executionState: options.executionState,
12
+ runTool: options.runTool,
13
+ autoAdvance: options.autoAdvance !== false,
14
+ parallel: options.parallel !== false,
15
+ knownTools: options.knownTools,
16
+ maxNodeRuns: options.maxNodeRuns,
17
+ });
18
+
19
+ const payload = result.modelPayload || result;
20
+ return {
21
+ ok: payload.status === "accepted",
22
+ ...payload,
23
+ executionState: result.executionState,
24
+ };
25
+ }
26
+
27
+ module.exports = {
28
+ runPlanGraphTool,
29
+ };
@@ -129,7 +129,16 @@ function normalizeModelLabel(model = "") {
129
129
  return "default";
130
130
  }
131
131
 
132
- function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = "", agentId = "", workspaceRoot = "", sessionId = "", width = 0 } = {}) {
132
+ function buildUcodeBannerLines({
133
+ model = "",
134
+ engine = "ufoo-core",
135
+ nickname = "",
136
+ agentId = "",
137
+ workspaceRoot = "",
138
+ sessionId = "",
139
+ width = 0,
140
+ planMode = false,
141
+ } = {}) {
133
142
  const modelLabel = normalizeModelLabel(model);
134
143
  void width;
135
144
  void engine;
@@ -151,6 +160,9 @@ function buildUcodeBannerLines({ model = "", engine = "ufoo-core", nickname = ""
151
160
  const infoLines = [];
152
161
  infoLines.push(`${chalk.dim("Version:")} ${chalk.cyan.bold(UCODE_VERSION)}`);
153
162
  infoLines.push(`${chalk.dim("Model:")} ${chalk.yellow(modelLabel)}`);
163
+ if (planMode) {
164
+ infoLines.push(`${chalk.dim("Mode:")} ${chalk.magenta.bold("PLAN")}`);
165
+ }
154
166
  infoLines.push(`${chalk.dim("Dictionary:")} ${chalk.gray(shortPath)}`);
155
167
  const normalizedSessionId = String(sessionId || "").trim();
156
168
  if (normalizedSessionId) {
@@ -254,6 +266,16 @@ function renderLogLinesWithMarkdownAnsi(text = "", state = {}) {
254
266
  return renderMarkdownLinesAnsi(text, state);
255
267
  }
256
268
 
269
+ function createMarkdownTableBuffer() {
270
+ const { createMarkdownTableBuffer: create } = require("./markdownRenderer");
271
+ return create();
272
+ }
273
+
274
+ function isTableRowLine(line = "") {
275
+ const { isTableRowLine: check } = require("./markdownRenderer");
276
+ return check(line);
277
+ }
278
+
257
279
  function messageContentText(message = {}) {
258
280
  if (!message || typeof message !== "object") return "";
259
281
  const content = message.content;
@@ -1233,6 +1255,8 @@ module.exports = {
1233
1255
  planProjectsRail,
1234
1256
  renderLogLinesWithMarkdown,
1235
1257
  renderLogLinesWithMarkdownAnsi,
1258
+ createMarkdownTableBuffer,
1259
+ isTableRowLine,
1236
1260
  resolveAgentSelectionOnDown,
1237
1261
  resolveHistoryDownTransition,
1238
1262
  shouldClearAgentSelectionOnUp,