u-foo 2.5.14 → 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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +517 -112
  4. package/src/code/commands.js +77 -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 +703 -0
  9. package/src/code/context/executionSegment.js +292 -0
  10. package/src/code/context/index.js +28 -0
  11. package/src/code/context/planGraph.js +1410 -0
  12. package/src/code/context/planGraphService.js +857 -0
  13. package/src/code/context/planMode.js +398 -0
  14. package/src/code/context/planProjection.js +432 -0
  15. package/src/code/context/projectSnapshot.js +201 -0
  16. package/src/code/context/promptLayers.js +175 -0
  17. package/src/code/context/reducers.js +328 -0
  18. package/src/code/context/stableJson.js +29 -0
  19. package/src/code/context/stateCommit.js +414 -0
  20. package/src/code/context/toolRuntime.js +172 -0
  21. package/src/code/context/transcript.js +182 -0
  22. package/src/code/context/transcriptSync.js +106 -0
  23. package/src/code/context/userInteraction.js +457 -0
  24. package/src/code/context/userNudge.js +116 -0
  25. package/src/code/context/workingSet.js +323 -0
  26. package/src/code/dispatch.js +20 -1
  27. package/src/code/index.js +8 -0
  28. package/src/code/modelCommand.js +87 -0
  29. package/src/code/nativeRunner.js +625 -34
  30. package/src/code/repl.js +196 -50
  31. package/src/code/runtime/agentWakeup.js +58 -0
  32. package/src/code/runtime/graphOwner.js +41 -0
  33. package/src/code/runtime/graphYieldRouter.js +42 -0
  34. package/src/code/runtime/index.js +15 -0
  35. package/src/code/runtime/loopMailbox.js +124 -0
  36. package/src/code/runtime/runtimeEvents.js +39 -0
  37. package/src/code/runtime/taskControl.js +565 -0
  38. package/src/code/runtime/taskFocus.js +165 -0
  39. package/src/code/runtime/taskLoop.js +383 -0
  40. package/src/code/runtime/taskRun.js +187 -0
  41. package/src/code/runtime/toolProvenance.js +70 -0
  42. package/src/code/runtime/workspaceLease.js +208 -0
  43. package/src/code/sessionStore.js +217 -15
  44. package/src/code/skills/index.js +10 -0
  45. package/src/code/skills/injection.js +66 -3
  46. package/src/code/skills/loader.js +21 -0
  47. package/src/code/skills/manifest.js +87 -0
  48. package/src/code/skills/render.js +15 -1
  49. package/src/code/taskDecomposer.js +56 -2
  50. package/src/code/tools/artifactRead.js +40 -0
  51. package/src/code/tools/askUser.js +11 -0
  52. package/src/code/tools/planGraph.js +29 -0
  53. package/src/code/tui.js +2 -0
  54. package/src/code/usageStore.js +15 -0
  55. package/src/ui/format/index.js +285 -45
  56. package/src/ui/format/markdownRenderer.js +436 -71
  57. package/src/ui/ink/ChatApp.js +39 -8
  58. package/src/ui/ink/UcodeApp.js +592 -43
  59. package/src/ui/ink/chatLogModel.js +102 -21
@@ -1,6 +1,21 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { randomUUID } = require("crypto");
4
+ const {
5
+ getTranscriptsDir,
6
+ getTranscriptFilePath,
7
+ loadTranscript,
8
+ migrateNlMessagesToTranscript,
9
+ appendTranscriptMessages,
10
+ transcriptEventsToMessages,
11
+ deleteTranscript,
12
+ } = require("./context/transcript");
13
+ const { deleteSessionArtifacts } = require("./context/artifacts");
14
+ const { deleteSessionCommitLog, maybeGcSessionArtifacts } = require("./context/artifactGc");
15
+ const { defaultContextPolicy } = require("./context/assembler");
16
+ const { emptyTaskContract } = require("./context/stateCommit");
17
+ const { emptyWorkingSet } = require("./context/workingSet");
18
+ const { emptyExecutionState } = require("./context/executionSegment");
4
19
 
5
20
  function getSessionsDir(workspaceRoot = process.cwd()) {
6
21
  const root = path.resolve(workspaceRoot || process.cwd());
@@ -40,21 +55,165 @@ function cloneMessages(value = []) {
40
55
  }
41
56
  }
42
57
 
