@xn-intenton-z2a/agentic-lib 7.2.5 → 7.2.7

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 (34) hide show
  1. package/.github/workflows/agentic-lib-init.yml +56 -0
  2. package/.github/workflows/agentic-lib-test.yml +7 -2
  3. package/.github/workflows/agentic-lib-workflow.yml +50 -3
  4. package/README.md +88 -17
  5. package/agentic-lib.toml +7 -0
  6. package/bin/agentic-lib.js +260 -496
  7. package/package.json +2 -1
  8. package/src/actions/agentic-step/config-loader.js +9 -0
  9. package/src/actions/agentic-step/index.js +104 -7
  10. package/src/actions/agentic-step/tasks/direct.js +435 -0
  11. package/src/actions/agentic-step/tasks/supervise.js +107 -180
  12. package/src/agents/agent-apply-fix.md +5 -2
  13. package/src/agents/agent-director.md +58 -0
  14. package/src/agents/agent-discovery.md +52 -0
  15. package/src/agents/agent-issue-resolution.md +18 -0
  16. package/src/agents/agent-iterate.md +45 -0
  17. package/src/agents/agent-supervisor.md +22 -50
  18. package/src/copilot/agents.js +39 -0
  19. package/src/copilot/config.js +308 -0
  20. package/src/copilot/context.js +318 -0
  21. package/src/copilot/hybrid-session.js +330 -0
  22. package/src/copilot/logger.js +43 -0
  23. package/src/copilot/sdk.js +36 -0
  24. package/src/copilot/session.js +372 -0
  25. package/src/copilot/tasks/fix-code.js +73 -0
  26. package/src/copilot/tasks/maintain-features.js +61 -0
  27. package/src/copilot/tasks/maintain-library.js +66 -0
  28. package/src/copilot/tasks/transform.js +120 -0
  29. package/src/copilot/tools.js +141 -0
  30. package/src/mcp/server.js +43 -25
  31. package/src/seeds/zero-README.md +31 -0
  32. package/src/seeds/zero-behaviour.test.js +12 -4
  33. package/src/seeds/zero-package.json +1 -1
  34. package/src/seeds/zero-playwright.config.js +1 -0
@@ -51,7 +51,9 @@ Tasks (run Copilot SDK transformations):
51
51
  fix-code Fix failing tests
52
52
 
53
53
  Iterator:
54
- iterate Run N cycles of maintain transform fix with budget tracking
54
+ iterate Single Copilot SDK session reads, writes, tests, iterates autonomously
55
+ iterate --here Discover the project and generate a MISSION.md, then iterate
56
+ iterate --list-missions List available built-in mission seeds
55
57
 
56
58
  MCP Server:
57
59
  mcp Start MCP server (for Claude Code, Cursor, etc.)
@@ -62,17 +64,28 @@ Options:
62
64
  --dry-run Show what would be done without making changes
63
65
  --target <path> Target repository (default: current directory)
64
66
  --mission <name> Mission seed name (default: hamming-distance) [purge only]
67
+ --mission-file <path> Use a custom mission file instead of a built-in seed
68
+ --list-missions List available built-in mission seeds
69
+ --here Discover the project and generate a MISSION.md, then iterate
70
+ --agent <name> Use a specific agent prompt (e.g. agent-iterate, agent-discovery)
65
71
  --model <name> Copilot SDK model (default: claude-sonnet-4)
66
- --cycles <N> Max iteration cycles (default: from transformation-budget)
67
- --steps <list> Steps per cycle: maintain-features,transform,fix-code
72
+ --timeout <ms> Session timeout in milliseconds (default: 600000)
73
+ --issue <N> GitHub issue number (fetched via gh CLI for context)
74
+ --pr <N> GitHub PR number (fetched via gh CLI for context)
75
+ --discussion <url> GitHub Discussion URL (fetched via gh CLI for context)
68
76
 
69
77
  Examples:
70
78
  npx @xn-intenton-z2a/agentic-lib init
71
79
  npx @xn-intenton-z2a/agentic-lib transform
72
80
  npx @xn-intenton-z2a/agentic-lib maintain-features --model gpt-5-mini
73
81
  npx @xn-intenton-z2a/agentic-lib reset --dry-run
