@xn-intenton-z2a/agentic-lib 7.2.6 → 7.2.8

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.
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Copyright (C) 2025-2026 Polycode Limited
3
+ // src/copilot/tasks/transform.js — Transform task (shared)
4
+ //
5
+ // Ported from src/actions/agentic-step/tasks/transform.js.
6
+ // GitHub context (octokit, issues) is optional for local CLI use.
7
+
8
+ import { existsSync } from "fs";
9
+ import {
10
+ runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
11
+ filterIssues, summariseIssue, extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION,
12
+ } from "../session.js";
13
+ import { defaultLogger } from "../logger.js";
14
+
15
+ export async function transform(context) {
16
+ const { config, instructions, writablePaths, testCommand, model, logger = defaultLogger } = context;
17
+ // octokit + repo are optional (absent in CLI mode)
18
+ const octokit = context.octokit || null;
19
+ const repo = context.repo || null;
20
+ const issueNumber = context.issueNumber || null;
21
+ const t = config.tuning || {};
22
+
23
+ const mission = readOptionalFile(config.paths.mission.path);
24
+ if (!mission) {
25
+ logger.warning(`No mission file found at ${config.paths.mission.path}`);
26
+ return { outcome: "nop", details: "No mission file found" };
27
+ }
28
+
29
+ if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
30
+ logger.info("Mission complete — skipping transformation");
31
+ return { outcome: "nop", details: "Mission already complete" };
32
+ }
33
+
34
+ const features = scanDirectory(config.paths.features.path, ".md", { fileLimit: t.featuresScan || 10 }, logger);
35
+ const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
36
+ fileLimit: t.sourceScan || 10,
37
+ contentLimit: t.sourceContent || 5000,
38
+ recursive: true, sortByMtime: true, clean: true, outline: true,
39
+ }, logger);
40
+ const webFiles = scanDirectory(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], {
41
+ fileLimit: t.sourceScan || 10,
42
+ contentLimit: t.sourceContent || 5000,
43
+ recursive: true, sortByMtime: true, clean: true,
44
+ }, logger);
45
+
46
+ // GitHub issues (optional)
47
+ let openIssues = [];
48
+ let rawIssuesCount = 0;
49
+ if (octokit && repo) {
50
+ const { data: rawIssues } = await octokit.rest.issues.listForRepo({ ...repo, state: "open", per_page: t.issuesScan || 20 });
51
+ rawIssuesCount = rawIssues.length;
52
+ openIssues = filterIssues(rawIssues, { staleDays: t.staleDays || 30 });
53
+ }
54
+
55
+ let targetIssue = null;
56
+ if (issueNumber && octokit && repo) {
57
+ try {
58
+ const { data: issue } = await octokit.rest.issues.get({ ...repo, issue_number: Number(issueNumber) });
59
+ targetIssue = issue;
60
+ } catch (err) {
61
+ logger.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
62
+ }
63
+ }
64
+
65
+ const agentInstructions = instructions || "Transform the repository toward its mission by identifying the next best action.";
66
+
67
+ const prompt = [
68
+ "## Instructions", agentInstructions, "",
69
+ ...(targetIssue ? [
70
+ `## Target Issue #${targetIssue.number}: ${targetIssue.title}`,
71
+ targetIssue.body || "(no description)",
72
+ `Labels: ${targetIssue.labels.map((l) => l.name).join(", ") || "none"}`,
73
+ "", "**Focus your transformation on resolving this specific issue.**", "",
74
+ ] : []),
75
+ "## Mission", mission, "",
76
+ `## Current Features (${features.length})`,
77
+ ...features.map((f) => `### ${f.name}\n${extractFeatureSummary(f.content, f.name)}`), "",
78
+ `## Current Source Files (${sourceFiles.length})`,
79
+ ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
80
+ ...(webFiles.length > 0 ? [
81
+ `## Website Files (${webFiles.length})`,
82
+ ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
83
+ ] : []),
84
+ ...(openIssues.length > 0 ? [
85
+ `## Open Issues (${openIssues.length})`,
86
+ ...openIssues.slice(0, t.issuesScan || 20).map((i) => summariseIssue(i, t.issueBodyLimit || 500)), "",
87
+ ] : []),
88
+ "## Output Artifacts",
89
+ `Save output artifacts to \`${config.paths.examples?.path || "examples/"}\`.`, "",
90
+ "## Your Task",
91
+ "Analyze the mission, features, source code, and open issues.",
92
+ "Determine the single most impactful next step to transform this repository.", "Then implement that step.", "",
93
+ "## When NOT to make changes",
94
+ "If the existing code already satisfies all requirements:", "- Do NOT make cosmetic changes", "- Instead, report that the mission is satisfied", "",
95
+ formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
96
+ "## Constraints", `- Run \`${testCommand}\` to validate your changes`,
97
+ ].join("\n");
98
+
99
+ logger.info(`Transform prompt length: ${prompt.length} chars`);
100
+
101
+ const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
102
+ model,
103
+ systemMessage: "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." + NARRATIVE_INSTRUCTION,
104
+ prompt, writablePaths, tuning: t, logger,
105
+ });
106
+
107
+ const promptBudget = [
108
+ { section: "mission", size: mission.length, files: "1", notes: "full" },
109
+ { section: "features", size: features.reduce((s, f) => s + f.content.length, 0), files: `${features.length}`, notes: "" },
110
+ { section: "source", size: sourceFiles.reduce((s, f) => s + f.content.length, 0), files: `${sourceFiles.length}`, notes: "" },
111
+ { section: "issues", size: openIssues.length * 80, files: `${openIssues.length}`, notes: `${rawIssuesCount - openIssues.length} filtered` },
112
+ ];
113
+
114
+ return {
115
+ outcome: "transformed", tokensUsed, inputTokens, outputTokens, cost, model,
116
+ details: resultContent.substring(0, 500),
117
+ narrative: extractNarrative(resultContent, "Transformation step completed."),
118
+ promptBudget,
119
+ };
120
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Copyright (C) 2025-2026 Polycode Limited
3
+ // src/copilot/tools.js — Shared tool definitions (Actions + CLI)
4
+ //
5
+ // Ported from src/actions/agentic-step/tools.js with logger abstraction.
6
+
7
+ import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "fs";
8
+ import { execSync } from "child_process";
9
+ import { dirname, resolve } from "path";
10
+ import { defaultLogger } from "./logger.js";
11
+
12
+ /**
13
+ * Check if a target path is within the allowed writable paths.
14
+ */
15
+ export function isPathWritable(targetPath, writablePaths) {
16
+ const resolvedTarget = resolve(targetPath);
17
+ return writablePaths.some((allowed) => {
18
+ const resolvedAllowed = resolve(allowed);
19
+ if (resolvedTarget === resolvedAllowed) return true;
20
+ if (allowed.endsWith("/") && resolvedTarget.startsWith(resolvedAllowed)) return true;
21
+ if (resolvedTarget.startsWith(resolvedAllowed + "/")) return true;
22
+ return false;
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Create the standard set of agent tools.
28
+ * Can accept defineTool as a parameter (for dynamic SDK import) or import it.
29
+ *
30
+ * @param {string[]} writablePaths
31
+ * @param {Object} [logger]
32
+ * @param {Function} [defineToolFn] - SDK defineTool function (optional — auto-imported if not provided)
33
+ * @returns {Array} Array of tools for createSession()
34
+ */
35
+ export function createAgentTools(writablePaths, logger = defaultLogger, defineToolFn) {
36
+ // If defineTool not provided, the caller must pass it.
37
+ // We can't dynamically import SDK here synchronously.
38
+ const defineTool = defineToolFn;
39
+ if (!defineTool) {
40
+ throw new Error("createAgentTools requires defineToolFn parameter (from Copilot SDK)");
41
+ }
42
+
43
+ const readFile = defineTool("read_file", {
44
+ description: "Read the contents of a file at the given path.",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ path: { type: "string", description: "Absolute or relative file path to read" },
49
+ },
50
+ required: ["path"],
51
+ },
52
+ handler: ({ path }) => {
53
+ const resolved = resolve(path);
54
+ logger.info(`[tool] read_file: ${resolved}`);
55
+ if (!existsSync(resolved)) return { error: `File not found: ${resolved}` };
56
+ try {
57
+ return { content: readFileSync(resolved, "utf8") };
58
+ } catch (err) {
59
+ return { error: `Failed to read ${resolved}: ${err.message}` };
60
+ }
61
+ },
62
+ });
63
+
64
+ const writeFile = defineTool("write_file", {
65
+ description: "Write content to a file. Parent directories created automatically. Only writable paths allowed.",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {
69
+ path: { type: "string", description: "Absolute or relative file path to write" },
70
+ content: { type: "string", description: "The full content to write to the file" },
71
+ },
72
+ required: ["path", "content"],
73
+ },
74
+ handler: ({ path, content }) => {
75
+ const resolved = resolve(path);
76
+ logger.info(`[tool] write_file: ${resolved}`);
77
+ if (!isPathWritable(resolved, writablePaths)) {
78
+ return { error: `Path is not writable: ${path}. Writable: ${writablePaths.join(", ")}` };
79
+ }
80
+ try {
81
+ const dir = dirname(resolved);
82
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
83
+ writeFileSync(resolved, content, "utf8");
84
+ return { success: true, path: resolved };
85
+ } catch (err) {
86
+ return { error: `Failed to write ${resolved}: ${err.message}` };
87
+ }
88
+ },
89
+ });
90
+
91
+ const listFiles = defineTool("list_files", {
92
+ description: "List files and directories at the given path.",
93
+ parameters: {
94
+ type: "object",
95
+ properties: {
96
+ path: { type: "string", description: "Directory path to list" },
97
+ recursive: { type: "boolean", description: "Whether to list recursively (default false)" },
98
+ },
99
+ required: ["path"],
100
+ },
101
+ handler: ({ path, recursive }) => {
102
+ const resolved = resolve(path);
103
+ logger.info(`[tool] list_files: ${resolved}`);
104
+ if (!existsSync(resolved)) return { error: `Directory not found: ${resolved}` };
105
+ try {
106
+ const entries = readdirSync(resolved, { withFileTypes: true, recursive: !!recursive });
107
+ return { files: entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name)) };
108
+ } catch (err) {
109
+ return { error: `Failed to list ${resolved}: ${err.message}` };
110
+ }
111
+ },
112
+ });
113
+
114
+ const runCommand = defineTool("run_command", {
115
+ description: "Run a shell command and return stdout/stderr.",
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ command: { type: "string", description: "The shell command to execute" },
120
+ cwd: { type: "string", description: "Working directory (default: current)" },
121
+ },
122
+ required: ["command"],
123
+ },
124
+ handler: ({ command, cwd }) => {
125
+ const workDir = cwd ? resolve(cwd) : process.cwd();
126
+ logger.info(`[tool] run_command: ${command} (cwd=${workDir})`);
127
+ const blocked = /\bgit\s+(commit|push|add|reset|checkout|rebase|merge|stash)\b/;
128
+ if (blocked.test(command)) {
129
+ return { error: "Git write commands are not allowed." };
130
+ }
131
+ try {
132
+ const stdout = execSync(command, { cwd: workDir, encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024 });
133
+ return { stdout, exitCode: 0 };
134
+ } catch (err) {
135
+ return { stdout: err.stdout || "", stderr: err.stderr || "", exitCode: err.status || 1, error: err.message };
136
+ }
137
+ },
138
+ });
139
+
140
+ return [readFile, writeFile, listFiles, runCommand];
141
+ }
package/src/mcp/server.js CHANGED
@@ -482,40 +482,58 @@ 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");
486
- const stepsToRun = steps || ["maintain-features", "transform", "fix-code"];
487
- const startIterNum = (meta.iterations?.length || 0) + 1;
485
+ const { runHybridSession } = await import("../copilot/hybrid-session.js");
486
+
487
+ let config;
488
+ try {
489
+ const { loadConfig } = await import("../copilot/config.js");
490
+ config = loadConfig(join(wsPath, "agentic-lib.toml"));
491
+ } catch {
492
+ config = { tuning: {}, model: meta.model || "gpt-5-mini" };
493
+ }
488
494
 