58
+ function normalizeContextPolicy(value = {}) {
59
+ const defaults = defaultContextPolicy();
60
+ const source = value && typeof value === "object" ? value : {};
61
+ return {
62
+ ...defaults,
63
+ ...source,
64
+ transcriptWindow: Number.isFinite(source.transcriptWindow)
65
+ ? Math.max(1, Math.floor(source.transcriptWindow))
66
+ : defaults.transcriptWindow,
67
+ };
68
+ }
69
+
43
70
  function buildSessionSnapshot(input = {}) {
44
71
  const source = input && typeof input === "object" ? input : {};
45
72
  const sessionId = resolveSessionId(source.sessionId);
46
73
  const createdAt = String(source.createdAt || "").trim() || toIsoNow();
47
- return {
48
- version: 1,
74
+
75
+ const base = {
49
76
  sessionId,
50
77
  workspaceRoot: String(source.workspaceRoot || process.cwd()).trim() || process.cwd(),
51
78
  provider: String(source.provider || "").trim(),
52
79
  model: String(source.model || "").trim(),
53
80
  context: String(source.context || ""),
54
- nlMessages: cloneMessages(source.nlMessages),
55
81
  createdAt,
56
82
  updatedAt: toIsoNow(),
57
83
  };
84
+
85
+ return {
86
+ version: 2,
87
+ ...base,
88
+ transcript: {
89
+ path: getTranscriptFilePath(base.workspaceRoot, sessionId),
90
+ },
91
+ artifacts: {
92
+ indexPath: path.join(base.workspaceRoot, ".ufoo", "agent", "ucode", "artifacts", sessionId),
93
+ },
94
+ contextPolicy: normalizeContextPolicy(source.contextPolicy),
95
+ summary: String(source.summary || "").trim(),
96
+ projectSnapshot: source.projectSnapshot && typeof source.projectSnapshot === "object"
97
+ ? source.projectSnapshot
98
+ : null,
99
+ taskContract: source.taskContract && typeof source.taskContract === "object"
100
+ ? source.taskContract
101
+ : emptyTaskContract(),
102
+ stateEpoch: source.stateEpoch && typeof source.stateEpoch === "object"
103
+ ? source.stateEpoch
104
+ : null,
105
+ workingSet: Array.isArray(source.workingSet) ? source.workingSet : emptyWorkingSet(),
106
+ executionState: source.executionState && typeof source.executionState === "object"
107
+ ? source.executionState
108
+ : emptyExecutionState(),
109
+ activeSkills: Array.isArray(source.activeSkills) ? source.activeSkills : [],
110
+ toolCallsSinceCommit: Number.isFinite(source.toolCallsSinceCommit)
111
+ ? Math.max(0, Math.floor(source.toolCallsSinceCommit))
112
+ : 0,
113
+ // In-memory compatibility for callers still reading nlMessages
114
+ nlMessages: cloneMessages(source.nlMessages),
115
+ };
116
+ }
117
+
118
+ function hydrateSessionFromDisk(snapshot = {}, workspaceRoot = process.cwd()) {
119
+ const payload = buildSessionSnapshot({
120
+ ...snapshot,
121
+ workspaceRoot: workspaceRoot || snapshot.workspaceRoot,
122
+ });
123
+ if (payload.version < 2) return payload;
124
+
125
+ const sessionId = payload.sessionId;
126
+ const transcript = loadTranscript(workspaceRoot, sessionId);
127
+ if (transcript.events.length > 0) {
128
+ const { transcriptEventsToMessages } = require("./context/transcript");
129
+ payload.nlMessages = transcriptEventsToMessages(transcript.events);
130
+ return payload;
131
+ }
132
+
133
+ if (Array.isArray(snapshot.nlMessages) && snapshot.nlMessages.length > 0) {
134
+ migrateNlMessagesToTranscript(workspaceRoot, sessionId, snapshot.nlMessages);
135
+ const reloaded = loadTranscript(workspaceRoot, sessionId);
136
+ const { transcriptEventsToMessages } = require("./context/transcript");
137
+ payload.nlMessages = transcriptEventsToMessages(reloaded.events);
138
+ }
139
+
140
+ return payload;
141
+ }
142
+
143
+ function syncTranscriptFromNlMessages(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
144
+ const messages = Array.isArray(nlMessages) ? nlMessages : [];
145
+ if (!sessionId || messages.length === 0) return;
146
+ const existing = loadTranscript(workspaceRoot, sessionId);
147
+ if (existing.events.length === 0) {
148
+ migrateNlMessagesToTranscript(workspaceRoot, sessionId, messages);
149
+ return;
150
+ }
151
+ const { matchTranscriptBaseline } = require("./context/assembler");
152
+ const priorMessages = transcriptEventsToMessages(existing.events);
153
+ const baseline = matchTranscriptBaseline(priorMessages, messages);
154
+ if (messages.length > baseline) {
155
+ appendTranscriptMessages(workspaceRoot, sessionId, messages.slice(baseline));
156
+ }
157
+ }
158
+
159
+ function listSessionSummaries(workspaceRoot = process.cwd(), { limit = 40 } = {}) {
160
+ const dir = getSessionsDir(workspaceRoot);
161
+ const cap = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 40;
162
+ if (!fs.existsSync(dir)) return [];
163
+ let names = [];
164
+ try {
165
+ names = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
166
+ } catch {
167
+ return [];
168
+ }
169
+
170
+ const rows = [];
171
+ for (const name of names) {
172
+ const filePath = path.join(dir, name);
173
+ let stat = null;
174
+ try {
175
+ stat = fs.statSync(filePath);
176
+ } catch {
177
+ continue;
178
+ }
179
+ let parsed = null;
180
+ try {
181
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
182
+ } catch {
183
+ parsed = null;
184
+ }
185
+ const sessionId = normalizeSessionId(
186
+ (parsed && parsed.sessionId) || name.replace(/\.json$/i, ""),
187
+ );
188
+ if (!sessionId) continue;
189
+ const updatedAt = String(
190
+ (parsed && (parsed.updatedAt || parsed.createdAt))
191
+ || (stat && stat.mtime && stat.mtime.toISOString())
192
+ || "",
193
+ ).trim();
194
+ const summary = String((parsed && parsed.summary) || "").trim().replace(/\s+/g, " ");
195
+ const model = String((parsed && parsed.model) || "").trim();
196
+ const bits = [
197
+ updatedAt ? updatedAt.slice(0, 19).replace("T", " ") : "",
198
+ model,
199
+ summary ? summary.slice(0, 48) : "",
200
+ ].filter(Boolean);
201
+ rows.push({
202
+ id: sessionId,
203
+ cmd: sessionId,
204
+ alias: sessionId,
205
+ desc: bits.join(" · "),
206
+ updatedAt,
207
+ mtimeMs: stat && Number.isFinite(stat.mtimeMs) ? stat.mtimeMs : 0,
208
+ });
209
+ }
210
+
211
+ rows.sort((left, right) => {
212
+ const byTime = (right.mtimeMs || 0) - (left.mtimeMs || 0);
213
+ if (byTime !== 0) return byTime;
214
+ return String(left.id).localeCompare(String(right.id));
215
+ });
216
+ return rows.slice(0, cap);
58
217
  }
59
218
 
60
219
  function getSessionFilePath(workspaceRoot = process.cwd(), sessionId = "") {
@@ -79,20 +238,19 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
79
238
  };
80
239
  }
