@xn-intenton-z2a/agentic-lib 7.1.61 → 7.1.62

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.
package/src/iterate.js ADDED
@@ -0,0 +1,285 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Copyright (C) 2025-2026 Polycode Limited
3
+ // src/iterate.js — Shared iteration loop for CLI and MCP server
4
+ //
5
+ // Runs N cycles of maintain → transform → fix, tracking transformation cost
6
+ // against a budget. Stops early on consecutive test passes, no-progress, or
7
+ // budget exhaustion.
8
+
9
+ import { existsSync, readFileSync, readdirSync } from "fs";
10
+ import { resolve, join, dirname } from "path";
11
+ import { execSync } from "child_process";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const binPath = resolve(__dirname, "../bin/agentic-lib.js");
16
+
17
+ /**
18
+ * Take a snapshot of all file contents in a directory (recursive).
19
+ * Returns an object mapping relative paths to file content strings.
20
+ */
21
+ export function snapshotDir(dirPath) {
22
+ const snapshot = {};
23
+ if (!existsSync(dirPath)) return snapshot;
24
+ try {
25
+ const files = readdirSync(dirPath, { recursive: true });
26
+ for (const f of files) {
27
+ const fp = join(dirPath, String(f));
28
+ try {
29
+ snapshot[String(f)] = readFileSync(fp, "utf8");
30
+ } catch {
31
+ // skip non-readable files
32
+ }
33
+ }
34
+ } catch {
35
+ // skip unreadable dirs
36
+ }
37
+ return snapshot;
38
+ }
39
+
40
+ /**
41
+ * Count the number of files that differ between two snapshots.
42
+ */
43
+ export function countChanges(before, after) {
44
+ let changes = 0;
45
+ const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
46
+ for (const key of allKeys) {
47
+ if (before[key] !== after[key]) changes++;
48
+ }
49
+ return changes;
50
+ }
51
+
52
+ /**
53
+ * Run an agentic-lib CLI command (e.g. "transform --target /tmp/ws --model gpt-5-mini").
54
+ */
55
+ export function runCli(args, cwd, timeoutMs = 300000) {
56
+ const cmd = `node ${binPath} ${args}`;
57
+ try {
58
+ const stdout = execSync(cmd, {
59
+ cwd: cwd || resolve(__dirname, ".."),
60
+ encoding: "utf8",
61
+ timeout: timeoutMs,
62
+ env: { ...process.env },
63
+ stdio: ["pipe", "pipe", "pipe"],
64
+ });
65
+ return { success: true, output: stdout };
66
+ } catch (err) {
67
+ return {
68
+ success: false,
69
+ output: `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`,
70
+ exitCode: err.status || 1,
71
+ };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Run tests in a workspace directory.
77
+ */
78
+ export function runTests(wsPath, timeoutMs = 120000) {
79
+ try {
80
+ const stdout = execSync("npm test 2>&1", {
81
+ cwd: wsPath,
82
+ encoding: "utf8",
83
+ timeout: timeoutMs,
84
+ env: { ...process.env },
85
+ stdio: ["pipe", "pipe", "pipe"],
86
+ });
87
+ return { success: true, output: stdout, exitCode: 0 };
88
+ } catch (err) {
89
+ return {
90
+ success: false,
91
+ output: `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`,
92
+ exitCode: err.status || 1,
93
+ };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Read cumulative transformation cost from intentïon.md.
99
+ * Each cost line: `**agentic-lib transformation cost:** N`
100
+ */
101
+ export function readTransformationCost(targetPath) {
102
+ const logPath = resolve(targetPath, "intentïon.md");
103
+ if (!existsSync(logPath)) return 0;
104
+ const content = readFileSync(logPath, "utf8");
105
+ const matches = content.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
106
+ return [...matches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
107
+ }
108
+
109
+ /**
110
+ * Read transformation budget from agentic-lib.toml.
111
+ * Falls back to 8 (the "recommended" profile default).
112
+ */
113
+ export function readBudget(targetPath) {
114
+ const tomlPath = resolve(targetPath, "agentic-lib.toml");
115
+ if (!existsSync(tomlPath)) return 8;
116
+ try {
117
+ const content = readFileSync(tomlPath, "utf8");
118
+ const match = content.match(/transformation-budget\s*=\s*(\d+)/);
119
+ return match ? parseInt(match[1], 10) : 8;
120
+ } catch {
121
+ return 8;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Detect the source path from agentic-lib.toml, defaulting to "src/lib".
127
+ */
128
+ function detectSourcePath(targetPath) {
129
+ const tomlPath = resolve(targetPath, "agentic-lib.toml");
130
+ if (!existsSync(tomlPath)) return "src/lib";
131
+ try {
132
+ const content = readFileSync(tomlPath, "utf8");
133
+ const match = content.match(/^source\s*=\s*"([^"]+)"/m);
134
+ return match ? match[1] : "src/lib";
135
+ } catch {
136
+ return "src/lib";
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Run an iteration loop: N cycles of steps, with stop conditions and budget tracking.
142
+ *
143
+ * @param {Object} options
144
+ * @param {string} options.targetPath - Workspace root directory
145
+ * @param {string} [options.model] - Copilot SDK model name
146
+ * @param {number} [options.maxCycles] - Max iterations (0 = use budget)
147
+ * @param {string[]} [options.steps] - Steps per cycle
148
+ * @param {boolean} [options.dryRun] - Skip actual Copilot calls
149
+ * @param {Function} [options.onCycleComplete] - Callback after each cycle
150
+ * @returns {Promise<{results: Array, totalCost: number, budget: number}>}
151
+ */
152
+ export async function runIterationLoop({
153
+ targetPath,
154
+ model = "gpt-5-mini",
155
+ maxCycles = 0,
156
+ steps = ["maintain-features", "transform", "fix-code"],
157
+ dryRun = false,
158
+ onCycleComplete,
159
+ }) {
160
+ const budget = readBudget(targetPath);
161
+ let totalCost = readTransformationCost(targetPath);
162
+ const remaining = Math.max(0, budget - totalCost);
163
+ const effectiveMax = maxCycles > 0 ? Math.min(maxCycles, remaining) : remaining;
164
+
165
+ if (effectiveMax <= 0) {
166
+ return {
167
+ results: [{ stopped: true, reason: `budget already exhausted (${totalCost}/${budget})` }],
168
+ totalCost,
169
+ budget,
170
+ };
171
+ }
172
+
173
+ const sourcePath = detectSourcePath(targetPath);
174
+ const results = [];
175
+ let consecutivePasses = 0;
176
+ let consecutiveNoChanges = 0;
177
+
178
+ for (let i = 0; i < effectiveMax; i++) {
179
+ const cycleStart = Date.now();
180
+ const cycleSteps = [];
181
+
182
+ // Snapshot source files before
183
+ const srcBefore = snapshotDir(resolve(targetPath, sourcePath));
184
+
185
+ // Run steps
186
+ for (const step of steps) {
187
+ if (dryRun) {
188
+ cycleSteps.push({ step, success: true, elapsed: "0.0", output: "[dry-run]" });
189
+ continue;
190
+ }
191
+ const stepStart = Date.now();
192
+ const result = runCli(`${step} --target ${targetPath} --model ${model}`, targetPath);
193
+ const stepElapsed = ((Date.now() - stepStart) / 1000).toFixed(1);
194
+ cycleSteps.push({
195
+ step,
196
+ success: result.success,
197
+ elapsed: stepElapsed,
198
+ output: result.output.substring(0, 500),
199
+ });
200
+ }
201
+
202
+ // Run tests
203
+ const testResult = dryRun ? { success: true, output: "[dry-run]" } : runTests(targetPath);
204
+ const testsPassed = testResult.success;
205
+
206
+ // Snapshot source files after
207
+ const srcAfter = dryRun ? srcBefore : snapshotDir(resolve(targetPath, sourcePath));
208
+ const filesChanged = countChanges(srcBefore, srcAfter);
209
+ const cost = filesChanged > 0 ? 1 : 0;
210
+ totalCost += cost;
211
+
212
+ const cycleElapsed = ((Date.now() - cycleStart) / 1000).toFixed(1);
213
+
214
+ const record = {
215
+ cycle: i + 1,
216
+ steps: cycleSteps,
217
+ testsPassed,
218
+ filesChanged,
219
+ cost,
220
+ totalCost,
221
+ budget,
222
+ elapsed: cycleElapsed,
223
+ model,
224
+ };
225
+ results.push(record);
226
+ if (onCycleComplete) onCycleComplete(record);
227
+
228
+ // Stop conditions
229
+ if (testsPassed) {
230
+ consecutivePasses++;
231
+ if (consecutivePasses >= 2) {
232
+ results.push({ stopped: true, reason: "tests passed 2 consecutive cycles" });
233
+ break;
234
+ }
235
+ } else {
236
+ consecutivePasses = 0;
237
+ }
238
+
239
+ if (filesChanged === 0) {
240
+ consecutiveNoChanges++;
241
+ if (consecutiveNoChanges >= 2) {
242
+ results.push({ stopped: true, reason: "no progress — 2 cycles with no file changes" });
243
+ break;
244
+ }
245
+ } else {
246
+ consecutiveNoChanges = 0;
247
+ }
248
+
249
+ if (totalCost >= budget) {
250
+ results.push({ stopped: true, reason: `budget exhausted (${totalCost}/${budget})` });
251
+ break;
252
+ }
253
+ }
254
+
255
+ return { results, totalCost, budget };
256
+ }
257
+
258
+ /**
259
+ * Format iteration results into a human-readable markdown string.
260
+ */
261
+ export function formatIterationResults(results, totalCost, budget, label = "Iterate") {
262
+ const lines = [`# ${label} Results`, `Budget: ${totalCost}/${budget}`, ""];
263
+ for (const r of results) {
264
+ if (r.stopped) {
265
+ lines.push(`**Stopped:** ${r.reason}`);
266
+ continue;
267
+ }
268
+ lines.push(`## Cycle ${r.cycle} (${r.model})`);
269
+ lines.push(`- Elapsed: ${r.elapsed}s`);
270
+ lines.push(`- Files changed: ${r.filesChanged}`);
271
+ lines.push(`- Tests: ${r.testsPassed ? "PASS" : "FAIL"}`);
272
+ lines.push(`- Cost: ${r.cost} (total: ${r.totalCost}/${r.budget})`);
273
+ for (const s of r.steps) {
274
+ lines.push(` - ${s.step}: ${s.success ? "OK" : "FAIL"} (${s.elapsed}s)`);
275
+ }
276
+ lines.push("");
277
+ }
278
+ const completed = results.filter((r) => !r.stopped).length;
279
+ const lastPassed = results.filter((r) => !r.stopped).slice(-1)[0]?.testsPassed;
280
+ lines.push("## Summary");
281
+ lines.push(`- Cycles completed: ${completed}`);
282
+ lines.push(`- Final test status: ${lastPassed ? "PASS" : "FAIL"}`);
283
+ lines.push(`- Total cost: ${totalCost}/${budget}`);
284
+ return lines.join("\n");
285
+ }
package/src/mcp/server.js CHANGED
@@ -227,7 +227,7 @@ const TOOLS = [
227
227
  overrides: {
228
228
  type: "object",
229
229
  description:
230
- "Individual tuning knob overrides. Keys: reasoning-effort, infinite-sessions, features-scan, source-scan, source-content, issues-scan, document-summary, discussion-comments",
230
+ "Individual tuning knob overrides. Keys: reasoning-effort, infinite-sessions, max-feature-files, max-source-files, max-source-chars, max-issues, max-summary-chars, max-discussion-comments",
231
231
  },
232
232
  },
233
233
  required: ["workspace"],
@@ -482,115 +482,40 @@ async function handleIterate({ workspace, cycles = 3, steps }) {
482
482
  return text(`Workspace "${workspace}" not found.`);
483
483
  }
484
484
 
485
+ const { runIterationLoop, formatIterationResults } = await import("../iterate.js");
485
486
  const stepsToRun = steps || ["maintain-features", "transform", "fix-code"];
486
- const results = [];
487
- let consecutivePasses = 0;
488
- let consecutiveNoChanges = 0;
489
487
  const startIterNum = (meta.iterations?.length || 0) + 1;
490
488
 
491
489
  meta.status = "iterating";
492
490
  writeMetadata(wsPath, meta);
493
491
 
494
- for (let i = 0; i < cycles; i++) {
495
- const iterNum = startIterNum + i;
496
- const iterStart = Date.now();
497
- const iterSteps = [];
498
-
499
- // Snapshot source files before
500
- const srcBefore = snapshotDir(join(wsPath, "src/lib"));
501
-
502
- for (const step of stepsToRun) {
503
- const stepStart = Date.now();
504
- const result = runCli(
505
- `${step} --target ${wsPath} --model ${meta.model}`,
506
- wsPath,
507
- 300000,
508
- );
509
- const stepElapsed = ((Date.now() - stepStart) / 1000).toFixed(1);
510
- iterSteps.push({
511
- step,
512
- success: result.success,
513
- elapsed: stepElapsed,
514
- output: result.output.substring(0, 500),
492
+ const { results, totalCost, budget } = await runIterationLoop({
493
+ targetPath: wsPath,
494
+ model: meta.model,
495
+ maxCycles: cycles,
496
+ steps: stepsToRun,
497
+ onCycleComplete: (record) => {
498
+ if (record.stopped) return;
499
+ // Persist each iteration to workspace metadata
500
+ meta.iterations.push({
501
+ number: startIterNum + record.cycle - 1,
502
+ profile: meta.profile,
503
+ model: meta.model,
504
+ steps: record.steps,
505
+ testsPassed: record.testsPassed,
506
+ filesChanged: record.filesChanged,
507
+ elapsed: record.elapsed,
515
508
  });
516
- }
517
-
518
- // Run tests
519
- const testResult = runInWorkspace("npm test 2>&1", wsPath, 60000);
520
- const testsPassed = testResult.success;
521
-
522
- // Snapshot source files after
523
- const srcAfter = snapshotDir(join(wsPath, "src/lib"));
524
- const filesChanged = countChanges(srcBefore, srcAfter);
525
-
526
- const iterElapsed = ((Date.now() - iterStart) / 1000).toFixed(1);
527
-
528
- const iterRecord = {
529
- number: iterNum,
530
- profile: meta.profile,
531
- model: meta.model,
532
- steps: iterSteps,
533
- testsPassed,
534
- filesChanged,
535
- elapsed: iterElapsed,
536
- };
537
-
538
- meta.iterations.push(iterRecord);
539
- writeMetadata(wsPath, meta);
540
-
541
- results.push(iterRecord);
542
-
543
- // Check stop conditions
544
- if (testsPassed) {
545
- consecutivePasses++;
546
- if (consecutivePasses >= 2) {
547
- results.push({ stopped: true, reason: "tests passed for 2 consecutive iterations" });
548
- break;
549
- }
550
- } else {
551
- consecutivePasses = 0;
552
- }
553
-
554
- if (filesChanged === 0) {
555
- consecutiveNoChanges++;
556
- if (consecutiveNoChanges >= 2) {
557
- results.push({ stopped: true, reason: "no files changed for 2 consecutive iterations (stalled)" });
558
- break;
559
- }
560
- } else {
561
- consecutiveNoChanges = 0;
562
- }
563
- }
509
+ writeMetadata(wsPath, meta);
510
+ },
511
+ });
564
512
 
565
513
  meta.status = "ready";
566
514
  writeMetadata(wsPath, meta);
567
515
 
568
- // Format results
569
- const lines = [`# Iterate Results: ${workspace}`, `Cycles requested: ${cycles}`, ""];
570
- for (const r of results) {
571
- if (r.stopped) {
572
- lines.push(`**Stopped early:** ${r.reason}`);
573
- continue;
574
- }
575
- lines.push(`## Iteration ${r.number} (${r.model}, ${r.profile})`);
576
- lines.push(`- Elapsed: ${r.elapsed}s`);
577
- lines.push(`- Files changed: ${r.filesChanged}`);
578
- lines.push(`- Tests: ${r.testsPassed ? "PASS" : "FAIL"}`);
579
- for (const s of r.steps) {
580
- lines.push(` - ${s.step}: ${s.success ? "OK" : "FAIL"} (${s.elapsed}s)`);
581
- }
582
- lines.push("");
583
- }
584
-
585
- const totalIters = results.filter((r) => !r.stopped).length;
586
- const lastPassed = results.filter((r) => !r.stopped).slice(-1)[0]?.testsPassed;
587
- lines.push("## Summary");
588
- lines.push(`- Iterations completed: ${totalIters}`);
589
- lines.push(`- Final test status: ${lastPassed ? "PASS" : "FAIL"}`);
590
- lines.push(`- Total iterations for this workspace: ${meta.iterations.length}`);
591
- lines.push(`- Profile: ${meta.profile} | Model: ${meta.model}`);
592
-
593
- return text(lines.join("\n"));
516
+ const output = formatIterationResults(results, totalCost, budget, `Iterate: ${workspace}`);
517
+ const extra = `\n- Total iterations for this workspace: ${meta.iterations.length}\n- Profile: ${meta.profile} | Model: ${meta.model}`;
518
+ return text(output + extra);
594
519
  }
595
520
 
596
521
  async function handleRunTests({ workspace }) {
@@ -841,34 +766,6 @@ function profileDefaultModel(profile) {
841
766
  return models[profile] || "gpt-5-mini";
842
767
  }
843
768
 
844
- function snapshotDir(dirPath) {
845
- const snapshot = {};
846
- if (!existsSync(dirPath)) return snapshot;
847
- try {
848
- const files = readdirSync(dirPath, { recursive: true });
849
- for (const f of files) {
850
- const fp = join(dirPath, String(f));
851
- try {
852
- snapshot[String(f)] = readFileSync(fp, "utf8");
853
- } catch {
854
- // skip non-readable files
855
- }
856
- }
857
- } catch {
858
- // skip unreadable dirs
859
- }
860
- return snapshot;
861
- }
862
-
863
- function countChanges(before, after) {
864
- let changes = 0;
865
- const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
866
- for (const key of allKeys) {
867
- if (before[key] !== after[key]) changes++;
868
- }
869
- return changes;
870
- }
871
-
872
769
  // ─── MCP Server ─────────────────────────────────────────────────────
873
770
 
874
771
  const toolHandlers = {
@@ -60,8 +60,8 @@ source = "src/lib/"
60
60
  tests = "tests/unit/"
61
61
 
62
62
  [limits]
63
- feature-issues = 2 # max concurrent feature issues
64
- attempts-per-issue = 2 # max retries per issue
63
+ max-feature-issues = 2 # max concurrent feature issues
64
+ max-attempts-per-issue = 2 # max retries per issue
65
65
  ```
66
66
 
67
67
  ## Updating
@@ -14,7 +14,7 @@
14
14
  "author": "",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@xn-intenton-z2a/agentic-lib": "^7.1.61"
17
+ "@xn-intenton-z2a/agentic-lib": "^7.1.62"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@vitest/coverage-v8": "^4.0.18",