489
495
  meta.status = "iterating";
490
496
  writeMetadata(wsPath, meta);
491
497
 
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,
508
- });
509
- writeMetadata(wsPath, meta);
510
- },
498
+ const result = await runHybridSession({
499
+ workspacePath: wsPath,
500
+ model: meta.model || config.model || "gpt-5-mini",
501
+ tuning: config.tuning || {},
502
+ timeoutMs: 600000,
511
503
  });
512
504
 
505
+ const iterNum = (meta.iterations?.length || 0) + 1;
506
+ meta.iterations.push({
507
+ number: iterNum,
508
+ profile: meta.profile,
509
+ model: meta.model,
510
+ testsPassed: result.testsPassed,
511
+ toolCalls: result.toolCalls,
512
+ testRuns: result.testRuns,
513
+ filesWritten: result.filesWritten,
514
+ elapsed: `${result.totalTime}`,
515
+ endReason: result.endReason,
516
+ });
513
517
  meta.status = "ready";
514
518
  writeMetadata(wsPath, meta);
515
519
 
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);
520
+ const lines = [
521
+ `# Iterate: ${workspace}`,
522
+ "",
523
+ `- Success: ${result.success}`,
524
+ `- Tests passed: ${result.testsPassed}`,
525
+ `- Session time: ${result.sessionTime}s`,
526
+ `- Total time: ${result.totalTime}s`,
527
+ `- Tool calls: ${result.toolCalls}`,
528
+ `- Test runs: ${result.testRuns}`,
529
+ `- Files written: ${result.filesWritten}`,
530
+ `- Tokens: ${result.tokensIn + result.tokensOut} (in=${result.tokensIn} out=${result.tokensOut})`,
531
+ `- End reason: ${result.endReason}`,
532
+ "",
533
+ `- Total iterations for this workspace: ${meta.iterations.length}`,
534
+ `- Profile: ${meta.profile} | Model: ${meta.model}`,
535
+ ];
536
+ return text(lines.join("\n"));
519
537
  }