74
- npx @xn-intenton-z2a/agentic-lib iterate --mission fizz-buzz --cycles 4
75
- npx @xn-intenton-z2a/agentic-lib iterate --steps transform,fix-code --cycles 2
82
+ npx @xn-intenton-z2a/agentic-lib iterate --mission fizz-buzz
83
+ npx @xn-intenton-z2a/agentic-lib iterate --mission-file ~/my-mission.md
84
+ npx @xn-intenton-z2a/agentic-lib iterate --here
85
+ npx @xn-intenton-z2a/agentic-lib iterate --list-missions
86
+ npx @xn-intenton-z2a/agentic-lib iterate --model gpt-5-mini --timeout 300000
87
+ npx @xn-intenton-z2a/agentic-lib iterate --agent agent-issue-resolution --issue 42
88
+ npx @xn-intenton-z2a/agentic-lib iterate --agent agent-apply-fix --pr 123
76
89
  `.trim();
77
90
 
78
91
  if (!command || command === "--help" || command === "-h" || command === "help") {
@@ -99,6 +112,20 @@ const cyclesIdx = flags.indexOf("--cycles");
99
112
  const cycles = cyclesIdx >= 0 ? parseInt(flags[cyclesIdx + 1], 10) : 0;
100
113
  const stepsIdx = flags.indexOf("--steps");
101
114
  const stepsFlag = stepsIdx >= 0 ? flags[stepsIdx + 1] : "";
115
+ const timeoutIdx = flags.indexOf("--timeout");
116
+ const timeoutMs = timeoutIdx >= 0 ? parseInt(flags[timeoutIdx + 1], 10) : 600000;
117
+ const listMissions = flags.includes("--list-missions");
118
+ const missionFileIdx = flags.indexOf("--mission-file");
119
+ const missionFile = missionFileIdx >= 0 ? resolve(flags[missionFileIdx + 1]) : "";
120
+ const hereMode = flags.includes("--here");
121
+ const agentIdx = flags.indexOf("--agent");
122
+ const agentFlag = agentIdx >= 0 ? flags[agentIdx + 1] : "";
123
+ const issueIdx = flags.indexOf("--issue");
124
+ const issueNumber = issueIdx >= 0 ? parseInt(flags[issueIdx + 1], 10) : 0;
125
+ const prIdx = flags.indexOf("--pr");
126
+ const prNumber = prIdx >= 0 ? parseInt(flags[prIdx + 1], 10) : 0;
127
+ const discussionIdx = flags.indexOf("--discussion");
128
+ const discussionUrl = discussionIdx >= 0 ? flags[discussionIdx + 1] : "";
102
129
 
103
130
  // ─── Task Commands ───────────────────────────────────────────────────
104
131
 
@@ -137,51 +164,203 @@ runInit();
137
164
  // ─── Iterator ────────────────────────────────────────────────────────
138
165
 
139
166
  async function runIterate() {
140
- const { runIterationLoop, formatIterationResults } = await import("../src/iterate.js");
167
+ // --list-missions: show available seeds and exit
168
+ if (listMissions) {
169
+ const missionsDir = resolve(srcDir, "seeds/missions");
170
+ const files = readdirSync(missionsDir).filter((f) => f.endsWith(".md")).sort();
171
+ console.log("\nAvailable missions:\n");
172
+ for (const f of files) {
173
+ const name = f.replace(".md", "");
174
+ const content = readFileSync(resolve(missionsDir, f), "utf8");
175
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#")) || "";
176
+ console.log(` ${name.padEnd(22)} ${firstLine.trim()}`);
177
+ }
178
+ console.log(`\nUsage: npx @xn-intenton-z2a/agentic-lib iterate --mission <name>`);
179
+ console.log(` npx @xn-intenton-z2a/agentic-lib iterate --mission-file <path>\n`);
180
+ return 0;
181
+ }
182
+
183
+ const { loadAgentPrompt } = await import("../src/copilot/agents.js");
184
+
185
+ // Resolve mission file path from config if available
186
+ let config;
187
+ try {
188
+ const { loadConfig } = await import("../src/copilot/config.js");
189
+ config = loadConfig(resolve(target, "agentic-lib.toml"));
190
+ } catch {
191
+ config = { tuning: {}, model: "gpt-5-mini", paths: {} };
192
+ }
193
+
194
+ // --here mode: run discovery agent to generate MISSION.md, then iterate
195
+ if (hereMode) {
196
+ // Determine mission output path: --mission-file > config paths.mission > default
197
+ const missionOutPath = missionFile
198
+ || (config.paths?.mission?.path ? resolve(target, config.paths.mission.path) : "")
199
+ || resolve(target, "MISSION.md");
200
+
201
+ const targetMission = resolve(target, "MISSION.md");
202
+ if (existsSync(targetMission)) {
203
+ console.log(`Using existing MISSION.md in ${target}`);
204
+ } else {
205
+ console.log("");
206
+ console.log("=== Discovery Phase ===");
207
+ console.log(`Target: ${target}`);
208
+ console.log(`Output: ${missionOutPath}`);
209
+ console.log("");
210
+
211
+ const discoveryPrompt = loadAgentPrompt("agent-discovery");
212
+ const { runHybridSession } = await import("../src/copilot/hybrid-session.js");
213
+ const effectiveModel = model || config.model || "gpt-5-mini";
214
+
215
+ const discoveryResult = await runHybridSession({
216
+ workspacePath: target,
217
+ model: effectiveModel,
218
+ tuning: config.tuning || {},
219
+ timeoutMs,
220
+ agentPrompt: discoveryPrompt,
221
+ userPrompt: [
222
+ "Explore this project directory and generate a MISSION.md file.",
223
+ `Write the mission file to: ${missionOutPath}`,
224
+ "",
225
+ "Examine the project structure, source code, tests, and documentation.",
226
+ "Then write a focused, achievable MISSION.md based on what you find.",
227
+ ].join("\n"),
228
+ });
229
+
230
+ console.log("");
231
+ console.log(`Discovery: ${discoveryResult.success ? "completed" : "finished"} (${discoveryResult.toolCalls} tool calls, ${discoveryResult.sessionTime}s)`);
232
+
233
+ if (!existsSync(missionOutPath)) {
234
+ console.error("Discovery did not produce a mission file. Cannot proceed.");
235
+ return 1;
236
+ }
237
+ console.log("");
238
+
239
+ // --here + --mission-file: stop after generating the mission file
240
+ if (missionFile) {
241
+ console.log(`Mission file written to: ${missionOutPath}`);
242
+ return 0;
243
+ }
244
+
245
+ // Copy to MISSION.md in target if discovery wrote elsewhere
246
+ if (missionOutPath !== targetMission && !existsSync(targetMission)) {
247
+ copyFileSync(missionOutPath, targetMission);
248
+ }
249
+ }
141
250
 
142
- // If --mission is specified, run init --purge first
143
- if (missionIdx >= 0) {
251
+ // Fall through to normal iterate with the generated mission
252
+ }
253
+
254
+ // Guard: require a mission source or existing MISSION.md in the target
255
+ if (!hereMode && !missionFile && missionIdx < 0) {
256
+ const targetMission = resolve(target, "MISSION.md");
257
+ if (!existsSync(targetMission)) {
258
+ console.error("No mission specified and no MISSION.md found in target directory.");
259
+ console.error("");
260
+ console.error("Usage:");
261
+ console.error(" iterate --mission <name> Use a built-in mission seed");
262
+ console.error(" iterate --mission-file <path> Use a custom mission file");
263
+ console.error(" iterate --here Discover and generate a mission");
264
+ console.error(" iterate --list-missions List available built-in missions");
265
+ console.error("");
266
+ console.error("Or run from a directory that already has a MISSION.md.");
267
+ return 1;
268
+ }
269
+ console.log(`Using existing MISSION.md in ${target}`);
270
+ }
271
+
272
+ // --mission-file: copy custom file as MISSION.md, run init --purge with empty mission
273
+ if (!hereMode && missionFile) {
274
+ if (!existsSync(missionFile)) {
275
+ console.error(`Mission file not found: ${missionFile}`);
276
+ return 1;
277
+ }
278
+ console.log(`\n=== Init with custom mission: ${missionFile} ===\n`);
279
+ execSync(
280
+ `node ${resolve(pkgRoot, "bin/agentic-lib.js")} init --purge --mission empty --target ${target}`,
281
+ { encoding: "utf8", timeout: 60000, stdio: "inherit" },
282
+ );
283
+ // Overwrite MISSION.md with the custom file
284
+ copyFileSync(missionFile, resolve(target, "MISSION.md"));
285
+ console.log(` COPY: ${missionFile} → MISSION.md\n`);
286
+ } else if (!hereMode && missionIdx >= 0) {
287
+ // --mission: use a built-in seed
144
288
  console.log(`\n=== Init with mission: ${mission} ===\n`);
145
- const initResult = execSync(
289
+ execSync(
146
290
  `node ${resolve(pkgRoot, "bin/agentic-lib.js")} init --purge --mission ${mission} --target ${target}`,
147
291
  { encoding: "utf8", timeout: 60000, stdio: "inherit" },
148
292
  );
149
293
  }
150
294
 
151
- const iterSteps = stepsFlag
152
- ? stepsFlag.split(",").map((s) => s.trim())
153
- : ["maintain-features", "transform", "fix-code"];
295
+ const { runHybridSession } = await import("../src/copilot/hybrid-session.js");
296
+ const { gatherLocalContext, gatherGitHubContext, buildUserPrompt } = await import("../src/copilot/context.js");
297
+
298
+ // Load agent prompt: --agent flag > default agent-iterate
299
+ const agentName = agentFlag || "agent-iterate";
300
+ let agentPrompt;
301
+ try {
302
+ agentPrompt = loadAgentPrompt(agentName);
303
+ console.log(`Agent: ${agentName}`);
304
+ } catch (err) {
305
+ console.error(`Failed to load agent: ${err.message}`);
306
+ return 1;
307
+ }
154
308
 
309
+ const effectiveModel = model || config.model || "gpt-5-mini";
155
310
  console.log("");
156
311
  console.log("=== agentic-lib iterate ===");
157
312
  console.log(`Target: ${target}`);
158
- console.log(`Model: ${model}`);
159
- console.log(`Cycles: ${cycles || "(from budget)"}`);
160
- console.log(`Steps: ${iterSteps.join(", ")}`);
161
- console.log(`Dry-run: ${dryRun}`);
313
+ console.log(`Model: ${effectiveModel}`);
314
+ if (issueNumber) console.log(`Issue: #${issueNumber}`);
315
+ if (prNumber) console.log(`PR: #${prNumber}`);
316
+ if (discussionUrl) console.log(`Discussion: ${discussionUrl}`);
162
317
  console.log("");