81
240
 
241
+ const toWrite = { ...payload };
242
+ if (toWrite.version >= 2) {
243
+ syncTranscriptFromNlMessages(normalizedRoot, payload.sessionId, payload.nlMessages);
244
+ // nlMessages live in transcript.jsonl; keep session.json light
245
+ delete toWrite.nlMessages;
246
+ }
247
+
82
248
  try {
83
249
  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.
250
+ fs.mkdirSync(getTranscriptsDir(normalizedRoot), { recursive: true });
86
251
  const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
87
- fs.writeFileSync(tmpFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
252
+ fs.writeFileSync(tmpFile, `${JSON.stringify(toWrite, null, 2)}\n`, "utf8");
88
253
  fs.renameSync(tmpFile, filePath);
89
- return {
90
- ok: true,
91
- error: "",
92
- sessionId: payload.sessionId,
93
- filePath,
94
- snapshot: payload,
95
- };
96
254
  } catch (err) {
97
255
  return {
98
256
  ok: false,
@@ -101,6 +259,26 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
101
259
  filePath,
102
260
  };
103
261
  }
262
+
263
+ // Artifact GC is throttled (default 2m) so long sessions do not accumulate
264
+ // unbounded tool result files between explicit maintenance runs.
265
+ let artifactGc = null;
266
+ try {
267
+ artifactGc = maybeGcSessionArtifacts(normalizedRoot, payload.sessionId, {
268
+ ...(snapshot.artifactGc && typeof snapshot.artifactGc === "object" ? snapshot.artifactGc : {}),
269
+ });
270
+ } catch {
271
+ artifactGc = { ok: false, error: "artifact gc failed", skipped: true };
272
+ }
273
+
274
+ return {
275
+ ok: true,
276
+ error: "",
277
+ sessionId: payload.sessionId,
278
+ filePath,
279
+ snapshot: payload,
280
+ artifactGc,
281
+ };
104
282
  }
105
283
 
106
284
  function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
