u-foo 2.5.13 → 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 +187 -31
  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 +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -1,6 +1,22 @@
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
+ const {
6
+ getTranscriptsDir,
7
+ getTranscriptFilePath,
8
+ loadTranscript,
9
+ migrateNlMessagesToTranscript,
10
+ appendTranscriptMessages,
11
+ transcriptEventsToMessages,
12
+ deleteTranscript,
13
+ } = require("./context/transcript");
14
+ const { deleteSessionArtifacts } = require("./context/artifacts");
15
+ const { deleteSessionCommitLog, maybeGcSessionArtifacts } = require("./context/artifactGc");
16
+ const { defaultContextPolicy } = require("./context/assembler");
17
+ const { emptyTaskContract } = require("./context/stateCommit");
18
+ const { emptyWorkingSet } = require("./context/workingSet");
19
+ const { emptyExecutionState } = require("./context/executionSegment");
4
20
 
5
21
  function getSessionsDir(workspaceRoot = process.cwd()) {
6
22
  const root = path.resolve(workspaceRoot || process.cwd());
@@ -40,21 +56,174 @@ function cloneMessages(value = []) {
40
56
  }
41
57
  }
42
58
 
59
+ function normalizeContextPolicy(value = {}) {
60
+ const defaults = defaultContextPolicy();
61
+ const source = value && typeof value === "object" ? value : {};
62
+ return {
63
+ ...defaults,
64
+ ...source,
65
+ transcriptWindow: Number.isFinite(source.transcriptWindow)
66
+ ? Math.max(1, Math.floor(source.transcriptWindow))
67
+ : defaults.transcriptWindow,
68
+ };
69
+ }
70
+
43
71
  function buildSessionSnapshot(input = {}) {
44
72
  const source = input && typeof input === "object" ? input : {};
45
73
  const sessionId = resolveSessionId(source.sessionId);
46
74
  const createdAt = String(source.createdAt || "").trim() || toIsoNow();
47
- return {
48
- version: 1,
75
+ const useV2 = isContextV2Enabled() || Number(source.version) >= 2;
76
+
77
+ const base = {
49
78
  sessionId,
50
79
  workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
51
80
  provider: String(source.provider || "").trim(),
52
81
  model: String(source.model || "").trim(),
53
82
  context: String(source.context || ""),
54
- nlMessages: cloneMessages(source.nlMessages),
55
83
  createdAt,
56
84
  updatedAt: toIsoNow(),
57
85
  };
86
+
87
+ if (!useV2) {
88
+ return {
89
+ version: 1,
90
+ ...base,
91
+ nlMessages: cloneMessages(source.nlMessages),
92
+ };
93
+ }
94
+
95
+ return {
96
+ version: 2,
97
+ ...base,
98
+ transcript: {
99
+ path: getTranscriptFilePath(base.workspaceRoot, sessionId),
100
+ },
101
+ artifacts: {
102
+ indexPath: path.join(base.workspaceRoot, ".ufoo", "agent", "ucode", "artifacts", sessionId),
103
+ },
104
+ contextPolicy: normalizeContextPolicy(source.contextPolicy),
105
+ summary: String(source.summary || "").trim(),
106
+ projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
107
+ ? source.projectSnapshot
108
+ : null,
109
+ taskContract: source.taskContract && typeof source.taskContract === "object"
110
+ ? source.taskContract
111
+ : emptyTaskContract(),
112
+ stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
113
+ ? source.stateEpoch
114
+ : null,
115
+ workingSet: Array.isArray(source.workingSet) ? source.workingSet : emptyWorkingSet(),
116
+ executionState: source.executionState && typeof source.executionState === "object"
117
+ ? source.executionState
118
+ : emptyExecutionState(),
119
+ activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
120
+ toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
121
+ ? Math.max(0, Math.floor(source.toolCallsSinceCommit))
122
+ : 0,
123
+ // In-memory compatibility for callers still reading nlMessages
124
+ nlMessages: cloneMessages(source.nlMessages),
125
+ };
126
+ }
127
+
128
+ function hydrateSessionFromDisk(snapshot = {}, workspaceRoot = process.cwd()) {
129
+ const payload = buildSessionSnapshot({
130
+ ...snapshot,
131
+ workspaceRoot: workspaceRoot || snapshot.workspaceRoot,
132
+ });
133
+ if (payload.version < 2) return payload;
134
+
135
+ const sessionId = payload.sessionId;
136
+ const transcript = loadTranscript(workspaceRoot, sessionId);
137
+ if (transcript.events.length > 0) {
138
+ const { transcriptEventsToMessages } = require("./context/transcript");
139
+ payload.nlMessages = transcriptEventsToMessages(transcript.events);
140
+ return payload;
141
+ }
142
+
143
+ if (Array.isArray(snapshot.nlMessages) && snapshot.nlMessages.length > 0) {
144
+ migrateNlMessagesToTranscript(workspaceRoot, sessionId, snapshot.nlMessages);
145
+ const reloaded = loadTranscript(workspaceRoot, sessionId);
146
+ const { transcriptEventsToMessages } = require("./context/transcript");
147
+ payload.nlMessages = transcriptEventsToMessages(reloaded.events);
148
+ }
149
+
150
+ return payload;
151
+ }
152
+
153
+ function syncTranscriptFromNlMessages(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
154
+ const messages = Array.isArray(nlMessages) ? nlMessages : [];
155
+ if (!sessionId || messages.length === 0) return;
156
+ const existing = loadTranscript(workspaceRoot, sessionId);
157
+ if (existing.events.length === 0) {
158
+ migrateNlMessagesToTranscript(workspaceRoot, sessionId, messages);
159
+ return;
160
+ }
161
+ const { matchTranscriptBaseline } = require("./context/assembler");
162
+ const priorMessages = transcriptEventsToMessages(existing.events);
163
+ const baseline = matchTranscriptBaseline(priorMessages, messages);
164
+ if (messages.length > baseline) {
165
+ appendTranscriptMessages(workspaceRoot, sessionId, messages.slice(baseline));
166
+ }
167
+ }
168
+
169
+ function listSessionSummaries(workspaceRoot = process.cwd(), { limit = 40 } = {}) {
170
+ const dir = getSessionsDir(workspaceRoot);
171
+ const cap = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 40;
172
+ if (!fs.existsSync(dir)) return [];
173
+ let names = [];
174
+ try {
175
+ names = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
176
+ } catch {
177
+ return [];
178
+ }
179
+
180
+ const rows = [];
181
+ for (const name of names) {
182
+ const filePath = path.join(dir, name);
183
+ let stat = null;
184
+ try {
185
+ stat = fs.statSync(filePath);
186
+ } catch {
187
+ continue;
188
+ }
189
+ let parsed = null;
190
+ try {
191
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
192
+ } catch {
193
+ parsed = null;
194
+ }
195
+ const sessionId = normalizeSessionId(
196
+ (parsed && parsed.sessionId) || name.replace(/\.json$/i, ""),
197
+ );
198
+ if (!sessionId) continue;
199
+ const updatedAt = String(
200
+ (parsed && (parsed.updatedAt || parsed.createdAt))
201
+ || (stat && stat.mtime && stat.mtime.toISOString())
202
+ || "",
203
+ ).trim();
204
+ const summary = String((parsed && parsed.summary) || "").trim().replace(/\s+/g, " ");
205
+ const model = String((parsed && parsed.model) || "").trim();
206
+ const bits = [
207
+ updatedAt ? updatedAt.slice(0, 19).replace("T", " ") : "",
208
+ model,
209
+ summary ? summary.slice(0, 48) : "",
210
+ ].filter(Boolean);
211
+ rows.push({
212
+ id: sessionId,
213
+ cmd: sessionId,
214
+ alias: sessionId,
215
+ desc: bits.join(" · "),
216
+ updatedAt,
217
+ mtimeMs: stat && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : 0,
218
+ });
219
+ }
220
+
221
+ rows.sort((left, right) => {
222
+ const byTime = (right.mtimeMs || 0) - (left.mtimeMs || 0);
223
+ if (byTime !== 0) return byTime;
224
+ return String(left.id).localeCompare(String(right.id));
225
+ });
226
+ return rows.slice(0, cap);
58
227
  }
59
228
 
60
229
  function getSessionFilePath(workspaceRoot = process.cwd(), sessionId = "") {
@@ -79,20 +248,19 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
79
248
  };
80
249
  }