163
318
 
164
- const { results, totalCost, budget } = await runIterationLoop({
165
- targetPath: target,
166
- model,
167
- maxCycles: cycles,
168
- steps: iterSteps,
169
- dryRun,
170
- onCycleComplete: (record) => {
171
- if (record.stopped) return;
172
- const status = record.testsPassed ? "PASS" : "FAIL";
173
- console.log(
174
- ` Cycle ${record.cycle}: ${record.filesChanged} files changed, tests ${status}, cost ${record.totalCost}/${record.budget} (${record.elapsed}s)`,
175
- );
176
- },
319
+ // Gather context for the agent
320
+ const localContext = gatherLocalContext(target, config);
321
+
322
+ // Optionally gather GitHub context
323
+ let githubContext;
324
+ if (issueNumber || prNumber || discussionUrl) {
325
+ console.log("Fetching GitHub context...");
326
+ githubContext = gatherGitHubContext({
327
+ issueNumber: issueNumber || undefined,
328
+ prNumber: prNumber || undefined,
329
+ discussionUrl: discussionUrl || undefined,
330
+ workspacePath: target,
331
+ });
332
+ }
333
+
334
+ // Build context-aware user prompt
335
+ const userPrompt = buildUserPrompt(agentName, localContext, githubContext, { tuning: config.tuning });
336
+
337
+ const result = await runHybridSession({
338
+ workspacePath: target,
339
+ model: effectiveModel,
340
+ tuning: config.tuning || {},
341
+ timeoutMs,
342
+ agentPrompt,
343
+ userPrompt,
344
+ writablePaths: config.writablePaths?.length > 0 ? config.writablePaths : undefined,
177
345
  });
178
346
 
179
347
  console.log("");
180
- console.log(formatIterationResults(results, totalCost, budget));
348
+ console.log("=== Results ===");
349
+ console.log(`Success: ${result.success}`);
350
+ console.log(`Tests passed: ${result.testsPassed}`);
351
+ console.log(`Session time: ${result.sessionTime}s`);
352
+ console.log(`Total time: ${result.totalTime}s`);
353
+ console.log(`Tool calls: ${result.toolCalls}`);
354
+ console.log(`Test runs: ${result.testRuns}`);
355
+ console.log(`Files written: ${result.filesWritten}`);
356
+ console.log(`Tokens: ${result.tokensIn + result.tokensOut} (in=${result.tokensIn} out=${result.tokensOut})`);
357
+ console.log(`End reason: ${result.endReason}`);
358
+ if (result.narrative) console.log(`Narrative: ${result.narrative}`);
359
+ if (result.agentMessage) console.log(`Agent: ${result.agentMessage}`);
360
+ if (result.errors.length > 0) console.log(`Errors: ${result.errors.length}`);
181
361
  console.log("");
182
362
 
183
- const lastCycle = results.filter((r) => !r.stopped).slice(-1)[0];
184
- return lastCycle?.testsPassed ? 0 : 1;
363
+ return result.success ? 0 : 1;
185
364
  }
186
365
 
187
366
  // ─── Task Runner ─────────────────────────────────────────────────────
@@ -194,107 +373,68 @@ async function runTask(taskName) {
194
373
  console.log(`Dry-run: ${dryRun}`);
195
374
  console.log("");
196
375
 
197
- // Find the Copilot SDK
198
- const sdkLocations = [
199
- resolve(pkgRoot, "node_modules/@github/copilot-sdk/dist/index.js"),
200
- resolve(pkgRoot, "src/actions/agentic-step/node_modules/@github/copilot-sdk/dist/index.js"),
201
- resolve(target, ".github/agentic-lib/actions/agentic-step/node_modules/@github/copilot-sdk/dist/index.js"),
202
- ];
203
- const sdkPath = sdkLocations.find((p) => existsSync(p));
204
- if (!sdkPath) {
205
- console.error("ERROR: @github/copilot-sdk not found.");
206
- console.error("Run: cd .github/agentic-lib/actions/agentic-step && npm ci");
207
- return 1;
208
- }
209
- const { CopilotClient, approveAll, defineTool } = await import(sdkPath);
210
-
211
- // Load config
212
- const config = await loadTaskConfig();
213
- const writablePaths = getWritablePathsFromConfig(config);
214
- const readOnlyPaths = getReadOnlyPathsFromConfig(config);
376
+ // Load config from shared module
377
+ const { loadConfig } = await import("../src/copilot/config.js");
378
+ const config = loadConfig(resolve(target, "agentic-lib.toml"));
379
+ const effectiveModel = model || config.model;
215
380
 
216
- console.log(`[config] supervisor=${config.supervisor || "daily"}`);
217
- console.log(`[config] writable=${writablePaths.join(", ")}`);
381
+ console.log(`[config] supervisor=${config.supervisor}`);
382
+ console.log(`[config] writable=${config.writablePaths.join(", ")}`);
218
383
  console.log(`[config] test=${config.testScript}`);
219
384
  console.log("");
220
385
 
221
- // Build task-specific prompt
222
- const { systemMessage, prompt } = buildTaskPrompt(taskName, config, writablePaths, readOnlyPaths);
223
-
224
- if (!prompt) {
225
- return 0; // buildTaskPrompt already logged why
226
- }
227
-
228
- console.log(`[prompt] ${prompt.length} chars`);
229
- console.log("");
230
-
231
386
  if (dryRun) {
232
- console.log("=== DRY RUN — prompt constructed but not sent ===");
233
- console.log("");
234
- console.log(prompt);
387
+ console.log("=== DRY RUN — task would run but not sending to Copilot ===");
235
388
  return 0;
236
389
  }
237
390
 
238
- // Create tools
239
- const tools = createCliTools(writablePaths, defineTool);
240
-
241
- // Set up auth
242
- const copilotToken = process.env.COPILOT_GITHUB_TOKEN;
243
- if (!copilotToken) {
244
- console.error("ERROR: COPILOT_GITHUB_TOKEN is required. Set it in your environment.");
245
- return 1;
246
- }
247
- console.log("[auth] Using COPILOT_GITHUB_TOKEN");
248
- const clientOptions = {};
249
- const env = { ...process.env };
250
- env.GITHUB_TOKEN = copilotToken;
251
- env.GH_TOKEN = copilotToken;
252
- clientOptions.env = env;
253
-
254
- const client = new CopilotClient(clientOptions);
391
+ // Change to target directory so relative paths in config work
392
+ const originalCwd = process.cwd();
393
+ process.chdir(target);
255
394
 
256
395
  try {
257
- console.log("[copilot] Creating session...");
258
- const session = await client.createSession({
259
- model,
260
- systemMessage: { content: systemMessage },
261
- tools,
262
- onPermissionRequest: approveAll,
263
- workingDirectory: target,
264
- });
265
- console.log(`[copilot] Session: ${session.sessionId}`);
266
-
267
- // Verbose event logging
268
- session.on((event) => {
269
- const type = event?.type || "unknown";
270
- if (type === "assistant.message") {
271
- const preview = event?.data?.content?.substring(0, 120) || "";
272
- console.log(`[event] ${type}: ${preview}...`);
273
- } else if (type === "tool.call") {
274
- const name = event?.data?.name || "?";
275
- const args = JSON.stringify(event?.data?.arguments || {}).substring(0, 200);
276
- console.log(`[event] tool.call: ${name}(${args})`);
277
- } else if (type === "session.error") {
278
- console.error(`[event] ERROR: ${JSON.stringify(event?.data || event)}`);
279
- } else if (type !== "session.idle") {
280
- console.log(`[event] ${type}`);
396
+ const context = {
397
+ config,
398
+ writablePaths: config.writablePaths,
399
+ model: effectiveModel,
400
+ testCommand: config.testScript,
401
+ logger: { info: console.log, warning: console.warn, error: console.error, debug: () => {} },
402
+ };
403
+
404
+ let result;
405
+ switch (taskName) {
406
+ case "transform": {
407
+ const { transform } = await import("../src/copilot/tasks/transform.js");
408
+ result = await transform(context);
409
+ break;
281
410
  }
282
- });
283
-
284
- const startTime = Date.now();
285
- console.log("[copilot] Sending prompt...");
286
- const response = await session.sendAndWait({ prompt }, 300000);
287
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
288
-
289
- const content = response?.data?.content || "(no content)";
290
- const tokens = response?.data?.usage?.totalTokens || 0;
411
+ case "maintain-features": {
412
+ const { maintainFeatures } = await import("../src/copilot/tasks/maintain-features.js");
413
+ result = await maintainFeatures(context);
414
+ break;
415
+ }
416
+ case "maintain-library": {
417
+ const { maintainLibrary } = await import("../src/copilot/tasks/maintain-library.js");
418
+ result = await maintainLibrary(context);
419
+ break;
420
+ }
421
+ case "fix-code": {
422
+ const { fixCode } = await import("../src/copilot/tasks/fix-code.js");
423
+ result = await fixCode(context);
424
+ break;
425
+ }
426
+ default:
427
+ console.error(`Unknown task: ${taskName}`);
428
+ return 1;
429
+ }
291
430
 
292
431
  console.log("");
293
- console.log(`=== ${taskName} completed in ${elapsed}s (${tokens} tokens) ===`);
294
- console.log("");
295
- console.log(content);
432
+ console.log(`=== ${taskName} completed ===`);
433
+ console.log(`Outcome: ${result.outcome}`);
434
+ if (result.details) console.log(`Details: ${result.details}`);
435
+ if (result.tokensUsed) console.log(`Tokens: ${result.tokensUsed} (in=${result.inputTokens} out=${result.outputTokens})`);
436
+ if (result.narrative) console.log(`Narrative: ${result.narrative}`);
296
437
  console.log("");
297
-
298
438
  return 0;
299
439
  } catch (err) {
300
440
  console.error("");
@@ -303,386 +443,10 @@ async function runTask(taskName) {
303
443
  if (err.stack) console.error(err.stack);
304
444
  return 1;
305
445
  } finally {
306
- await client.stop();
307
- }
308
- }
309
-
310
- // ─── Task Config + Prompts ───────────────────────────────────────────
311
-
312
- async function loadTaskConfig() {
313
- const tomlPath = resolve(target, "agentic-lib.toml");
314
-
315
- if (!existsSync(tomlPath)) {
316
- throw new Error(`Config file not found: ${tomlPath}. Create agentic-lib.toml in the project root.`);
317
- }
318
-
319
- console.log(`[config] Loading ${tomlPath}`);
320
- const { parse } = await import("smol-toml");
321
- const toml = parse(readFileSync(tomlPath, "utf8"));
322
- return {
323
- missionPath: toml.paths?.mission || "MISSION.md",
324
- sourcePath: toml.paths?.source || "src/lib/",
325
- testsPath: toml.paths?.tests || "tests/unit/",
326
- featuresPath: toml.paths?.features || "features/",
327
- libraryPath: toml.paths?.docs || "library/",
328
- sourcesPath: toml.paths?.["library-sources"] || "SOURCES.md",
329
- examplesPath: toml.paths?.examples || "examples/",
330
- readmePath: toml.paths?.readme || "README.md",
331
- depsPath: toml.paths?.dependencies || "package.json",
332
- testScript: toml.execution?.test || "npm ci && npm test",
333
- featureLimit: toml.limits?.["max-feature-issues"] || 2,
334
- intentionPath: toml.bot?.["log-file"] || "intentïon.md",
335
- };
336
- }
337
-
338
- function getWritablePathsFromConfig(config) {
339
- return [
340
- config.sourcePath,
341
- config.testsPath,
342
- config.featuresPath,
343
- config.libraryPath,
344
- config.examplesPath,
345
- config.readmePath,
346
- config.depsPath,
347
- ].filter(Boolean);
348
- }
349
-
350
- function getReadOnlyPathsFromConfig(config) {
351
- return [config.missionPath, config.sourcesPath].filter(Boolean);
352
- }
353
-
354
- function readOptional(relPath) {
355
- try {
356
- return readFileSync(resolve(target, relPath), "utf8");
357
- } catch (err) {
358
- console.debug(`[readOptional] ${relPath}: ${err.message}`);
359
- return "";
446
+ process.chdir(originalCwd);
360
447
  }
361
448
  }
362
449
 
363
- function scanDir(relPath, extensions, opts = {}) {
364
- const { fileLimit = 10, contentLimit } = opts;
365
- const dir = resolve(target, relPath);
366
- const exts = Array.isArray(extensions) ? extensions : [extensions];
367
- if (!existsSync(dir)) return [];
368
- try {
369
- return readdirSync(dir, { recursive: true })
370
- .filter((f) => exts.some((ext) => String(f).endsWith(ext)))
371
- .slice(0, fileLimit)
372
- .map((f) => {
373
- try {
374
- const content = readFileSync(resolve(dir, String(f)), "utf8");
375
- return { name: String(f), content: contentLimit ? content.substring(0, contentLimit) : content };
376
- } catch (err) {
377
- console.debug(`[scanDir] ${dir}/${f}: ${err.message}`);
378
- return { name: String(f), content: "" };
379
- }
380
- });
381
- } catch (err) {
382
- console.debug(`[scanDir] ${dir}: ${err.message}`);
383
- return [];
384
- }
385
- }
386
-
387
- function formatPaths(writable, readOnly) {
388
- return [
389
- "## File Paths",
390
- "### Writable (you may modify these)",
391
- writable.length > 0 ? writable.map((p) => `- ${p}`).join("\n") : "- (none)",
392
- "",
393
- "### Read-Only (for context only, do NOT modify)",
394
- readOnly.length > 0 ? readOnly.map((p) => `- ${p}`).join("\n") : "- (none)",
395
- ].join("\n");
396
- }
397
-
398
- function buildTaskPrompt(taskName, config, writablePaths, readOnlyPaths) {
399
- const pathsSection = formatPaths(writablePaths, readOnlyPaths);
400
-
401
- switch (taskName) {
402
- case "transform":
403
- return buildTransformPrompt(config, pathsSection);
404
- case "maintain-features":
405
- return buildMaintainFeaturesPrompt(config, pathsSection);
406
- case "maintain-library":
407
- return buildMaintainLibraryPrompt(config, pathsSection);
408
- case "fix-code":
409
- return buildFixCodePrompt(config, pathsSection);
410
- default:
411
- console.error(`Unknown task: ${taskName}`);
412
- return { systemMessage: "", prompt: null };
413
- }
414
- }
415
-
416
- function buildTransformPrompt(config, pathsSection) {
417
- const mission = readOptional(config.missionPath);
418
- if (!mission) {
419
- console.error(`No mission file found at ${config.missionPath}`);
420
- return { systemMessage: "", prompt: null };
421
- }
422
- console.log(`[context] Mission: ${mission.substring(0, 80).trim()}...`);
423
-
424
- const features = scanDir(config.featuresPath, ".md");
425
- const sourceFiles = scanDir(config.sourcePath, [".js", ".ts"], { contentLimit: 2000 });
426
- console.log(`[context] Features: ${features.length}, Source files: ${sourceFiles.length}`);
427
-
428
- return {
429
- systemMessage:
430
- "You are an autonomous code transformation agent. Your goal is to advance the repository toward its mission by making the most impactful change possible in a single step.",
431
- prompt: [
432
- "## Instructions",
433
- "Transform the repository toward its mission by identifying the next best action.",
434
- "",
435
- "## Mission",
436
- mission,
437
- "",
438
- `## Current Features (${features.length})`,
439
- ...features.map((f) => `### ${f.name}\n${f.content.substring(0, 500)}`),
440
- "",
441
- `## Current Source Files (${sourceFiles.length})`,
442
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
443
- "",
444
- "## Output Artifacts",
445
- "If your changes produce output artifacts (plots, visualizations, data files, usage examples),",
446
- `save them to the \`${config.examplesPath || "examples/"}\` directory.`,
447
- "This directory is for demonstrating what the code can do.",
448
- "",
449
- "## Your Task",
450
- "Analyze the mission, features, and source code.",
451
- "Determine the single most impactful next step.",
452
- "Then implement that step by writing files.",
453
- "",
454
- pathsSection,
455
- "",
456
- "## Constraints",
457
- `- Run \`${config.testScript}\` to validate your changes`,
458
- ].join("\n"),
459
- };
460
- }
461
-
462
- function buildMaintainFeaturesPrompt(config, pathsSection) {
463
- const mission = readOptional(config.missionPath);
464
- if (!mission) {
465
- console.error(`No mission file found at ${config.missionPath}`);
466
- return { systemMessage: "", prompt: null };
467
- }
468
-
469
- const features = scanDir(config.featuresPath, ".md");
470
- const libraryDocs = scanDir(config.libraryPath, ".md", { contentLimit: 1000 });
471
- console.log(`[context] Mission loaded, features: ${features.length}, library: ${libraryDocs.length}`);
472
-
473
- return {
474
- systemMessage:
475
- "You are a feature lifecycle manager. Create, update, and prune feature specification files to keep the project focused on its mission.",
476
- prompt: [
477
- "## Instructions",
478
- "Maintain the feature set by creating, updating, or pruning features.",
479
- "",
480
- "## Mission",
481
- mission,
482
- "",
483
- `## Current Features (${features.length}/${config.featureLimit} max)`,
484
- ...features.map((f) => `### ${f.name}\n${f.content}`),
485
- "",
486
- libraryDocs.length > 0 ? `## Library Documents (${libraryDocs.length})` : "",
487
- ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
488
- "",
489
- "## Your Task",
490
- `1. Review each existing feature — if it is already implemented or irrelevant, delete it.`,
491
- `2. If there are fewer than ${config.featureLimit} features, create new features aligned with the mission.`,
492
- "3. Ensure each feature has clear, testable acceptance criteria.",
493
- "",
494
- pathsSection,
495
- "",
496
- "## Constraints",
497
- `- Maximum ${config.featureLimit} feature files`,
498
- "- Feature files must be markdown with a descriptive filename",
499
- ].join("\n"),
500
- };
501
- }
502
-
503
- function buildMaintainLibraryPrompt(config, pathsSection) {
504
- const sources = readOptional(config.sourcesPath);
505
- if (!sources.trim()) {
506
- console.log("No SOURCES.md or empty — nothing to maintain.");
507
- return { systemMessage: "", prompt: null };
508
- }
509
-
510
- const libraryDocs = scanDir(config.libraryPath, ".md", { contentLimit: 500 });
511
- console.log(`[context] Sources loaded, library: ${libraryDocs.length}`);
512
-
513
- return {
514
- systemMessage:
515
- "You are a knowledge librarian. Maintain a library of technical documents extracted from web sources.",
516
- prompt: [
517
- "## Instructions",
518
- "Maintain the library by updating documents from sources.",
519
- "",
520
- "## Sources",
521
- sources,
522
- "",
523
- `## Current Library Documents (${libraryDocs.length})`,
524
- ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
525
- "",
526
- "## Your Task",
527
- "1. Read each URL in SOURCES.md and extract technical content.",
528
- "2. Create or update library documents based on the source content.",
529
- "3. Remove library documents that no longer have corresponding sources.",
530
- "",
531
- pathsSection,
532
- ].join("\n"),
533
- };
534
- }
535
-
536
- function buildFixCodePrompt(config, pathsSection) {
537
- // Run tests and capture output
538
- console.log(`[fix-code] Running: ${config.testScript}`);
539
- let testOutput;
540
- try {
541
- testOutput = execSync(config.testScript, { cwd: target, encoding: "utf8", timeout: 120000 });
542
- console.log("[fix-code] Tests pass — nothing to fix.");
543
- return { systemMessage: "", prompt: null };
544
- } catch (err) {
545
- testOutput = `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`;
546
- console.log(`[fix-code] Tests failing — ${testOutput.length} chars of output`);
547
- }
548
-
549
- const sourceFiles = scanDir(config.sourcePath, [".js", ".ts"], { contentLimit: 2000 });
550
- const testFiles = scanDir(config.testsPath, [".js", ".ts", ".test.js"], { contentLimit: 2000 });
551
-
552
- return {
553
- systemMessage:
554
- "You are an autonomous coding agent fixing failing tests. Make minimal, targeted changes to fix the test failures.",
555
- prompt: [
556
- "## Instructions",
557
- "Fix the failing tests by modifying the source code.",
558
- "",
559
- "## Test Output (failing)",
560
- "```",
561
- testOutput.substring(0, 5000),
562
- "```",
563
- "",
564
- `## Source Files (${sourceFiles.length})`,
565
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
566
- "",
567
- `## Test Files (${testFiles.length})`,
568
- ...testFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
569
- "",
570
- pathsSection,
571
- "",
572
- "## Constraints",
573
- `- Run \`${config.testScript}\` to validate your fixes`,
574
- "- Make minimal changes to fix the failing tests",
575
- ].join("\n"),
576
- };
577
- }
578
-
579
- // ─── CLI Tools for Copilot SDK ───────────────────────────────────────
580
-
581
- function createCliTools(writablePaths, defineTool) {
582
- const readFile = defineTool("read_file", {
583
- description: "Read the contents of a file.",
584
- parameters: {
585
- type: "object",
586
- properties: { path: { type: "string", description: "File path to read" } },
587
- required: ["path"],
588
- },
589
- handler: ({ path }) => {
590
- const resolved = resolve(target, path);
591
- console.log(` [tool] read_file: ${resolved}`);
592
- if (!existsSync(resolved)) return { error: `File not found: ${resolved}` };
593
- try {
594
- return { content: readFileSync(resolved, "utf8") };
595
- } catch (err) {
596
- return { error: err.message };
597
- }
598
- },
599
- });
600
-
601
- const writeFile = defineTool("write_file", {
602
- description: "Write content to a file. Parent directories are created automatically.",
603
- parameters: {
604
- type: "object",
605
- properties: {
606
- path: { type: "string", description: "File path to write" },
607
- content: { type: "string", description: "Content to write" },
608
- },
609
- required: ["path", "content"],
610
- },
611
- handler: ({ path, content }) => {
612
- const resolved = resolve(target, path);
613
- const isWritable = writablePaths.some((wp) => path.startsWith(wp) || resolved.startsWith(resolve(target, wp)));
614
- console.log(` [tool] write_file: ${resolved} (${content.length} chars, writable=${isWritable})`);
615
- if (!isWritable && !dryRun) {
616
- return { error: `Path not writable: ${path}. Writable: ${writablePaths.join(", ")}` };
617
- }
618
- if (dryRun) {
619
- console.log(` [tool] DRY RUN — would write ${content.length} chars to ${resolved}`);
620
- return { success: true, dryRun: true };
621
- }
622
- try {
623
- const dir = dirname(resolved);
624
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
625
- writeFileSync(resolved, content, "utf8");
626
- return { success: true, path: resolved };
627
- } catch (err) {
628
- return { error: err.message };
629
- }
630
- },
631
- });
632
-
633
- const listFiles = defineTool("list_files", {
634
- description: "List files and directories at the given path.",
635
- parameters: {
636
- type: "object",
637
- properties: {
638
- path: { type: "string", description: "Directory path to list" },
639
- recursive: { type: "boolean", description: "List recursively" },
640
- },
641
- required: ["path"],
642
- },
643
- handler: ({ path, recursive }) => {
644
- const resolved = resolve(target, path);
645
- console.log(` [tool] list_files: ${resolved}`);
646
- if (!existsSync(resolved)) return { error: `Not found: ${resolved}` };
647
- try {
648
- const entries = readdirSync(resolved, { withFileTypes: true, recursive: !!recursive });
649
- return { files: entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name)) };
650
- } catch (err) {
651
- return { error: err.message };
652
- }
653
- },
654
- });
655
-
656
- const runCommand = defineTool("run_command", {
657
- description: "Run a shell command and return stdout/stderr.",
658
- parameters: {
659
- type: "object",
660
- properties: {
661
- command: { type: "string", description: "Shell command to execute" },
662
- cwd: { type: "string", description: "Working directory" },
663
- },
664
- required: ["command"],
665
- },
666
- handler: ({ command: cmd, cwd }) => {
667
- const workDir = cwd ? resolve(target, cwd) : target;
668
- console.log(` [tool] run_command: ${cmd} (cwd=${workDir})`);
669
- const blocked = /\bgit\s+(commit|push|add|reset|checkout|rebase|merge|stash)\b/;
670
- if (blocked.test(cmd)) {
671
- console.log(` [tool] BLOCKED git write command: ${cmd}`);
672
- return { error: "Git write commands are not allowed. Use read_file/write_file tools instead." };
673
- }
674
- try {
675
- const stdout = execSync(cmd, { cwd: workDir, encoding: "utf8", timeout: 120000 });
676
- return { stdout, exitCode: 0 };
677
- } catch (err) {
678
- return { stdout: err.stdout || "", stderr: err.stderr || "", exitCode: err.status || 1 };
679
- }
680
- },
681
- });
682
-
683
- return [readFile, writeFile, listFiles, runCommand];
684
- }
685
-
686
450
  // ─── Init Runner ─────────────────────────────────────────────────────
687
451
 
688
452
  function initTransformFile(src, dst, label) {