520
538
 
521
539
  async function handleRunTests({ workspace }) {
@@ -46,6 +46,37 @@ MISSION.md → [supervisor] → dispatch workflows → Issue → Code → Test
46
46
 
47
47
  The pipeline runs as GitHub Actions workflows. An LLM supervisor gathers repository context (issues, PRs, workflow runs, features) and strategically dispatches other workflows. Each workflow uses the Copilot SDK to make targeted changes.
48
48
 
49
+ ## File Layout
50
+
51
+ ```
52
+ src/lib/main.js ← library (Node entry point: identity + mission functions)
53
+ src/web/index.html ← web page (browser: imports lib-meta.js, demonstrates library)
54
+ tests/unit/main.test.js ← unit tests (import main.js directly, test API-level detail)
55
+ tests/unit/web.test.js ← web structure tests (read index.html as text, verify wiring)
56
+ tests/behaviour/ ← Playwright E2E (run page in browser, import main.js for coupling)
57
+ docs/ ← build output (generated by npm run build:web)
58
+ docs/lib-meta.js ← generated: exports name, version, description from package.json
59
+ ```
60
+
61
+ These files form a **coupled unit**. Changes to the library must flow through to the web page, and tests verify this coupling:
62
+
63
+ - `src/lib/main.js` exports `getIdentity()` → returns `{ name, version, description }` from `package.json`
64
+ - `npm run build:web` copies `src/web/*` to `docs/` and generates `docs/lib-meta.js` from `package.json`
65
+ - `src/web/index.html` imports `lib-meta.js` at runtime → displays library identity on the page
66
+ - The behaviour test imports `getIdentity()` from `main.js` AND reads `#lib-version` from the rendered page → asserts they match
67
+
68
+ This coupling test proves the web page is consuming the real library via the build pipeline. Mission-specific functions should follow the same path — never duplicate library logic inline in the web page.
69
+
70
+ ## Test Strategy
71
+
72
+ | Test layer | What it tests | How it binds |
73
+ |------------|--------------|--------------|
74
+ | **Unit tests** (`tests/unit/main.test.js`) | Library API: return values, error types, edge cases | Imports directly from `src/lib/main.js` |
75
+ | **Web structure tests** (`tests/unit/web.test.js`) | HTML structure: expected elements, `lib-meta.js` import | Reads `src/web/index.html` as text |
76
+ | **Behaviour tests** (`tests/behaviour/`) | End-to-end: page renders, interactive elements work | Playwright loads the built site; coupling test imports `getIdentity()` from `main.js` and asserts the page displays the same version |
77
+
78
+ The **coupling test** in the behaviour test is the key invariant: it proves the web page displays values from the actual library, not hardcoded or duplicated values. If the build pipeline breaks, this test fails.
79
+
49
80
  ## Configuration
50
81
 
51
82
  Edit `agentic-lib.toml` to tune the system:
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
3
  import { test, expect } from "@playwright/test";
4
+ import { getIdentity } from "../../src/lib/main.js";
4
5
 
5
6
  test("homepage returns 200 and renders", async ({ page }) => {
6
7
  const response = await page.goto("/", { waitUntil: "networkidle" });
@@ -12,3 +13,10 @@ test("homepage returns 200 and renders", async ({ page }) => {
12
13
 
13
14
  await page.screenshot({ path: "SCREENSHOT_INDEX.png", fullPage: true });
14
15
  });
16
+
17
+ test("page displays the library version from src/lib/main.js", async ({ page }) => {
18
+ const { version } = getIdentity();
19
+ await page.goto("/", { waitUntil: "networkidle" });
20
+ const pageVersion = await page.locator("#lib-version").textContent();
21
+ expect(pageVersion).toContain(version);
22
+ });
@@ -17,7 +17,7 @@
17
17
  "author": "",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@xn-intenton-z2a/agentic-lib": "^7.2.6"
20
+ "@xn-intenton-z2a/agentic-lib": "^7.2.8"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@playwright/test": "^1.58.0",