81
250
 
251
+ const toWrite = { ...payload };
252
+ if (toWrite.version >= 2) {
253
+ syncTranscriptFromNlMessages(normalizedRoot, payload.sessionId, payload.nlMessages);
254
+ // nlMessages live in transcript.jsonl; keep session.json light
255
+ delete toWrite.nlMessages;
256
+ }
257
+
82
258
  try {
83
259
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
84
- // Write to a temp file and rename so a crash mid-write cannot leave a
85
- // corrupted session JSON behind.
260
+ fs.mkdirSync(getTranscriptsDir(normalizedRoot), { recursive: true });
86
261
  const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
87
- fs.writeFileSync(tmpFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
262
+ fs.writeFileSync(tmpFile, `${JSON.stringify(toWrite, null, 2)}\n`, "utf8");
88
263
  fs.renameSync(tmpFile, filePath);
89
- return {
90
- ok: true,
91
- error: "",
92
- sessionId: payload.sessionId,
93
- filePath,
94
- snapshot: payload,
95
- };
96
264
  } catch (err) {
97
265
  return {
98
266
  ok: false,
@@ -101,6 +269,26 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
101
269
  filePath,
102
270
  };
103
271
  }
272
+
273
+ // Artifact GC is throttled (default 2m) so long sessions do not accumulate
274
+ // unbounded tool result files between explicit maintenance runs.
275
+ let artifactGc = null;
276
+ try {
277
+ artifactGc = maybeGcSessionArtifacts(normalizedRoot, payload.sessionId, {
278
+ ...(snapshot.artifactGc && typeof snapshot.artifactGc === "object" ? snapshot.artifactGc : {}),
279
+ });
280
+ } catch {
281
+ artifactGc = { ok: false, error: "artifact gc failed", skipped: true };
282
+ }
283
+
284
+ return {
285
+ ok: true,
286
+ error: "",
287
+ sessionId: payload.sessionId,
288
+ filePath,
289
+ snapshot: payload,
290
+ artifactGc,
291
+ };
104
292
  }
