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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.github/workflows/agentic-lib-init.yml +56 -0
  2. package/.github/workflows/agentic-lib-test.yml +7 -2
  3. package/.github/workflows/agentic-lib-workflow.yml +50 -3
  4. package/README.md +88 -17
  5. package/agentic-lib.toml +7 -0
  6. package/bin/agentic-lib.js +260 -496
  7. package/package.json +2 -1
  8. package/src/actions/agentic-step/config-loader.js +9 -0
  9. package/src/actions/agentic-step/index.js +104 -7
  10. package/src/actions/agentic-step/tasks/direct.js +435 -0
  11. package/src/actions/agentic-step/tasks/supervise.js +107 -180
  12. package/src/agents/agent-apply-fix.md +5 -2
  13. package/src/agents/agent-director.md +58 -0
  14. package/src/agents/agent-discovery.md +52 -0
  15. package/src/agents/agent-issue-resolution.md +18 -0
  16. package/src/agents/agent-iterate.md +45 -0
  17. package/src/agents/agent-supervisor.md +22 -50
  18. package/src/copilot/agents.js +39 -0
  19. package/src/copilot/config.js +308 -0
  20. package/src/copilot/context.js +318 -0
  21. package/src/copilot/hybrid-session.js +330 -0
  22. package/src/copilot/logger.js +43 -0
  23. package/src/copilot/sdk.js +36 -0
  24. package/src/copilot/session.js +372 -0
  25. package/src/copilot/tasks/fix-code.js +73 -0
  26. package/src/copilot/tasks/maintain-features.js +61 -0
  27. package/src/copilot/tasks/maintain-library.js +66 -0
  28. package/src/copilot/tasks/transform.js +120 -0
  29. package/src/copilot/tools.js +141 -0
  30. package/src/mcp/server.js +43 -25
  31. package/src/seeds/zero-README.md +31 -0
  32. package/src/seeds/zero-behaviour.test.js +12 -4
  33. package/src/seeds/zero-package.json +1 -1
  34. package/src/seeds/zero-playwright.config.js +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.2.5",
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: ">= 1", status: resolvedCount >= 1 ? "MET" : "NOT MET" },
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
- const conditionsMet = openIssues === 0 && openPrs === 0 && resolved >= 1;
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 (conditionsMet) {
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
- if (resolved < 1) parts.push("No issues have been resolved yet.");
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
- const transformationCost = COST_TASKS.includes(task) && !isNop ? 1 : 0;
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
+ }