@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xn-intenton-z2a/agentic-lib",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.7",
|
|
4
4
|
"description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"bin/",
|
|
73
73
|
"src/dist-transform.js",
|
|
74
74
|
"src/iterate.js",
|
|
75
|
+
"src/copilot/",
|
|
75
76
|
".github/workflows/agentic-lib-*.yml",
|
|
76
77
|
"agentic-lib.toml",
|
|
77
78
|
"src/actions/agentic-step/*.js",
|
|
@@ -257,6 +257,14 @@ export function loadConfig(configPath) {
|
|
|
257
257
|
const execution = toml.execution || {};
|
|
258
258
|
const bot = toml.bot || {};
|
|
259
259
|
|
|
260
|
+
// Mission-complete thresholds (with safe defaults)
|
|
261
|
+
const mc = toml["mission-complete"] || {};
|
|
262
|
+
const missionCompleteThresholds = {
|
|
263
|
+
minResolvedIssues: mc["min-resolved-issues"] ?? 3,
|
|
264
|
+
requireDedicatedTests: mc["require-dedicated-tests"] ?? true,
|
|
265
|
+
maxSourceTodos: mc["max-source-todos"] ?? 0,
|
|
266
|
+
};
|
|
267
|
+
|
|
260
268
|
return {
|
|
261
269
|
supervisor: toml.schedule?.supervisor || "daily",
|
|
262
270
|
model: toml.tuning?.model || toml.schedule?.model || "gpt-5-mini",
|
|
@@ -274,6 +282,7 @@ export function loadConfig(configPath) {
|
|
|
274
282
|
},
|
|
275
283
|
init: toml.init || null,
|
|
276
284
|
tdd: toml.tdd === true,
|
|
285
|
+
missionCompleteThresholds,
|
|
277
286
|
writablePaths,
|
|
278
287
|
readOnlyPaths,
|
|
279
288
|
configToml: rawToml,
|
|
@@ -9,7 +9,8 @@ import * as core from "@actions/core";
|
|
|
9
9
|
import * as github from "@actions/github";
|
|
10
10
|
import { loadConfig, getWritablePaths } from "./config-loader.js";
|
|
11
11
|
import { logActivity, generateClosingNotes } from "./logging.js";
|
|
12
|
-
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
12
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
13
14
|
|
|
14
15
|
// Task implementations
|
|
15
16
|
import { resolveIssue } from "./tasks/resolve-issue.js";
|
|
@@ -21,6 +22,7 @@ import { enhanceIssue } from "./tasks/enhance-issue.js";
|
|
|
21
22
|
import { reviewIssue } from "./tasks/review-issue.js";
|
|
22
23
|
import { discussions } from "./tasks/discussions.js";
|
|
23
24
|
import { supervise } from "./tasks/supervise.js";
|
|
25
|
+
import { direct } from "./tasks/direct.js";
|
|
24
26
|
|
|
25
27
|
const TASKS = {
|
|
26
28
|
"resolve-issue": resolveIssue,
|
|
@@ -32,8 +34,38 @@ const TASKS = {
|
|
|
32
34
|
"review-issue": reviewIssue,
|
|
33
35
|
"discussions": discussions,
|
|
34
36
|
"supervise": supervise,
|
|
37
|
+
"direct": direct,
|
|
35
38
|
};
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Recursively count TODO/FIXME comments in source files under a directory.
|
|
42
|
+
* @param {string} dir - Directory to scan
|
|
43
|
+
* @param {string[]} [extensions] - File extensions to include (default: .js, .ts, .mjs)
|
|
44
|
+
* @returns {number} Total count of TODO/FIXME occurrences
|
|
45
|
+
*/
|
|
46
|
+
export function countSourceTodos(dir, extensions = [".js", ".ts", ".mjs"]) {
|
|
47
|
+
let count = 0;
|
|
48
|
+
if (!existsSync(dir)) return 0;
|
|
49
|
+
try {
|
|
50
|
+
const entries = readdirSync(dir);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
53
|
+
const fullPath = join(dir, entry);
|
|
54
|
+
try {
|
|
55
|
+
const stat = statSync(fullPath);
|
|
56
|
+
if (stat.isDirectory()) {
|
|
57
|
+
count += countSourceTodos(fullPath, extensions);
|
|
58
|
+
} else if (extensions.some((ext) => entry.endsWith(ext))) {
|
|
59
|
+
const content = readFileSync(fullPath, "utf8");
|
|
60
|
+
const matches = content.match(/\bTODO\b/gi);
|
|
61
|
+
if (matches) count += matches.length;
|
|
62
|
+
}
|
|
63
|
+
} catch { /* skip unreadable files */ }
|
|
64
|
+
}
|
|
65
|
+
} catch { /* skip unreadable dirs */ }
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
/**
|
|
38
70
|
* Build mission-complete metrics array for the intentïon.md dashboard.
|
|
39
71
|
*/
|
|
@@ -47,10 +79,28 @@ function buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featu
|
|
|
47
79
|
// Count open PRs from result if available
|
|
48
80
|
const openPrs = result.openPrCount || 0;
|
|
49
81
|
|
|
82
|
+
// W9: Count TODO comments in source directory
|
|
83
|
+
const sourcePath = config.paths?.source?.path || "src/lib/";
|
|
84
|
+
const sourceDir = sourcePath.endsWith("/") ? sourcePath.slice(0, -1) : sourcePath;
|
|
85
|
+
// Scan the parent src/ directory to catch all source TODOs
|
|
86
|
+
const srcRoot = sourceDir.includes("/") ? sourceDir.split("/").slice(0, -1).join("/") || "src" : "src";
|
|
87
|
+
const todoCount = countSourceTodos(srcRoot);
|
|
88
|
+
|
|
89
|
+
// W3: Check for dedicated test files
|
|
90
|
+
const hasDedicatedTests = result.hasDedicatedTests ?? false;
|
|
91
|
+
|
|
92
|
+
// W11: Thresholds from config
|
|
93
|
+
const thresholds = config.missionCompleteThresholds || {};
|
|
94
|
+
const minResolved = thresholds.minResolvedIssues ?? 3;
|
|
95
|
+
const requireTests = thresholds.requireDedicatedTests ?? true;
|
|
96
|
+
const maxTodos = thresholds.maxSourceTodos ?? 0;
|
|
97
|
+
|
|
50
98
|
const metrics = [
|
|
51
99
|
{ metric: "Open issues", value: String(openIssues), target: "0", status: openIssues === 0 ? "MET" : "NOT MET" },
|
|
52
100
|
{ metric: "Open PRs", value: String(openPrs), target: "0", status: openPrs === 0 ? "MET" : "NOT MET" },
|
|
53
|
-
{ metric: "Issues resolved (review or PR merge)", value: String(resolvedCount), target:
|
|
101
|
+
{ metric: "Issues resolved (review or PR merge)", value: String(resolvedCount), target: `>= ${minResolved}`, status: resolvedCount >= minResolved ? "MET" : "NOT MET" },
|
|
102
|
+
{ metric: "Dedicated test files", value: hasDedicatedTests ? "YES" : "NO", target: requireTests ? "YES" : "—", status: !requireTests || hasDedicatedTests ? "MET" : "NOT MET" },
|
|
103
|
+
{ metric: "Source TODO count", value: String(todoCount), target: `<= ${maxTodos}`, status: todoCount <= maxTodos ? "MET" : "NOT MET" },
|
|
54
104
|
{ metric: "Transformation budget used", value: `${cumulativeCost}/${budgetCap}`, target: budgetCap > 0 ? `< ${budgetCap}` : "unlimited", status: budgetCap > 0 && cumulativeCost >= budgetCap ? "EXHAUSTED" : "OK" },
|
|
55
105
|
{ metric: "Cumulative transforms", value: String(cumulativeCost), target: ">= 1", status: cumulativeCost >= 1 ? "MET" : "NOT MET" },
|
|
56
106
|
{ metric: "Mission complete declared", value: missionComplete ? "YES" : "NO", target: "—", status: "—" },
|
|
@@ -67,6 +117,8 @@ function buildMissionReadiness(metrics) {
|
|
|
67
117
|
const openIssues = parseInt(metrics.find((m) => m.metric === "Open issues")?.value || "0", 10);
|
|
68
118
|
const openPrs = parseInt(metrics.find((m) => m.metric === "Open PRs")?.value || "0", 10);
|
|
69
119
|
const resolved = parseInt(metrics.find((m) => m.metric === "Issues resolved (review or PR merge)")?.value || "0", 10);
|
|
120
|
+
const hasDedicatedTests = metrics.find((m) => m.metric === "Dedicated test files")?.value === "YES";
|
|
121
|
+
const todoCount = parseInt(metrics.find((m) => m.metric === "Source TODO count")?.value || "0", 10);
|
|
70
122
|
const missionComplete = metrics.find((m) => m.metric === "Mission complete declared")?.value === "YES";
|
|
71
123
|
const missionFailed = metrics.find((m) => m.metric === "Mission failed declared")?.value === "YES";
|
|
72
124
|
|
|
@@ -77,17 +129,23 @@ function buildMissionReadiness(metrics) {
|
|
|
77
129
|
return "Mission has been declared failed.";
|
|
78
130
|
}
|
|
79
131
|
|
|
80
|
-
|
|
132
|
+
// Check all NOT MET conditions
|
|
133
|
+
const notMet = metrics.filter((m) => m.status === "NOT MET");
|
|
134
|
+
const allMet = notMet.length === 0;
|
|
81
135
|
const parts = [];
|
|
82
136
|
|
|
83
|
-
if (
|
|
137
|
+
if (allMet) {
|
|
84
138
|
parts.push("Mission complete conditions ARE met.");
|
|
85
|
-
parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) resolved.`);
|
|
139
|
+
parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) resolved, dedicated tests: ${hasDedicatedTests ? "yes" : "no"}, TODOs: ${todoCount}.`);
|
|
86
140
|
} else {
|
|
87
141
|
parts.push("Mission complete conditions are NOT met.");
|
|
88
142
|
if (openIssues > 0) parts.push(`${openIssues} open issue(s) remain.`);
|
|
89
143
|
if (openPrs > 0) parts.push(`${openPrs} open PR(s) remain.`);
|
|
90
|
-
|
|
144
|
+
for (const m of notMet) {
|
|
145
|
+
if (m.metric !== "Open issues" && m.metric !== "Open PRs") {
|
|
146
|
+
parts.push(`${m.metric}: ${m.value} (target: ${m.target}).`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
91
149
|
}
|
|
92
150
|
|
|
93
151
|
return parts.join(" ");
|
|
@@ -166,9 +224,23 @@ async function run() {
|
|
|
166
224
|
const profileName = config.tuning?.profileName || "unknown";
|
|
167
225
|
|
|
168
226
|
// Transformation cost: 1 for code-changing tasks, 0 otherwise
|
|
227
|
+
// W4: Instability transforms (infrastructure fixes) don't count against mission budget
|
|
169
228
|
const COST_TASKS = ["transform", "fix-code", "maintain-features", "maintain-library"];
|
|
170
229
|
const isNop = result.outcome === "nop" || result.outcome === "error";
|
|
171
|
-
|
|
230
|
+
let isInstabilityTransform = false;
|
|
231
|
+
if (issueNumber && COST_TASKS.includes(task) && !isNop) {
|
|
232
|
+
try {
|
|
233
|
+
const { data: issueData } = await context.octokit.rest.issues.get({
|
|
234
|
+
...context.repo,
|
|
235
|
+
issue_number: Number(issueNumber),
|
|
236
|
+
});
|
|
237
|
+
isInstabilityTransform = issueData.labels.some((l) => l.name === "instability");
|
|
238
|
+
if (isInstabilityTransform) {
|
|
239
|
+
core.info(`Issue #${issueNumber} has instability label — transform does not count against mission budget`);
|
|
240
|
+
}
|
|
241
|
+
} catch { /* ignore — conservative: count as mission transform */ }
|
|
242
|
+
}
|
|
243
|
+
const transformationCost = COST_TASKS.includes(task) && !isNop && !isInstabilityTransform ? 1 : 0;
|
|
172
244
|
|
|
173
245
|
// Read cumulative transformation cost from the activity log
|
|
174
246
|
const intentionFilepath = config.intentionBot?.intentionFilepath;
|
|
@@ -190,6 +262,31 @@ async function run() {
|
|
|
190
262
|
? readdirSync(libraryPath).filter((f) => f.endsWith(".md")).length
|
|
191
263
|
: 0;
|
|
192
264
|
|
|
265
|
+
// W3/W10: Detect dedicated test files (centrally, for all tasks)
|
|
266
|
+
let hasDedicatedTests = result.hasDedicatedTests ?? false;
|
|
267
|
+
if (!hasDedicatedTests) {
|
|
268
|
+
try {
|
|
269
|
+
const { scanDirectory: scanDir } = await import("./copilot.js");
|
|
270
|
+
const testDirs = ["tests", "__tests__"];
|
|
271
|
+
for (const dir of testDirs) {
|
|
272
|
+
if (existsSync(dir)) {
|
|
273
|
+
const testFiles = scanDir(dir, [".js", ".ts", ".mjs"], { limit: 20 });
|
|
274
|
+
for (const tf of testFiles) {
|
|
275
|
+
if (/^(main|web|behaviour)\.test\.[jt]s$/.test(tf.name)) continue;
|
|
276
|
+
const content = readFileSync(tf.path, "utf8");
|
|
277
|
+
if (/from\s+['"].*src\/lib\//.test(content) || /require\s*\(\s*['"].*src\/lib\//.test(content)) {
|
|
278
|
+
hasDedicatedTests = true;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (hasDedicatedTests) break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch { /* ignore — scanDirectory not available in test environment */ }
|
|
286
|
+
}
|
|
287
|
+
// Inject hasDedicatedTests into result for buildMissionMetrics
|
|
288
|
+
result.hasDedicatedTests = hasDedicatedTests;
|
|
289
|
+
|
|
193
290
|
// Count open automated issues (feature vs maintenance)
|
|
194
291
|
let featureIssueCount = 0;
|
|
195
292
|
let maintenanceIssueCount = 0;
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/direct.js — Director: mission-complete/failed evaluation via LLM
|
|
4
|
+
//
|
|
5
|
+
// Gathers mission metrics, builds an advisory assessment, asks the LLM
|
|
6
|
+
// to decide mission-complete, mission-failed, or produce a gap analysis.
|
|
7
|
+
// The director does NOT dispatch workflows or create issues — that's the supervisor's job.
|
|
8
|
+
|
|
9
|
+
import * as core from "@actions/core";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
11
|
+
import { runCopilotTask, readOptionalFile, scanDirectory, filterIssues } from "../copilot.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count TODO comments recursively in a directory.
|
|
15
|
+
*/
|
|
16
|
+
function countTodos(dir) {
|
|
17
|
+
let n = 0;
|
|
18
|
+
if (!existsSync(dir)) return 0;
|
|
19
|
+
try {
|
|
20
|
+
const entries = readdirSync(dir);
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
23
|
+
const fp = `${dir}/${entry}`;
|
|
24
|
+
try {
|
|
25
|
+
const stat = statSync(fp);
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
n += countTodos(fp);
|
|
28
|
+
} else if (/\.(js|ts|mjs)$/.test(entry)) {
|
|
29
|
+
const content = readFileSync(fp, "utf8");
|
|
30
|
+
const m = content.match(/\bTODO\b/gi);
|
|
31
|
+
if (m) n += m.length;
|
|
32
|
+
}
|
|
33
|
+
} catch { /* skip */ }
|
|
34
|
+
}
|
|
35
|
+
} catch { /* skip */ }
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect dedicated test files that import from src/lib/.
|
|
41
|
+
*/
|
|
42
|
+
function detectDedicatedTests() {
|
|
43
|
+
let hasDedicatedTests = false;
|
|
44
|
+
const dedicatedTestFiles = [];
|
|
45
|
+
const testDirs = ["tests", "__tests__"];
|
|
46
|
+
for (const dir of testDirs) {
|
|
47
|
+
if (existsSync(dir)) {
|
|
48
|
+
try {
|
|
49
|
+
const testFiles = scanDirectory(dir, [".js", ".ts", ".mjs"], { limit: 20 });
|
|
50
|
+
for (const tf of testFiles) {
|
|
51
|
+
if (/^(main|web|behaviour)\.test\.[jt]s$/.test(tf.name)) continue;
|
|
52
|
+
const content = readFileSync(tf.path, "utf8");
|
|
53
|
+
if (/from\s+['"].*src\/lib\//.test(content) || /require\s*\(\s*['"].*src\/lib\//.test(content)) {
|
|
54
|
+
hasDedicatedTests = true;
|
|
55
|
+
dedicatedTestFiles.push(tf.name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { hasDedicatedTests, dedicatedTestFiles };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the metric-based mission-complete advisory string.
|
|
66
|
+
* This is the mechanical check — purely rule-based, no LLM.
|
|
67
|
+
*/
|
|
68
|
+
function buildMetricAssessment(ctx, config) {
|
|
69
|
+
const thresholds = config.missionCompleteThresholds || {};
|
|
70
|
+
const minResolved = thresholds.minResolvedIssues ?? 3;
|
|
71
|
+
const requireTests = thresholds.requireDedicatedTests ?? true;
|
|
72
|
+
const maxTodos = thresholds.maxSourceTodos ?? 0;
|
|
73
|
+
|
|
74
|
+
const metrics = [
|
|
75
|
+
{ metric: "Open issues", value: ctx.issuesSummary.length, target: 0, met: ctx.issuesSummary.length === 0 },
|
|
76
|
+
{ metric: "Open PRs", value: ctx.prsSummary.length, target: 0, met: ctx.prsSummary.length === 0 },
|
|
77
|
+
{ metric: "Issues resolved", value: ctx.resolvedCount, target: minResolved, met: ctx.resolvedCount >= minResolved },
|
|
78
|
+
{ metric: "Dedicated tests", value: ctx.hasDedicatedTests ? "YES" : "NO", target: requireTests ? "YES" : "—", met: !requireTests || ctx.hasDedicatedTests },
|
|
79
|
+
{ metric: "Source TODOs", value: ctx.sourceTodoCount, target: maxTodos, met: ctx.sourceTodoCount <= maxTodos },
|
|
80
|
+
{ metric: "Budget", value: ctx.cumulativeTransformationCost, target: ctx.transformationBudget || "unlimited", met: !(ctx.transformationBudget > 0 && ctx.cumulativeTransformationCost >= ctx.transformationBudget) },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const allMet = metrics.every((m) => m.met);
|
|
84
|
+
const notMet = metrics.filter((m) => !m.met);
|
|
85
|
+
|
|
86
|
+
const table = [
|
|
87
|
+
"| Metric | Value | Target | Status |",
|
|
88
|
+
"|--------|-------|--------|--------|",
|
|
89
|
+
...metrics.map((m) => `| ${m.metric} | ${m.value} | ${typeof m.target === "number" ? (m.metric.includes("TODO") ? `<= ${m.target}` : m.metric.includes("resolved") ? `>= ${m.target}` : `${m.target}`) : m.target} | ${m.met ? "MET" : "NOT MET"} |`),
|
|
90
|
+
].join("\n");
|
|
91
|
+
|
|
92
|
+
let assessment;
|
|
93
|
+
if (allMet) {
|
|
94
|
+
assessment = "ALL METRICS MET — mission-complete conditions are satisfied.";
|
|
95
|
+
} else {
|
|
96
|
+
assessment = `${notMet.length} metric(s) NOT MET: ${notMet.map((m) => `${m.metric}=${m.value}`).join(", ")}.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { metrics, allMet, notMet, table, assessment };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the director prompt.
|
|
104
|
+
*/
|
|
105
|
+
function buildPrompt(ctx, agentInstructions, metricAssessment) {
|
|
106
|
+
return [
|
|
107
|
+
"## Instructions",
|
|
108
|
+
agentInstructions,
|
|
109
|
+
"",
|
|
110
|
+
"## Mission",
|
|
111
|
+
ctx.mission || "(no mission defined)",
|
|
112
|
+
"",
|
|
113
|
+
"## Metric Based Mission Complete Assessment",
|
|
114
|
+
metricAssessment.assessment,
|
|
115
|
+
"",
|
|
116
|
+
"### Mission-Complete Metrics",
|
|
117
|
+
metricAssessment.table,
|
|
118
|
+
"",
|
|
119
|
+
"## Repository State",
|
|
120
|
+
`### Open Issues (${ctx.issuesSummary.length})`,
|
|
121
|
+
ctx.issuesSummary.join("\n") || "none",
|
|
122
|
+
"",
|
|
123
|
+
`### Recently Closed Issues (${ctx.recentlyClosedSummary.length})`,
|
|
124
|
+
ctx.recentlyClosedSummary.join("\n") || "none",
|
|
125
|
+
"",
|
|
126
|
+
`### Open PRs (${ctx.prsSummary.length})`,
|
|
127
|
+
ctx.prsSummary.join("\n") || "none",
|
|
128
|
+
"",
|
|
129
|
+
...(ctx.sourceExports?.length > 0
|
|
130
|
+
? [
|
|
131
|
+
`### Source Exports`,
|
|
132
|
+
...ctx.sourceExports.map((e) => `- ${e}`),
|
|
133
|
+
"",
|
|
134
|
+
]
|
|
135
|
+
: []),
|
|
136
|
+
`### Test Coverage`,
|
|
137
|
+
ctx.hasDedicatedTests
|
|
138
|
+
? `Dedicated test files: ${ctx.dedicatedTestFiles.join(", ")}`
|
|
139
|
+
: "**No dedicated test files found.**",
|
|
140
|
+
"",
|
|
141
|
+
`### Source TODO Count: ${ctx.sourceTodoCount}`,
|
|
142
|
+
"",
|
|
143
|
+
`### Transformation Budget: ${ctx.cumulativeTransformationCost}/${ctx.transformationBudget || "unlimited"}`,
|
|
144
|
+
"",
|
|
145
|
+
`### Recent Activity`,
|
|
146
|
+
ctx.recentActivity || "none",
|
|
147
|
+
"",
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse the director's LLM response.
|
|
153
|
+
*/
|
|
154
|
+
function parseDirectorResponse(content) {
|
|
155
|
+
const decisionMatch = content.match(/\[DECISION\]([\s\S]*?)\[\/DECISION\]/);
|
|
156
|
+
const reasonMatch = content.match(/\[REASON\]([\s\S]*?)\[\/REASON\]/);
|
|
157
|
+
const analysisMatch = content.match(/\[ANALYSIS\]([\s\S]*?)\[\/ANALYSIS\]/);
|
|
158
|
+
|
|
159
|
+
const decision = decisionMatch ? decisionMatch[1].trim().toLowerCase() : "in-progress";
|
|
160
|
+
const reason = reasonMatch ? reasonMatch[1].trim() : "";
|
|
161
|
+
const analysis = analysisMatch ? analysisMatch[1].trim() : content.substring(0, 500);
|
|
162
|
+
|
|
163
|
+
return { decision, reason, analysis };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute mission-complete: write signal file and commit via Contents API.
|
|
168
|
+
*/
|
|
169
|
+
async function executeMissionComplete(octokit, repo, reason) {
|
|
170
|
+
const signal = [
|
|
171
|
+
"# Mission Complete",
|
|
172
|
+
"",
|
|
173
|
+
`- **Timestamp:** ${new Date().toISOString()}`,
|
|
174
|
+
`- **Detected by:** director`,
|
|
175
|
+
`- **Reason:** ${reason}`,
|
|
176
|
+
"",
|
|
177
|
+
"This file was created automatically. To restart transformations, delete this file or run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
|
|
178
|
+
].join("\n");
|
|
179
|
+
writeFileSync("MISSION_COMPLETE.md", signal);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const contentBase64 = Buffer.from(signal).toString("base64");
|
|
183
|
+
let existingSha;
|
|
184
|
+
try {
|
|
185
|
+
const { data } = await octokit.rest.repos.getContent({ ...repo, path: "MISSION_COMPLETE.md", ref: "main" });
|
|
186
|
+
existingSha = data.sha;
|
|
187
|
+
} catch { /* doesn't exist yet */ }
|
|
188
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
189
|
+
...repo,
|
|
190
|
+
path: "MISSION_COMPLETE.md",
|
|
191
|
+
message: "mission-complete: " + reason.substring(0, 72),
|
|
192
|
+
content: contentBase64,
|
|
193
|
+
branch: "main",
|
|
194
|
+
...(existingSha ? { sha: existingSha } : {}),
|
|
195
|
+
});
|
|
196
|
+
core.info("MISSION_COMPLETE.md committed to main");
|
|
197
|
+
} catch (err) {
|
|
198
|
+
core.warning(`Could not commit MISSION_COMPLETE.md: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute mission-failed: write signal file and commit via Contents API.
|
|
204
|
+
*/
|
|
205
|
+
async function executeMissionFailed(octokit, repo, reason) {
|
|
206
|
+
const signal = [
|
|
207
|
+
"# Mission Failed",
|
|
208
|
+
"",
|
|
209
|
+
`- **Timestamp:** ${new Date().toISOString()}`,
|
|
210
|
+
`- **Detected by:** director`,
|
|
211
|
+
`- **Reason:** ${reason}`,
|
|
212
|
+
"",
|
|
213
|
+
"This file was created automatically. To restart, delete this file and run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
|
|
214
|
+
].join("\n");
|
|
215
|
+
writeFileSync("MISSION_FAILED.md", signal);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const contentBase64 = Buffer.from(signal).toString("base64");
|
|
219
|
+
let existingSha;
|
|
220
|
+
try {
|
|
221
|
+
const { data } = await octokit.rest.repos.getContent({ ...repo, path: "MISSION_FAILED.md", ref: "main" });
|
|
222
|
+
existingSha = data.sha;
|
|
223
|
+
} catch { /* doesn't exist yet */ }
|
|
224
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
225
|
+
...repo,
|
|
226
|
+
path: "MISSION_FAILED.md",
|
|
227
|
+
message: "mission-failed: " + reason.substring(0, 72),
|
|
228
|
+
content: contentBase64,
|
|
229
|
+
branch: "main",
|
|
230
|
+
...(existingSha ? { sha: existingSha } : {}),
|
|
231
|
+
});
|
|
232
|
+
core.info("MISSION_FAILED.md committed to main");
|
|
233
|
+
} catch (err) {
|
|
234
|
+
core.warning(`Could not commit MISSION_FAILED.md: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Director task: evaluate mission readiness and produce a decision or gap analysis.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object} context - Task context from index.js
|
|
242
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
243
|
+
*/
|
|
244
|
+
export async function direct(context) {
|
|
245
|
+
const { octokit, repo, config, instructions, model } = context;
|
|
246
|
+
const t = config.tuning || {};
|
|
247
|
+
|
|
248
|
+
// --- Gather context (similar to supervisor but focused on metrics) ---
|
|
249
|
+
const mission = readOptionalFile(config.paths.mission.path);
|
|
250
|
+
const intentionLogFull = readOptionalFile(config.intentionBot.intentionFilepath);
|
|
251
|
+
const recentActivity = intentionLogFull.split("\n").slice(-20).join("\n");
|
|
252
|
+
|
|
253
|
+
const costMatches = intentionLogFull.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
|
|
254
|
+
const cumulativeTransformationCost = [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
|
|
255
|
+
|
|
256
|
+
const missionComplete = existsSync("MISSION_COMPLETE.md");
|
|
257
|
+
const missionFailed = existsSync("MISSION_FAILED.md");
|
|
258
|
+
const transformationBudget = config.transformationBudget || 0;
|
|
259
|
+
|
|
260
|
+
// If already decided, skip
|
|
261
|
+
if (missionComplete) {
|
|
262
|
+
return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md exists)" };
|
|
263
|
+
}
|
|
264
|
+
if (missionFailed) {
|
|
265
|
+
return { outcome: "nop", details: "Mission already failed (MISSION_FAILED.md exists)" };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Skip in maintenance mode
|
|
269
|
+
if (config.supervisor === "maintenance") {
|
|
270
|
+
return { outcome: "nop", details: "Maintenance mode — director skipped" };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const initTimestamp = config.init?.timestamp || null;
|
|
274
|
+
|
|
275
|
+
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
276
|
+
...repo, state: "open", per_page: t.issuesScan || 20, sort: "created", direction: "asc",
|
|
277
|
+
});
|
|
278
|
+
const issuesOnly = openIssues.filter((i) => !i.pull_request);
|
|
279
|
+
const filteredIssues = filterIssues(issuesOnly, { staleDays: t.staleDays || 30, initTimestamp });
|
|
280
|
+
const issuesSummary = filteredIssues.map((i) => {
|
|
281
|
+
const labels = i.labels.map((l) => l.name).join(", ");
|
|
282
|
+
return `#${i.number}: ${i.title} [${labels || "no labels"}]`;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Recently closed issues
|
|
286
|
+
let recentlyClosedSummary = [];
|
|
287
|
+
let resolvedCount = 0;
|
|
288
|
+
try {
|
|
289
|
+
const { data: closedIssuesRaw } = await octokit.rest.issues.listForRepo({
|
|
290
|
+
...repo, state: "closed", labels: "automated", per_page: 10, sort: "updated", direction: "desc",
|
|
291
|
+
});
|
|
292
|
+
const initEpoch = initTimestamp ? new Date(initTimestamp).getTime() : 0;
|
|
293
|
+
const closedFiltered = closedIssuesRaw.filter((i) =>
|
|
294
|
+
!i.pull_request && (initEpoch <= 0 || new Date(i.created_at).getTime() >= initEpoch)
|
|
295
|
+
);
|
|
296
|
+
for (const ci of closedFiltered) {
|
|
297
|
+
let closeReason = "closed";
|
|
298
|
+
try {
|
|
299
|
+
const { data: comments } = await octokit.rest.issues.listComments({
|
|
300
|
+
...repo, issue_number: ci.number, per_page: 5, sort: "created", direction: "desc",
|
|
301
|
+
});
|
|
302
|
+
if (comments.some((c) => c.body?.includes("Automated Review Result"))) {
|
|
303
|
+
closeReason = "RESOLVED";
|
|
304
|
+
} else {
|
|
305
|
+
const { data: events } = await octokit.rest.issues.listEvents({
|
|
306
|
+
...repo, issue_number: ci.number, per_page: 10,
|
|
307
|
+
});
|
|
308
|
+
if (events.some((e) => e.event === "closed" && e.commit_id)) {
|
|
309
|
+
closeReason = "RESOLVED";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Check for automerge closure (issue has "merged" label — set by ci-automerge)
|
|
313
|
+
if (closeReason !== "RESOLVED") {
|
|
314
|
+
const issueLabels = ci.labels.map((l) => (typeof l === "string" ? l : l.name));
|
|
315
|
+
if (issueLabels.includes("merged")) {
|
|
316
|
+
closeReason = "RESOLVED";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch { /* ignore */ }
|
|
320
|
+
if (closeReason === "RESOLVED") resolvedCount++;
|
|
321
|
+
recentlyClosedSummary.push(`#${ci.number}: ${ci.title} — ${closeReason}`);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
core.warning(`Could not fetch recently closed issues: ${err.message}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Open PRs
|
|
328
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
329
|
+
...repo, state: "open", per_page: 10, sort: "updated", direction: "desc",
|
|
330
|
+
});
|
|
331
|
+
const prsSummary = openPRs.map((pr) => `#${pr.number}: ${pr.title} (${pr.head.ref})`);
|
|
332
|
+
|
|
333
|
+
// Source exports
|
|
334
|
+
let sourceExports = [];
|
|
335
|
+
try {
|
|
336
|
+
const sourcePath = config.paths.source?.path || "src/lib/";
|
|
337
|
+
if (existsSync(sourcePath)) {
|
|
338
|
+
const sourceFiles = scanDirectory(sourcePath, [".js", ".ts"], { limit: 5 });
|
|
339
|
+
for (const sf of sourceFiles) {
|
|
340
|
+
const content = readFileSync(sf.path, "utf8");
|
|
341
|
+
const exports = [...content.matchAll(/export\s+(?:async\s+)?(?:function|const|let|var|class)\s+(\w+)/g)]
|
|
342
|
+
.map((m) => m[1]);
|
|
343
|
+
if (exports.length > 0) {
|
|
344
|
+
sourceExports.push(`${sf.name}: ${exports.join(", ")}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch { /* ignore */ }
|
|
349
|
+
|
|
350
|
+
// Dedicated tests
|
|
351
|
+
const { hasDedicatedTests, dedicatedTestFiles } = detectDedicatedTests();
|
|
352
|
+
|
|
353
|
+
// TODO count
|
|
354
|
+
const sourcePath = config.paths.source?.path || "src/lib/";
|
|
355
|
+
const sourceDir = sourcePath.endsWith("/") ? sourcePath.slice(0, -1) : sourcePath;
|
|
356
|
+
const srcRoot = sourceDir.includes("/") ? sourceDir.split("/").slice(0, -1).join("/") || "src" : "src";
|
|
357
|
+
const sourceTodoCount = countTodos(srcRoot);
|
|
358
|
+
|
|
359
|
+
// Build context
|
|
360
|
+
const ctx = {
|
|
361
|
+
mission,
|
|
362
|
+
recentActivity,
|
|
363
|
+
issuesSummary,
|
|
364
|
+
recentlyClosedSummary,
|
|
365
|
+
resolvedCount,
|
|
366
|
+
prsSummary,
|
|
367
|
+
sourceExports,
|
|
368
|
+
hasDedicatedTests,
|
|
369
|
+
dedicatedTestFiles,
|
|
370
|
+
sourceTodoCount,
|
|
371
|
+
cumulativeTransformationCost,
|
|
372
|
+
transformationBudget,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Build metric-based advisory
|
|
376
|
+
const metricAssessment = buildMetricAssessment(ctx, config);
|
|
377
|
+
core.info(`Metric assessment: ${metricAssessment.assessment}`);
|
|
378
|
+
|
|
379
|
+
// --- LLM decision ---
|
|
380
|
+
const agentInstructions = instructions || "You are the director. Evaluate mission readiness.";
|
|
381
|
+
const prompt = buildPrompt(ctx, agentInstructions, metricAssessment);
|
|
382
|
+
|
|
383
|
+
const { content, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
384
|
+
model,
|
|
385
|
+
systemMessage:
|
|
386
|
+
"You are the director of an autonomous coding repository. Your job is to evaluate whether the mission is complete, failed, or in progress. You produce a structured assessment — you do NOT dispatch workflows or create issues.",
|
|
387
|
+
prompt,
|
|
388
|
+
writablePaths: [],
|
|
389
|
+
tuning: t,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const { decision, reason, analysis } = parseDirectorResponse(content);
|
|
393
|
+
core.info(`Director decision: ${decision} — ${reason}`);
|
|
394
|
+
|
|
395
|
+
// Execute the decision
|
|
396
|
+
let outcome = "directed";
|
|
397
|
+
if (decision === "mission-complete" && metricAssessment.allMet) {
|
|
398
|
+
if (process.env.GITHUB_REPOSITORY !== "xn-intenton-z2a/agentic-lib") {
|
|
399
|
+
await executeMissionComplete(octokit, repo, reason);
|
|
400
|
+
outcome = "mission-complete";
|
|
401
|
+
}
|
|
402
|
+
} else if (decision === "mission-complete" && !metricAssessment.allMet) {
|
|
403
|
+
core.info("Director chose mission-complete but metrics are NOT all met — overriding to in-progress");
|
|
404
|
+
outcome = "directed";
|
|
405
|
+
} else if (decision === "mission-failed") {
|
|
406
|
+
if (process.env.GITHUB_REPOSITORY !== "xn-intenton-z2a/agentic-lib") {
|
|
407
|
+
await executeMissionFailed(octokit, repo, reason);
|
|
408
|
+
outcome = "mission-failed";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Set output for downstream jobs to check
|
|
413
|
+
core.setOutput("director-decision", decision);
|
|
414
|
+
core.setOutput("director-analysis", analysis.substring(0, 500));
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
outcome,
|
|
418
|
+
tokensUsed,
|
|
419
|
+
inputTokens,
|
|
420
|
+
outputTokens,
|
|
421
|
+
cost,
|
|
422
|
+
model,
|
|
423
|
+
details: `Decision: ${decision}\nReason: ${reason}\nAnalysis: ${analysis.substring(0, 300)}`,
|
|
424
|
+
narrative: `Director: ${reason}`,
|
|
425
|
+
metricAssessment: metricAssessment.assessment,
|
|
426
|
+
directorAnalysis: analysis,
|
|
427
|
+
hasDedicatedTests,
|
|
428
|
+
resolvedCount,
|
|
429
|
+
changes: outcome === "mission-complete"
|
|
430
|
+
? [{ action: "mission-complete", file: "MISSION_COMPLETE.md", sizeInfo: reason.substring(0, 100) }]
|
|
431
|
+
: outcome === "mission-failed"
|
|
432
|
+
? [{ action: "mission-failed", file: "MISSION_FAILED.md", sizeInfo: reason.substring(0, 100) }]
|
|
433
|
+
: [],
|
|
434
|
+
};
|
|
435
|
+
}
|