105
293
 
106
294
  function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
@@ -130,12 +318,13 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
130
318
  try {
131
319
  const raw = fs.readFileSync(filePath, "utf8");
132
320
  const parsed = JSON.parse(raw);
133
- const snapshot = buildSessionSnapshot({
321
+ const snapshot = hydrateSessionFromDisk({
134
322
  ...parsed,
135
323
  sessionId: normalizedId,
136
324
  workspaceRoot: normalizedRoot,
137
325
  createdAt: parsed && parsed.createdAt ? parsed.createdAt : "",
138
- });
326
+ nlMessages: parsed && parsed.nlMessages ? parsed.nlMessages : [],
327
+ }, normalizedRoot);
139
328
  return {
140
329
  ok: true,
141
330
  error: "",
@@ -154,13 +343,36 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
154
343
  }
155
344
  }
156
345
 
346
+ function deleteSessionData(workspaceRoot = process.cwd(), sessionId = "") {
347
+ const normalizedId = normalizeSessionId(sessionId);
348
+ if (!normalizedId) return { ok: false, error: "invalid session id" };
349
+ const filePath = getSessionFilePath(workspaceRoot, normalizedId);
350
+ try {
351
+ if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
352
+ deleteTranscript(workspaceRoot, normalizedId);
353
+ deleteSessionArtifacts(workspaceRoot, normalizedId);
354
+ deleteSessionCommitLog(workspaceRoot, normalizedId);
355
+ return { ok: true, error: "" };
356
+ } catch (err) {
357
+ return {
358
+ ok: false,
359
+ error: err && err.message ? err.message : "failed to delete session data",
360
+ };
361
+ }
362
+ }
363
+
157
364
  module.exports = {
158
365
  getSessionsDir,
366
+ getTranscriptsDir,
367
+ getTranscriptFilePath,
159
368
  normalizeSessionId,
160
369
  createSessionId,
161
370
  resolveSessionId,
162
371
  buildSessionSnapshot,
372
+ hydrateSessionFromDisk,
163
373
  getSessionFilePath,
164
374
  saveSessionSnapshot,
165
375
  loadSessionSnapshot,
376
+ listSessionSummaries,
377
+ deleteSessionData,
166
378
  };
