u-foo 2.5.14 → 2.5.15

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +140 -30
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +313 -27
  39. package/src/ui/ink/chatLogModel.js +102 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.5.14",
3
+ "version": "2.5.15",
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",
@@ -24,13 +24,12 @@ function getShellName() {
24
24
  return shell || "unknown";
25
25
  }
26
26
 
27
- function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" } = {}) {
27
+ function getSessionStableEnvironmentSection({ workspaceRoot = "", model = "", provider = "" } = {}) {
28
28
  const cwd = workspaceRoot || process.cwd();
29
29
  const isGit = getIsGit(cwd);
30
30
  const platform = process.platform;
31
31
  const shell = getShellName();
32
32
  const osInfo = `${os.type()} ${os.release()}`;
33
- const date = new Date().toISOString().slice(0, 10);
34
33
 
35
34
  const lines = [
36
35
  `Working directory: ${cwd}`,
@@ -38,16 +37,11 @@ function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" }
38
37
  `Platform: ${platform}`,
39
38
  `Shell: ${shell}`,
40
39
  `OS: ${osInfo}`,
41
- `Date: ${date}`,
42
40
  ];
43
41
 
44
42
  if (provider) lines.push(`Provider: ${provider}`);
45
43
  if (model) lines.push(`Model: ${model}`);
46
44
 
47
- // Tell the model who it is on the bus. Without this, the only identities
48
- // it ever sees are other agents' records in shared context — and it
49
- // adopts them (observed in the wild: ucode-3 introducing itself as
50
- // claude-6, then accepting a wrong name from the user).
51
45
  const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
52
46
  const nickname = String(process.env.UFOO_NICKNAME || "").trim();
53
47
  if (subscriberId || nickname) {
@@ -58,4 +52,22 @@ function getEnvironmentSection({ workspaceRoot = "", model = "", provider = "" }
58
52
  return `# Environment\n${lines.map((l) => ` - ${l}`).join("\n")}`;
59
53
  }
60
54
 
61
- module.exports = { getEnvironmentSection, getIsGit };
55
+ function getTurnDynamicEnvironmentSection({ workspaceRoot = "" } = {}) {
56
+ const cwd = workspaceRoot || process.cwd();
57
+ const date = new Date().toISOString().slice(0, 10);
58
+ return `# Turn Environment\n - Date: ${date}\n - Working directory (current): ${cwd}`;
59
+ }
60
+
61
+ function getEnvironmentSection(options = {}) {
62
+ return [
63
+ getSessionStableEnvironmentSection(options),
64
+ getTurnDynamicEnvironmentSection(options),
65
+ ].join("\n\n");
66
+ }
67
+
68
+ module.exports = {
69
+ getEnvironmentSection,
70
+ getSessionStableEnvironmentSection,
71
+ getTurnDynamicEnvironmentSection,
72
+ getIsGit,
73
+ };
package/src/code/agent.js CHANGED
@@ -12,6 +12,31 @@ const {
12
12
  } = require("./sessionStore");
13
13
  const { buildPromptContext } = require("../agents/prompts/native");
14
14
  const { buildSkillInjections } = require("./skills");
15
+ const { isContextV2Enabled } = require("./context/featureFlag");
16
+ const {
17
+ assembleModelContext,
18
+ syncMessagesToTranscript,
19
+ applyContextSideEffects,
20
+ ensureProjectSnapshot,
21
+ recordToolCallInSession,
22
+ commitAfterSegmentEnd,
23
+ sanitizeModelMessages,
24
+ } = require("./context/assembler");
25
+ const { buildLayeredSystemPrompt } = require("./context/promptLayers");
26
+ const {
27
+ createProjectPreflightContextV2,
28
+ } = require("./context/projectSnapshot");
29
+ const {
30
+ ensureTaskContract,
31
+ ensureStateEpoch,
32
+ parseStructuredSideEffects,
33
+ patchTaskContractFromUserMessage,
34
+ } = require("./context/stateCommit");
35
+ const { applyWorkingSetPlan } = require("./context/workingSet");
36
+ const {
37
+ parseExecutionSegment,
38
+ executeExecutionSegment,
39
+ } = require("./context/executionSegment");
15
40
  const {
16
41
  runUbusCommand,
17
42
  parseBusCheckOutput,
@@ -34,6 +59,104 @@ const {
34
59
  parseAgentArgs,
35
60
  } = require("./repl");
36
61
 
62
+ function ensureContextSessionState(state = {}) {
63
+ if (!Array.isArray(state.workingSet)) state.workingSet = [];
64
+ if (!state.executionState || typeof state.executionState !== "object") {
65
+ const { emptyExecutionState } = require("./context/executionSegment");
66
+ state.executionState = emptyExecutionState();
67
+ }
68
+ if (!state.contextPolicy || typeof state.contextPolicy !== "object") {
69
+ const { defaultContextPolicy } = require("./context/assembler");
70
+ state.contextPolicy = defaultContextPolicy();
71
+ }
72
+ if (!Number.isFinite(state.toolCallsSinceCommit)) state.toolCallsSinceCommit = 0;
73
+ ensureStateEpoch(state);
74
+ return state;
75
+ }
76
+
77
+ function buildSkillBodyBlocks(skillInjections = {}) {
78
+ const blocks = Array.isArray(skillInjections.blocks) ? skillInjections.blocks : [];
79
+ return blocks.map((block) => {
80
+ const text = String(block || "");
81
+ if (text.includes("<active_skill>")) return text;
82
+ return text.replace(/^<skill>/, "<active_skill>").replace(/<\/skill>/, "</active_skill>");
83
+ });
84
+ }
85
+
86
+ async function runExecutionSegmentSteps({
87
+ segment = {},
88
+ workspaceRoot = process.cwd(),
89
+ sessionId = "",
90
+ state = {},
91
+ pushToolLog = () => null,
92
+ } = {}) {
93
+ const exec = executeExecutionSegment({
94
+ segment,
95
+ executionState: state.executionState,
96
+ onStepStart: ({ tool, args }) => {
97
+ pushToolLog({ tool, phase: "start", args, error: "" });
98
+ },
99
+ onStepComplete: ({ tool, args, result }) => {
100
+ pushToolLog({
101
+ tool,
102
+ phase: result && result.ok === false ? "error" : "",
103
+ args,
104
+ error: result && result.ok === false ? String(result.error || "") : "",
105
+ });
106
+ if (isContextV2Enabled() && result && result.ok !== false) {
107
+ const plan = require("./context/workingSet").defaultContextPlanFromToolEvent(
108
+ tool,
109
+ result.artifactId,
110
+ args,
111
+ );
112
+ if (plan) state.workingSet = applyWorkingSetPlan(state.workingSet, plan, state);
113
+ }
114
+ if ((tool === "write" || tool === "edit") && args && args.path) {
115
+ const filePath = String(args.path);
116
+ if (!state.executionState || typeof state.executionState !== "object") {
117
+ state.executionState = require("./context/executionSegment").emptyExecutionState();
118
+ }
119
+ const files = Array.isArray(state.executionState.modifiedFiles)
120
+ ? state.executionState.modifiedFiles.slice()
121
+ : [];
122
+ if (!files.includes(filePath)) files.push(filePath);
123
+ state.executionState.modifiedFiles = files;
124
+ }
125
+ },
126
+ runStep: ({ tool, args }) => {
127
+ const { runToolCall: dispatchToolCall } = require("./dispatch");
128
+ const { persistToolResultToContext } = require("./context/assembler");
129
+ const result = dispatchToolCall(
130
+ { tool, args },
131
+ { workspaceRoot, cwd: workspaceRoot, sessionId },
132
+ );
133
+ if (!result || result.ok === false || !isContextV2Enabled()) {
134
+ return result;
135
+ }
136
+ const persisted = persistToolResultToContext({
137
+ workspaceRoot,
138
+ sessionId,
139
+ tool,
140
+ args,
141
+ rawResult: result,
142
+ });
143
+ recordToolCallInSession(state, persisted, workspaceRoot);
144
+ return persisted.modelPayload || result;
145
+ },
146
+ });
147
+ state.executionState = exec.executionState;
148
+ if (isContextV2Enabled()) {
149
+ commitAfterSegmentEnd(state, exec, workspaceRoot);
150
+ }
151
+ return {
152
+ ok: exec.ok,
153
+ segmentId: exec.segmentId,
154
+ error: exec.error,
155
+ stoppedAt: exec.stoppedAt,
156
+ };
157
+ }
158
+
159
+
37
160
  function readTextOrFile(value = "") {
38
161
  const raw = String(value || "").trim();
39
162
  if (!raw) return "";
@@ -137,14 +260,14 @@ function isCliCancelledError(message = "") {
137
260
  }
138
261
 
139
262
  function computeExtendedTimeout(baseTimeoutMs) {
140
- const base = Number.isFinite(baseTimeoutMs) ? Math.max(1000, Math.floor(baseTimeoutMs)) : 300000;
141
- return Math.min(1800000, Math.max(base * 2, base + 120000));
263
+ const base = Number.isFinite(baseTimeoutMs) ? Math.max(1000, Math.floor(baseTimeoutMs)) : 43200000;
264
+ return Math.min(43200000, Math.max(base * 2, base + 120000));
142
265
  }
143
266
 
144
267
  // Reasoning models routinely blew the old 10min budget across a multi-turn
145
- // tool loop. Total per-task budget defaults to 30min and can be raised per
268
+ // tool loop. Total per-task budget defaults to 12h and can be raised per
146
269
  // call, via --timeout-ms, or via UFOO_UCODE_TASK_TIMEOUT_MS.
147
- const DEFAULT_NL_TASK_TIMEOUT_MS = 1800000;
270
+ const DEFAULT_NL_TASK_TIMEOUT_MS = 43200000;
148
271
 
149
272
  function resolveNlTaskTimeoutMs(value) {
150
273
  if (Number.isFinite(value) && value > 0) return Math.max(1000, Math.floor(value));
@@ -190,7 +313,7 @@ function normalizeToolLogEvent(event = {}) {
190
313
  if (!event || typeof event !== "object") return null;
191
314
  const tool = String(event.tool || event.name || "").trim().toLowerCase();
192
315
  if (!tool) return null;
193
- if (tool !== "read" && tool !== "write" && tool !== "edit" && tool !== "bash") return null;
316
+ if (tool !== "read" && tool !== "write" && tool !== "edit" && tool !== "bash" && tool !== "artifact_read") return null;
194
317
  const phase = String(event.phase || "update").trim().toLowerCase();
195
318
  const normalizedPhase = phase === "error" ? "error" : (phase === "start" ? "start" : "");
196
319
  if (!normalizedPhase) return null;
@@ -247,7 +370,7 @@ function pushSkillWarning(logs = [], onToolLog = null, warning = "") {
247
370
 
248
371
  function stripSkillBlocksFromText(value = "") {
249
372
  return String(value || "")
250
- .replace(/<skill>\s*[\s\S]*?<\/skill>\s*/g, "")
373
+ .replace(/<(?:active_)?skill>\s*[\s\S]*?<\/(?:active_)?skill>\s*/gi, "")
251
374
  .trim();
252
375
  }
253
376
 
@@ -427,28 +550,80 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
427
550
  const useDecomposition = isBugFixTask && !options.disableDecomposition;
428
551
  const analysisTask = isProjectAnalysisTask(taskText);
429
552
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
430
- const preflightContext = analysisTask
431
- ? createProjectPreflightContext({
432
- workspaceRoot,
433
- pushToolLog,
434
- })
435
- : "";
553
+ const contextV2 = isContextV2Enabled();
554
+ if (contextV2) ensureContextSessionState(state);
555
+
556
+ let preflightContext = "";
557
+ let projectSnapshot = state.projectSnapshot || null;
558
+ if (analysisTask) {
559
+ if (contextV2) {
560
+ projectSnapshot = createProjectPreflightContextV2({
561
+ workspaceRoot,
562
+ sessionId: String(state.sessionId || ""),
563
+ pushToolLog,
564
+ existingSnapshot: state.projectSnapshot,
565
+ });
566
+ state.projectSnapshot = projectSnapshot;
567
+ } else {
568
+ preflightContext = createProjectPreflightContext({
569
+ workspaceRoot,
570
+ pushToolLog,
571
+ });
572
+ }
573
+ } else if (contextV2) {
574
+ projectSnapshot = ensureProjectSnapshot(state, workspaceRoot);
575
+ }
576
+
577
+ if (contextV2) {
578
+ ensureTaskContract(state, taskText);
579
+ state.taskContract = patchTaskContractFromUserMessage(state.taskContract, taskText);
580
+ }
581
+
436
582
  const taskPrompt = analysisTask
437
583
  ? `${taskText}\n\nAnalysis requirements:\n- Inspect repository evidence before concluding.\n- Cite concrete file observations.\n- Keep findings concise and actionable.`
438
584
  : taskText;
439
585
  const skillInjections = buildSkillInjections({
440
586
  prompt: taskPrompt,
441
587
  workspaceRoot,
588
+ sessionId: String(state.sessionId || ""),
589
+ persistBodies: contextV2,
590
+ useActiveSkillTag: contextV2,
442
591
  });
443
592
  for (const warning of skillInjections.warnings || []) {
444
593
  pushSkillWarning(logs, onToolLog, warning);
445
594
  }
446
- const effectiveTaskPrompt = Array.isArray(skillInjections.blocks) && skillInjections.blocks.length > 0
447
- ? `${skillInjections.blocks.join("\n\n")}\n\n${taskPrompt}`
448
- : taskPrompt;
449
- const systemContext = [String(state.context || "").trim(), preflightContext]
450
- .filter(Boolean)
451
- .join("\n\n");
595
+ if (contextV2 && Array.isArray(skillInjections.activeSkills) && skillInjections.activeSkills.length > 0) {
596
+ state.activeSkills = skillInjections.activeSkills;
597
+ }
598
+ const skillBodyBlocks = contextV2
599
+ ? buildSkillBodyBlocks(skillInjections)
600
+ : (skillInjections.blocks || []);
601
+ // v1: skill body rides in the user prompt.
602
+ // v2: skill body goes only into turnDynamic (system layered prompt) to avoid
603
+ // double injection and mixed system/user privilege semantics.
604
+ const effectiveTaskPrompt = contextV2
605
+ ? taskPrompt
606
+ : (skillBodyBlocks.length > 0
607
+ ? `${skillBodyBlocks.join("\n\n")}\n\n${taskPrompt}`
608
+ : taskPrompt);
609
+
610
+ let assembled = null;
611
+ let systemContext = "";
612
+ if (contextV2) {
613
+ assembled = assembleModelContext(state, {
614
+ workspaceRoot,
615
+ model,
616
+ provider,
617
+ turnDynamic: skillBodyBlocks.join("\n\n"),
618
+ latestUserMessage: effectiveTaskPrompt,
619
+ });
620
+ systemContext = assembled.systemPrompt;
621
+ state.summary = assembled.summary || state.summary;
622
+ } else {
623
+ systemContext = [String(state.context || "").trim(), preflightContext]
624
+ .filter(Boolean)
625
+ .join("\n\n");
626
+ }
452
627
 
453
628
  const onStream = onDelta
454
629
  ? (delta) => {
@@ -468,20 +643,32 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
468
643
  : runNativeAgentTask;
469
644
  const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
470
645
  const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
646
+ let lastTranscriptBaseline = 0;
471
647
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
472
648
  toolEventsThisAttempt = 0;
649
+ const historyMessages = contextV2 && assembled
650
+ ? assembled.messages
651
+ : (Array.isArray(state.nlMessages) ? state.nlMessages : []);
652
+ // Sanitized length matches what nativeRunner clones before appending this
653
+ // turn's user/tool/assistant messages — used as the transcript sync baseline.
654
+ lastTranscriptBaseline = sanitizeModelMessages(historyMessages).length;
473
655
  return runNativeAgentImpl({
474
656
  workspaceRoot,
475
657
  provider,
476
658
  model,
477
659
  prompt: effectiveTaskPrompt,
478
660
  systemPrompt: systemContext,
479
- messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
480
- sessionId: String(sessionIdValue || ""),
661
+ systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
662
+ messages: historyMessages,
663
+ sessionId: String(sessionIdValue || state.sessionId || ""),
481
664
  timeoutMs: timeoutOverrideMs,
482
665
  onStreamDelta: onStream,
483
666
  onThinkingDelta,
484
667
  onPhase,
668
+ contextV2,
669
+ onArtifactPersisted: contextV2
670
+ ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
671
+ : null,
485
672
  onToolEvent: (event) => {
486
673
  toolEventsThisAttempt += 1;
487
674
  pushToolLog(event);
@@ -493,6 +680,29 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
493
680
  try {
494
681
  let cliRes;
495
682
 
683
+ const requestedSegment = parseExecutionSegment(options.executionSegment || options.nextSegment || null);
684
+ if (contextV2 && requestedSegment && requestedSegment.steps && requestedSegment.steps.length > 0) {
685
+ const segmentResult = await runExecutionSegmentSteps({
686
+ segment: requestedSegment,
687
+ workspaceRoot,
688
+ sessionId: String(state.sessionId || ""),
689
+ state,
690
+ pushToolLog,
691
+ });
692
+ if (!segmentResult.ok) {
693
+ return {
694
+ ok: false,
695
+ summary: "",
696
+ artifacts: [],
697
+ logs: logs.slice(),
698
+ error: segmentResult.error,
699
+ metrics: {},
700
+ streamed: false,
701
+ streamLastChar: "",
702
+ };
703
+ }
704
+ }
705
+
496
706
  // Use decomposed runner for bug fix tasks
497
707
  if (useDecomposition) {
498
708
  const decomposedResult = await runDecomposedTask({
@@ -506,6 +716,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
506
716
  systemPrompt: systemContext,
507
717
  messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
508
718
  sessionId: String(state.sessionId || ""),
719
+ state: contextV2 ? state : null,
720
+ contextV2,
721
+ systemBlocks: contextV2 && assembled ? assembled.systemBlocks : null,
509
722
  });
510
723
 
511
724
  if (decomposedResult.ok) {
@@ -555,14 +768,36 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
555
768
  }
556
769
  if (cliRes && Array.isArray(cliRes.messages)) {
557
770
  state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
771
+ if (contextV2) {
772
+ syncMessagesToTranscript(state, cliRes.messages, workspaceRoot, {
773
+ baselineCount: lastTranscriptBaseline,
774
+ });
775
+ }
558
776
  }
559
777
  const normalized = String(cliRes.output || "").trim();
778
+ const sideEffects = contextV2 ? parseStructuredSideEffects(normalized) : null;
779
+ if (contextV2 && sideEffects) {
780
+ applyContextSideEffects(state, sideEffects);
781
+ const nextSegment = parseExecutionSegment(sideEffects);
782
+ if (nextSegment && nextSegment.steps && nextSegment.steps.length > 0) {
783
+ await runExecutionSegmentSteps({
784
+ segment: nextSegment,
785
+ workspaceRoot,
786
+ sessionId: String(state.sessionId || ""),
787
+ state,
788
+ pushToolLog,
789
+ });
790
+ }
791
+ }
560
792
  const summary = extractJsonSummary(normalized);
561
793
  const resolvedSummary = String(summary || "").trim() || buildNlFallbackSummary(logs);
794
+ const artifactIds = contextV2 && Array.isArray(state.workingSet)
795
+ ? state.workingSet.map((entry) => entry.artifactId).filter(Boolean)
796
+ : [];
562
797
  return {
563
798
  ok: true,
564
799
  summary: resolvedSummary,
565
- artifacts: [],
800
+ artifacts: artifactIds,
566
801
  logs: logs.slice(),
567
802
  error: "",
568
803
  metrics: {},
@@ -626,8 +861,7 @@ function buildNlContext({
626
861
  || readTextOrFile(process.env.UFOO_UCODE_PROMPT_FILE)
627
862
  || "";
628
863
 
629
- // New modular prompt assembly
630
- return clampContext(buildPromptContext({
864
+ return clampContext(resolveWireSystemPrompt({
631
865
  workspaceRoot: workspaceRoot || process.cwd(),
632
866
  model,
633
867
  provider,
@@ -635,6 +869,44 @@ function buildNlContext({
635
869
  }));
636
870
  }
637
871
 
872
+ /**
873
+ * Single wire entry for system prompt assembly.
874
+ * v2 (default): layered Context Manager prompt.
875
+ * v1 (explicit off): legacy flat buildPromptContext.
876
+ */
877
+ function resolveWireSystemPrompt({
878
+ workspaceRoot = process.cwd(),
879
+ model = "",
880
+ provider = "",
881
+ appendSystemPrompt = "",
882
+ overrideSystemPrompt = "",
883
+ epochDynamic = "",
884
+ turnDynamic = "",
885
+ sessionStableExtras = "",
886
+ } = {}) {
887
+ if (overrideSystemPrompt) return String(overrideSystemPrompt);
888
+
889
+ if (isContextV2Enabled()) {
890
+ return buildLayeredSystemPrompt({
891
+ workspaceRoot,
892
+ model,
893
+ provider,
894
+ appendSystemPrompt,
895
+ epochDynamic,
896
+ turnDynamic,
897
+ sessionStableExtras,
898
+ }).flatText;
899
+ }
900
+
901
+ // Legacy v1 path — kept for UFOO_UCODE_CONTEXT_V2=0 compatibility only.
902
+ return buildPromptContext({
903
+ workspaceRoot,
904
+ model,
905
+ provider,
906
+ appendSystemPrompt,
907
+ });
908
+ }
909
+
638
910
  function buildSessionSnapshotFromState(state = {}) {
639
911
  const source = state && typeof state === "object" ? state : {};
640
912
  return {
@@ -645,6 +917,27 @@ function buildSessionSnapshotFromState(state = {}) {
645
917
  context: String(source.context || ""),
646
918
  nlMessages: Array.isArray(source.nlMessages) ? source.nlMessages : [],
647
919
  createdAt: String(source.sessionCreatedAt || "").trim(),
920
+ summary: String(source.summary || "").trim(),
921
+ projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
922
+ ? source.projectSnapshot
923
+ : null,
924
+ taskContract: source.taskContract && typeof source.taskContract === "object"
925
+ ? source.taskContract
926
+ : null,
927
+ stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
928
+ ? source.stateEpoch
929
+ : null,
930
+ workingSet: Array.isArray(source.workingSet) ? source.workingSet : [],
931
+ executionState: source.executionState && typeof source.executionState === "object"
932
+ ? source.executionState
933
+ : null,
934
+ contextPolicy: source.contextPolicy && typeof source.contextPolicy === "object"
935
+ ? source.contextPolicy
936
+ : null,
937
+ toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
938
+ ? Math.max(0, Math.floor(source.toolCallsSinceCommit))
939
+ : 0,
940
+ activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
648
941
  };
649
942
  }
650
943
 
@@ -708,12 +1001,32 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
708
1001
  state.context = String(snapshot.context || "");
709
1002
  state.nlMessages = Array.isArray(snapshot.nlMessages) ? snapshot.nlMessages : [];
710
1003
  state.sessionCreatedAt = String(snapshot.createdAt || "").trim();
1004
+ state.summary = String(snapshot.summary || "").trim();
1005
+ state.projectSnapshot = snapshot.projectSnapshot || null;
1006
+ state.taskContract = snapshot.taskContract || null;
1007
+ state.stateEpoch = snapshot.stateEpoch || null;
1008
+ state.workingSet = Array.isArray(snapshot.workingSet) ? snapshot.workingSet : [];
1009
+ state.executionState = snapshot.executionState || null;
1010
+ state.contextPolicy = snapshot.contextPolicy || null;
1011
+ state.toolCallsSinceCommit = Number.isFinite(snapshot.toolCallsSinceCommit)
1012
+ ? snapshot.toolCallsSinceCommit
1013
+ : 0;
1014
+ state.activeSkills = Array.isArray(snapshot.activeSkills) ? snapshot.activeSkills : [];
1015
+ if (isContextV2Enabled()) {
1016
+ ensureContextSessionState(state);
1017
+ const { ensureTranscript } = require("./context/assembler");
1018
+ ensureTranscript(state, state.workspaceRoot);
1019
+ if (Array.isArray(state.transcriptEvents) && state.transcriptEvents.length > 0) {
1020
+ const { transcriptEventsToMessages } = require("./context/transcript");
1021
+ state.nlMessages = transcriptEventsToMessages(state.transcriptEvents, { preferArtifact: true });
1022
+ }
1023
+ }
711
1024
 
712
1025
  return {
713
1026
  ok: true,
714
1027
  error: "",
715
1028
  sessionId: state.sessionId,
716
- restoredMessages: state.nlMessages.length,
1029
+ restoredMessages: Array.isArray(state.nlMessages) ? state.nlMessages.length : 0,
717
1030
  };
718
1031
  }
719
1032
 
@@ -726,7 +1039,9 @@ module.exports = {
726
1039
  isProjectAnalysisTask,
727
1040
  buildNlFallbackSummary,
728
1041
  buildNlContext,
1042
+ resolveWireSystemPrompt,
729
1043
  stripSkillBlocksFromMessages,
1044
+ stripSkillBlocksFromText,
730
1045
  resolvePlannerProvider,
731
1046
  extractJsonSummary,
732
1047
  enrichNativeError,
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Native ucode slash-command registry (shared by REPL help + TUI completions).
5
+ */
6
+
7
+ const UCODE_COMMAND_REGISTRY = [
8
+ { cmd: "/help", desc: "Show available commands", order: 10 },
9
+ { cmd: "/status", desc: "Show session / usage status", order: 20 },
10
+ { cmd: "/model", desc: "Show or switch the active model", order: 25 },
11
+ { cmd: "/ubus", desc: "Check pending bus messages", order: 30 },
12
+ { cmd: "/resume", desc: "Resume a saved session", order: 40 },
13
+ { cmd: "/skills", desc: "List or show skills", order: 50 },
14
+ { cmd: "/bg", desc: "Run a task in the background", order: 60 },
15
+ { cmd: "/exit", desc: "Exit ucode", order: 90 },
16
+ ];
17
+
18
+ const UCODE_COMMAND_TREE = {
19
+ "/help": { desc: "Show available commands" },
20
+ "/status": { desc: "Show session / usage status" },
21
+ "/model": {
22
+ desc: "Show or switch the active model",
23
+ hasArguments: true,
24
+ optionalArguments: true,
25
+ },
26
+ "/ubus": { desc: "Check pending bus messages" },
27
+ "/resume": { desc: "Resume a saved session", hasArguments: true },
28
+ "/skills": {
29
+ desc: "List or show skills",
30
+ children: {
31
+ list: { desc: "List available skills", order: 1 },
32
+ show: { desc: "Show a skill by name", order: 2, hasArguments: true },
33
+ },
34
+ },
35
+ "/bg": { desc: "Run a task in the background", hasArguments: true },
36
+ "/exit": { desc: "Exit ucode" },
37
+ "/quit": { desc: "Exit ucode" },
38
+ };
39
+
40
+ function listUcodeCommandsForHelp() {
41
+ return [
42
+ "Commands:",
43
+ " /help",
44
+ " /exit|/quit",
45
+ " /ubus",
46
+ " /status",
47
+ " /model [model-id]",
48
+ " /skills [list]",
49
+ " /skills show <name>",
50
+ " /bg <task>",
51
+ " /resume <session-id>",
52
+ " tool <read|write|edit|bash> <args-json>",
53
+ " run <read|write|edit|bash> <args-json>",
54
+ ].join("\n");
55
+ }
56
+
57
+ module.exports = {
58
+ UCODE_COMMAND_REGISTRY,
59
+ UCODE_COMMAND_TREE,
60
+ listUcodeCommandsForHelp,
61
+ };