@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.
- package/.github/workflows/agentic-lib-init.yml +56 -0
- package/.github/workflows/agentic-lib-test.yml +7 -2
- package/.github/workflows/agentic-lib-workflow.yml +50 -3
- package/README.md +88 -17
- package/agentic-lib.toml +7 -0
- package/bin/agentic-lib.js +260 -496
- package/package.json +2 -1
- package/src/actions/agentic-step/config-loader.js +9 -0
- package/src/actions/agentic-step/index.js +104 -7
- package/src/actions/agentic-step/tasks/direct.js +435 -0
- package/src/actions/agentic-step/tasks/supervise.js +107 -180
- package/src/agents/agent-apply-fix.md +5 -2
- package/src/agents/agent-director.md +58 -0
- package/src/agents/agent-discovery.md +52 -0
- package/src/agents/agent-issue-resolution.md +18 -0
- package/src/agents/agent-iterate.md +45 -0
- package/src/agents/agent-supervisor.md +22 -50
- package/src/copilot/agents.js +39 -0
- package/src/copilot/config.js +308 -0
- package/src/copilot/context.js +318 -0
- package/src/copilot/hybrid-session.js +330 -0
- package/src/copilot/logger.js +43 -0
- package/src/copilot/sdk.js +36 -0
- package/src/copilot/session.js +372 -0
- package/src/copilot/tasks/fix-code.js +73 -0
- package/src/copilot/tasks/maintain-features.js +61 -0
- package/src/copilot/tasks/maintain-library.js +66 -0
- package/src/copilot/tasks/transform.js +120 -0
- package/src/copilot/tools.js +141 -0
- package/src/mcp/server.js +43 -25
- package/src/seeds/zero-README.md +31 -0
- package/src/seeds/zero-behaviour.test.js +12 -4
- package/src/seeds/zero-package.json +1 -1
- package/src/seeds/zero-playwright.config.js +1 -0
|
@@ -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 {
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
model: meta.model,
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
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 }) {
|
package/src/seeds/zero-README.md
CHANGED
|
@@ -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,14 +1,22 @@
|
|
|
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
|
-
const response = await page.goto("/");
|
|
7
|
+
const response = await page.goto("/", { waitUntil: "networkidle" });
|
|
7
8
|
expect(response.status()).toBe(200);
|
|
8
9
|
|
|
9
|
-
await expect(page.locator("#lib-name")).toBeVisible();
|
|
10
|
-
await expect(page.locator("#lib-version")).toBeVisible();
|
|
11
|
-
await expect(page.locator("#demo-output")).toBeVisible();
|
|
10
|
+
await expect(page.locator("#lib-name")).toBeVisible({ timeout: 10000 });
|
|
11
|
+
await expect(page.locator("#lib-version")).toBeVisible({ timeout: 10000 });
|
|
12
|
+
await expect(page.locator("#demo-output")).toBeVisible({ timeout: 10000 });
|
|
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
|
+
});
|