@@ -130,12 +308,13 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
130
308
  try {
131
309
  const raw = fs.readFileSync(filePath, "utf8");
132
310
  const parsed = JSON.parse(raw);
133
- const snapshot = buildSessionSnapshot({
311
+ const snapshot = hydrateSessionFromDisk({
134
312
  ...parsed,
135
313
  sessionId: normalizedId,
136
314
  workspaceRoot: normalizedRoot,
137
315
  createdAt: parsed && parsed.createdAt ? parsed.createdAt : "",
138
- });
316
+ nlMessages: parsed && parsed.nlMessages ? parsed.nlMessages : [],
317
+ }, normalizedRoot);
139
318
  return {
140
319
  ok: true,
141
320
  error: "",
@@ -154,13 +333,36 @@ function loadSessionSnapshot(workspaceRoot = process.cwd(), sessionId = "") {
154
333
  }
155
334
  }
156
335
 
336
+ function deleteSessionData(workspaceRoot = process.cwd(), sessionId = "") {
337
+ const normalizedId = normalizeSessionId(sessionId);
338
+ if (!normalizedId) return { ok: false, error: "invalid session id" };
339
+ const filePath = getSessionFilePath(workspaceRoot, normalizedId);
340
+ try {
341
+ if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
342
+ deleteTranscript(workspaceRoot, normalizedId);
343
+ deleteSessionArtifacts(workspaceRoot, normalizedId);
344
+ deleteSessionCommitLog(workspaceRoot, normalizedId);
345
+ return { ok: true, error: "" };
346
+ } catch (err) {
347
+ return {
348
+ ok: false,
349
+ error: err && err.message ? err.message : "failed to delete session data",
350
+ };
351
+ }
352
+ }
353
+
157
354
  module.exports = {
158
355
  getSessionsDir,
356
+ getTranscriptsDir,
357
+ getTranscriptFilePath,
159
358
  normalizeSessionId,
160
359
  createSessionId,
161
360
  resolveSessionId,
162
361
  buildSessionSnapshot,
362
+ hydrateSessionFromDisk,
163
363
  getSessionFilePath,
164
364
  saveSessionSnapshot,
165
365
  loadSessionSnapshot,
366
+ listSessionSummaries,
367
+ deleteSessionData,
166
368
  };
@@ -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,8 @@ module.exports = {
132
192
  markdownSkillLinks,
133
193
  resolveSkillLinkTarget,
134
194
  findSkillByPath,
195
+ readSkillBlock,
196
+ persistSkillBodyArtifact,
135
197
  buildSkillInjections,
198
+ sanitizeSkillContent,
136
199
  };
@@ -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,34 @@
4
4
  */
5
5
 
6
6
  const { runNativeAgentTask } = require("./nativeRunner");
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
+ }
7
35
 
8
36
  /**
9
37
  * Decompose a bug fix task into manageable steps
@@ -132,6 +160,8 @@ async function runDecomposedTask({
132
160
  systemPrompt,
133
161
  messages = [],
134
162
  sessionId = "",
163
+ state = null,
164
+ systemBlocks = null,
135
165
  }) {
136
166
  const steps = decomposeBugFixTask(task);
137
167
  const results = [];
@@ -165,19 +195,43 @@ async function runDecomposedTask({
165
195
  try {
166
196
  // Run the step with its own timeout
167
197
  const stepPrompt = buildStepPrompt(step, results);
198
+ let stepMessages = messages;
199
+ let stepSystemPrompt = systemPrompt;
200
+ let stepSystemBlocks = systemBlocks;
201
+ if (state) {
202
+ const skillBodies = renderActiveSkillBodiesFromState(state);
203
+ const assembled = assembleModelContext(state, {
204
+ workspaceRoot,
205
+ model,
206
+ provider,
207
+ turnDynamic: [...skillBodies, stepPrompt].filter(Boolean).join("\n\n"),
208
+ });
209
+ stepMessages = assembled.messages;
210
+ stepSystemPrompt = assembled.systemPrompt;
211
+ stepSystemBlocks = assembled.systemBlocks;
212
+ }
168
213
  const stepResult = await runNativeAgentTask({
169
214
  workspaceRoot,
170
215
  provider,
171
216
  model,
172
217
  prompt: stepPrompt,
173
- systemPrompt,
174
- messages,
218
+ systemPrompt: stepSystemPrompt,
219
+ systemBlocks: stepSystemBlocks,
220
+ messages: stepMessages,
175
221
  sessionId,
176
222
  timeoutMs: step.timeoutMs,
177
223
  onToolEvent,
178
224
  signal,
225
+ onArtifactPersisted: state
226
+ ? (persisted) => recordToolCallInSession(state, persisted, workspaceRoot)
227
+ : null,
179
228
  });
180
229
 
230
+ if (state && stepResult && Array.isArray(stepResult.messages)) {
231
+ const { syncMessagesToTranscript } = require("./context/assembler");
232
+ syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
233
+ }
234
+
181
235
  results.push({
182
236
  step: step.id,
183
237
  name: step.name,