@@ -9,6 +9,12 @@ const {
9
9
  const {
10
10
  buildSkillInjections,
11
11
  } = require("./injection");
12
+ const {
13
+ buildSkillManifest,
14
+ buildSkillManifests,
15
+ renderSkillManifestSection,
16
+ renderActiveSkillBlock,
17
+ } = require("./manifest");
12
18
 
13
19
  function showSkill({ name = "", workspaceRoot = process.cwd(), asJson = false } = {}) {
14
20
  const outcome = listUcodeSkills({ workspaceRoot });
@@ -70,5 +76,9 @@ module.exports = {
70
76
  renderSkillsSection,
71
77
  formatSkillsList,
72
78
  buildSkillInjections,
79
+ buildSkillManifest,
80
+ buildSkillManifests,
81
+ renderSkillManifestSection,
82
+ renderActiveSkillBlock,
73
83
  showSkill,
74
84
  };
@@ -5,6 +5,10 @@ const {
5
5
  findSkillsByName,
6
6
  listUcodeSkills,
7
7
  } = require("./loader");
8
+ const {
9
+ buildSkillManifest,
10
+ renderActiveSkillBlock,
11
+ } = require("./manifest");
8
12
 
9
13
  function markdownSkillLinks(prompt = "") {
10
14
  const text = String(prompt || "");
@@ -71,9 +75,41 @@ function sanitizeSkillContent(content = "") {
71
75
  return text;
72
76
  }
73
77
 
74
- function readSkillBlock(skill) {
78
+ function readSkillBlock(skill, options = {}) {
75
79
  const content = sanitizeSkillContent(fs.readFileSync(skill.path, "utf8"));
76
- return `<skill>\n<name>${skill.name}</name>\n<path>${String(skill.path).replace(/\\/g, "/")}</path>\n${content}\n</skill>`;
80
+ const useActive = options.useActiveSkillTag === true;
81
+ const tag = useActive ? "active_skill" : "skill";
82
+ const bodyArtifactId = String(options.bodyArtifactId || "").trim();
83
+ const header = [
84
+ `<${tag}>`,
85
+ `<name>${skill.name}</name>`,
86
+ `<path>${String(skill.path).replace(/\\/g, "/")}</path>`,
87
+ bodyArtifactId ? `<bodyArtifactId>${bodyArtifactId}</bodyArtifactId>` : "",
88
+ ].filter(Boolean).join("\n");
89
+ return `${header}\n${content}\n</${tag}>`;
90
+ }
91
+
92
+ function persistSkillBodyArtifact(skill = {}, content = "", options = {}) {
93
+ const workspaceRoot = options.workspaceRoot || process.cwd();
94
+ const sessionId = String(options.sessionId || "").trim();
95
+ if (!sessionId) return "";
96
+ try {
97
+ const { saveArtifact } = require("../context/artifacts");
98
+ const saved = saveArtifact(workspaceRoot, sessionId, {
99
+ type: "skill_body",
100
+ tool: "skill",
101
+ source: skill.path || "",
102
+ args: { skill: skill.name || "" },
103
+ raw: { ok: true, path: skill.path || "", content: String(content || "") },
104
+ summary: `skill body ${skill.name || ""}`,
105
+ createdBy: "skill_injection",
106
+ });
107
+ return saved && saved.artifact && saved.artifact.artifactId
108
+ ? saved.artifact.artifactId
109
+ : "";
110
+ } catch {
111
+ return "";
112
+ }
77
113
  }
78
114
 
79
115
  function buildSkillInjections({
@@ -81,6 +117,9 @@ function buildSkillInjections({
81
117
  workspaceRoot = process.cwd(),
82
118
  skillsOutcome = null,
83
119
  loadSkills = listUcodeSkills,
120
+ sessionId = "",
121
+ persistBodies = false,
122
+ useActiveSkillTag = false,
84
123
  } = {}) {
85
124
  const outcome = skillsOutcome || loadSkills({ workspaceRoot });
86
125
  const skills = Array.isArray(outcome.skills) ? outcome.skills : [];
@@ -111,9 +150,28 @@ function buildSkillInjections({
111
150
  }
112
151
 
113
152
  const blocks = [];
153
+ const manifests = [];
154
+ const activeSkills = [];
114
155
  for (const skill of selected.values()) {
115
156
  try {
116
- blocks.push(readSkillBlock(skill));
157
+ const rawContent = fs.readFileSync(skill.path, "utf8");
158
+ const content = sanitizeSkillContent(rawContent);
159
+ let bodyArtifactId = "";
160
+ if (persistBodies && sessionId) {
161
+ bodyArtifactId = persistSkillBodyArtifact(skill, content, { workspaceRoot, sessionId });
162
+ }
163
+ const manifest = buildSkillManifest(skill, { bodyArtifactId });
164
+ manifests.push(manifest);
165
+ activeSkills.push({
166
+ name: skill.name,
167
+ path: skill.path,
168
+ bodyArtifactId,
169
+ });
170
+ if (useActiveSkillTag) {
171
+ blocks.push(renderActiveSkillBlock(manifest, content));
172
+ } else {
173
+ blocks.push(readSkillBlock(skill, { bodyArtifactId, useActiveSkillTag: false }));
174
+ }
117
175
  } catch (err) {
118
176
  warnings.push(`failed to read skill ${skill.path}: ${err && err.message ? err.message : "read failed"}`);
119
177
  }
@@ -121,6 +179,8 @@ function buildSkillInjections({
121
179
 
122
180
  return {
123
181
  blocks,
182
+ manifests,
183
+ activeSkills,
124
184
  warnings,
125
185
  skills,
126
186
  errors: Array.isArray(outcome.errors) ? outcome.errors : [],
@@ -132,5 +192,7 @@ module.exports = {
132
192
  markdownSkillLinks,
133
193
  resolveSkillLinkTarget,
134
194
  findSkillByPath,
195
+ readSkillBlock,
196
+ persistSkillBodyArtifact,
135
197
  buildSkillInjections,
136
198
  };
@@ -98,11 +98,32 @@ function parseSkillFile(filePath, rootInfo = {}) {
98
98
  || data.shortDescription
99
99
  || ""
100
100
  ).trim();
101
+ const workflowSummary = String(
102
+ data.workflowSummary
103
+ || data["workflow-summary"]
104
+ || metadata.workflowSummary
105
+ || metadata["workflow-summary"]
106
+ || shortDescription
107
+ || ""
108
+ ).trim();
109
+ const triggersRaw = data.triggers != null
110
+ ? data.triggers
111
+ : (data.trigger != null
112
+ ? data.trigger
113
+ : (metadata.triggers != null ? metadata.triggers : metadata.trigger));
114
+ let triggers = [];
115
+ if (Array.isArray(triggersRaw)) {
116
+ triggers = triggersRaw.map((item) => String(item || "").trim()).filter(Boolean);
117
+ } else if (typeof triggersRaw === "string" && triggersRaw.trim()) {
118
+ triggers = triggersRaw.split(/[,|]/).map((item) => item.trim()).filter(Boolean);
119
+ }
101
120
 
102
121
  return {
103
122
  name,
104
123
  description,
105
124
  shortDescription,
125
+ workflowSummary,
126
+ triggers,
106
127
  path: skillPath,
107
128
  dir,
108
129
  scope: rootInfo.scope || "repo",
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ function asStringList(value) {
4
+ if (Array.isArray(value)) {
5
+ return value.map((item) => String(item || "").trim()).filter(Boolean);
6
+ }
7
+ if (typeof value === "string" && value.trim()) {
8
+ return value.split(/[,|]/).map((item) => item.trim()).filter(Boolean);
9
+ }
10
+ return [];
11
+ }
12
+
13
+ function buildSkillManifest(skill = {}, options = {}) {
14
+ const source = skill && typeof skill === "object" ? skill : {};
15
+ const bodyArtifactId = String(options.bodyArtifactId || source.bodyArtifactId || "").trim();
16
+ const triggers = Array.isArray(source.triggers) && source.triggers.length > 0
17
+ ? source.triggers.map(String).filter(Boolean)
18
+ : asStringList(source.trigger);
19
+ const workflowSummary = String(
20
+ source.workflowSummary
21
+ || source.shortDescription
22
+ || source.description
23
+ || "",
24
+ ).trim().slice(0, 400);
25
+
26
+ return {
27
+ name: String(source.name || "").trim(),
28
+ description: String(source.description || "").trim(),
29
+ shortDescription: String(source.shortDescription || "").trim(),
30
+ triggers,
31
+ workflowSummary,
32
+ path: String(source.path || "").replace(/\\/g, "/"),
33
+ scope: String(source.scope || "").trim(),
34
+ bodyArtifactId,
35
+ };
36
+ }
37
+
38
+ function buildSkillManifests(skills = [], options = {}) {
39
+ return (Array.isArray(skills) ? skills : [])
40
+ .filter((skill) => skill && skill.enabled !== false)
41
+ .map((skill) => buildSkillManifest(skill, options));
42
+ }
43
+
44
+ function renderSkillManifestSection(manifests = []) {
45
+ const list = Array.isArray(manifests) ? manifests.filter((m) => m && m.name) : [];
46
+ if (list.length === 0) return "";
47
+
48
+ const lines = [
49
+ "## Skill Manifests",
50
+ "Lightweight skill cards for selection. Full skill body loads only when a skill is explicitly activated.",
51
+ ];
52
+ for (const manifest of list) {
53
+ lines.push(`### ${manifest.name}`);
54
+ if (manifest.workflowSummary) lines.push(`- Workflow: ${manifest.workflowSummary}`);
55
+ if (manifest.triggers && manifest.triggers.length > 0) {
56
+ lines.push(`- Triggers: ${manifest.triggers.join(", ")}`);
57
+ }
58
+ if (manifest.bodyArtifactId) {
59
+ lines.push(`- Body artifact: artifact://${manifest.bodyArtifactId}`);
60
+ } else if (manifest.path) {
61
+ lines.push(`- Path: ${manifest.path}`);
62
+ }
63
+ }
64
+ return lines.join("\n");
65
+ }
66
+
67
+ function renderActiveSkillBlock(manifest = {}, body = "") {
68
+ const name = String(manifest.name || "").trim() || "skill";
69
+ const pathText = String(manifest.path || "").replace(/\\/g, "/");
70
+ const bodyArtifactId = String(manifest.bodyArtifactId || "").trim();
71
+ const header = [
72
+ `<active_skill>`,
73
+ `<name>${name}</name>`,
74
+ pathText ? `<path>${pathText}</path>` : "",
75
+ bodyArtifactId ? `<bodyArtifactId>${bodyArtifactId}</bodyArtifactId>` : "",
76
+ manifest.workflowSummary ? `<workflowSummary>${manifest.workflowSummary}</workflowSummary>` : "",
77
+ ].filter(Boolean).join("\n");
78
+ return `${header}\n${String(body || "").trim()}\n</active_skill>`;
79
+ }
80
+
81
+ module.exports = {
82
+ asStringList,
83
+ buildSkillManifest,
84
+ buildSkillManifests,
85
+ renderSkillManifestSection,
86
+ renderActiveSkillBlock,
87
+ };
@@ -1,4 +1,9 @@
1
- function renderSkillsSection(skills = []) {
1
+ const {
2
+ buildSkillManifests,
3
+ renderSkillManifestSection,
4
+ } = require("./manifest");
5
+
6
+ function renderSkillsSection(skills = [], options = {}) {
2
7
  const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
3
8
  if (list.length === 0) return "";
4
9
 
@@ -19,6 +24,15 @@ function renderSkillsSection(skills = []) {
19
24
  lines.push("- When a skill is selected, read only the specific skill body and nearby referenced files needed for the task.");
20
25
  lines.push("- If a skill is ambiguous, missing, or unreadable, say so briefly and continue with the best fallback.");
21
26
 
27
+ if (options.includeManifests !== false) {
28
+ const manifests = buildSkillManifests(list);
29
+ const manifestSection = renderSkillManifestSection(manifests);
30
+ if (manifestSection) {
31
+ lines.push("");
32
+ lines.push(manifestSection);
33
+ }
34
+ }
35
+
22
36
  return lines.join("\n");
23
37
  }
24
38
 
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  const { runNativeAgentTask } = require("./nativeRunner");
7
+ const { isContextV2Enabled } = require("./context/featureFlag");
8
+ const { assembleModelContext, recordToolCallInSession } = require("./context/assembler");
7
9
 
8
10
  /**
9
11
  * Decompose a bug fix task into manageable steps
@@ -132,10 +134,14 @@ async function runDecomposedTask({
132
134
  systemPrompt,
133
135
  messages = [],
134
136
  sessionId = "",
137
+ state = null,
138
+ contextV2 = false,
139
+ systemBlocks = null,
135
140
  }) {
136
141
  const steps = decomposeBugFixTask(task);
137
142
  const results = [];
138
143
  let aborted = false;
144
+ const useV2 = Boolean(contextV2 || isContextV2Enabled());
139
145
 
140
146
  // Check if already aborted
141
147
  if (signal && signal.aborted) {
@@ -165,19 +171,43 @@ async function runDecomposedTask({
165
171
  try {
166
172
  // Run the step with its own timeout
167
173
  const stepPrompt = buildStepPrompt(step, results);
174
+ let stepMessages = messages;
175
+ let stepSystemPrompt = systemPrompt;
176
+ let stepSystemBlocks = systemBlocks;
177
+ if (useV2 && state) {
178
+ const assembled = assembleModelContext(state, {
179
+ workspaceRoot,
180
+ model,
181
+ provider,
182
+ turnDynamic: stepPrompt,
183
+ });
184
+ stepMessages = assembled.messages;
185
+ stepSystemPrompt = assembled.systemPrompt;
186
+ stepSystemBlocks = assembled.systemBlocks;
187
+ }
168
188
  const stepResult = await runNativeAgentTask({
169
189
  workspaceRoot,
170
190
  provider,
171
191
  model,
172
192
  prompt: stepPrompt,
173
- systemPrompt,
174
- messages,
193
+ systemPrompt: stepSystemPrompt,
194
+ systemBlocks: stepSystemBlocks,
195
+ messages: stepMessages,
175
196
  sessionId,
176
197
  timeoutMs: step.timeoutMs,
177
198
  onToolEvent,
178
199
  signal,
200
+ contextV2: useV2,
201
+ onArtifactPersisted: useV2 && state
202
+ ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
203
+ : null,
179
204
  });
180
205
 
206
+ if (useV2 && state && stepResult && Array.isArray(stepResult.messages)) {
207
+ const { syncMessagesToTranscript } = require("./context/assembler");
208
+ syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
209
+ }
210
+
181
211
  results.push({
182
212
  step: step.id,
183
213
  name: step.name,