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

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.
@@ -333,3 +333,59 @@ jobs:
333
333
  exit 1
334
334
  fi
335
335
  done
336
+
337
+ # W8: Create initial seed issues after purge so the pipeline has work to do
338
+ - name: Create initial seed issues
339
+ if: github.repository != 'xn-intenton-z2a/agentic-lib' && env.INIT_MODE == 'purge' && needs.params.outputs.dry-run != 'true'
340
+ uses: actions/github-script@v8
341
+ with:
342
+ script: |
343
+ const fs = require('fs');
344
+ const missionContent = fs.existsSync('MISSION.md')
345
+ ? fs.readFileSync('MISSION.md', 'utf8')
346
+ : '(no MISSION.md found)';
347
+
348
+ // Ensure labels exist
349
+ for (const label of ['automated', 'ready']) {
350
+ try {
351
+ await github.rest.issues.createLabel({
352
+ ...context.repo, name: label,
353
+ color: label === 'automated' ? '0e8a16' : '1d76db',
354
+ description: label === 'automated' ? 'Created by automation' : 'Ready for dev transform',
355
+ });
356
+ } catch (e) { /* label already exists */ }
357
+ }
358
+
359
+ // W8a: Initial unit tests issue
360
+ const unitTestBody = [
361
+ 'Create a unit test file for each of the major features in the mission ',
362
+ 'and put a TODO in a trivial empty passing test in each.',
363
+ '',
364
+ '## MISSION.md',
365
+ '',
366
+ missionContent,
367
+ ].join('\n');
368
+ const { data: issue1 } = await github.rest.issues.create({
369
+ ...context.repo,
370
+ title: 'Initial unit tests',
371
+ body: unitTestBody,
372
+ labels: ['automated', 'ready'],
373
+ });
374
+ core.info(`Created issue #${issue1.number}: Initial unit tests`);
375
+
376
+ // W8b: Initial web layout issue
377
+ const webLayoutBody = [
378
+ 'Create the home page layout to showcase each of the major features in the mission ',
379
+ 'and put a TODO in a trivial empty passing test in each.',
380
+ '',
381
+ '## MISSION.md',
382
+ '',
383
+ missionContent,
384
+ ].join('\n');
385
+ const { data: issue2 } = await github.rest.issues.create({
386
+ ...context.repo,
387
+ title: 'Initial web layout',
388
+ body: webLayoutBody,
389
+ labels: ['automated', 'ready'],
390
+ });
391
+ core.info(`Created issue #${issue2.number}: Initial web layout`);
@@ -90,8 +90,13 @@ jobs:
90
90
  - name: Install dependencies
91
91
  run: npm ci
92
92
 
93
- - name: Run behaviour tests
94
- run: npm run test:behaviour
93
+ - name: Run behaviour tests (with retry)
94
+ run: |
95
+ npm run test:behaviour || {
96
+ echo "::warning::Behaviour test attempt 1 failed — retrying"
97
+ sleep 2
98
+ npm run test:behaviour
99
+ }
95
100
  #env:
96
101
  # HOME: /root
97
102
 
@@ -621,14 +621,55 @@ jobs:
621
621
  commit-message: "agentic-step: maintain features and library"
622
622
  push-ref: ${{ github.ref_name }}
623
623
 
624
- # ─── Supervisor: LLM decides what to do (after maintain has features) ──
625
- supervisor:
626
- needs: [params, pr-cleanup, telemetry, maintain]
624
+ # ─── Director: LLM evaluates mission status (complete/failed/in-progress) ──
625
+ director:
626
+ needs: [params, telemetry, maintain]
627
627
  if: |
628
628
  !cancelled() &&
629
629
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
630
630
  needs.params.result == 'success'
631
631
  runs-on: ubuntu-latest
632
+ outputs:
633
+ decision: ${{ steps.director.outputs.director-decision }}
634
+ analysis: ${{ steps.director.outputs.director-analysis }}
635
+ steps:
636
+ - uses: actions/checkout@v6
637
+
638
+ - uses: actions/setup-node@v6
639
+ with:
640
+ node-version: "24"
641
+
642
+ - name: Self-init (agentic-lib dev only)
643
+ if: hashFiles('scripts/self-init.sh') != '' && hashFiles('.github/agentic-lib/actions/agentic-step/package.json') == ''
644
+ run: bash scripts/self-init.sh
645
+
646
+ - name: Install agentic-step dependencies
647
+ working-directory: .github/agentic-lib/actions/agentic-step
648
+ run: npm ci
649
+
650
+ - name: Run director
651
+ id: director
652
+ if: github.repository != 'xn-intenton-z2a/agentic-lib'
653
+ uses: ./.github/agentic-lib/actions/agentic-step
654
+ env:
655
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
656
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
657
+ with:
658
+ task: "direct"
659
+ config: ${{ needs.params.outputs.config-path }}
660
+ instructions: ".github/agentic-lib/agents/agent-director.md"
661
+ model: ${{ needs.params.outputs.model }}
662
+
663
+ # ─── Supervisor: LLM decides what to do (after director evaluates) ──
664
+ supervisor:
665
+ needs: [params, pr-cleanup, telemetry, maintain, director]
666
+ if: |
667
+ !cancelled() &&
668
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
669
+ needs.params.result == 'success' &&
670
+ needs.director.outputs.decision != 'mission-complete' &&
671
+ needs.director.outputs.decision != 'mission-failed'
672
+ runs-on: ubuntu-latest
632
673
  steps:
633
674
  - uses: actions/checkout@v6
634
675
 
@@ -1175,6 +1216,12 @@ jobs:
1175
1216
  set +e
1176
1217
  npm run --if-present test:behaviour 2>&1 | tail -30
1177
1218
  EXIT_CODE=$?
1219
+ if [ $EXIT_CODE -ne 0 ]; then
1220
+ echo "::warning::Behaviour test attempt 1 failed — retrying"
1221
+ sleep 2
1222
+ npm run --if-present test:behaviour 2>&1 | tail -30
1223
+ EXIT_CODE=$?
1224
+ fi
1178
1225
  set -e
1179
1226
  if [ $EXIT_CODE -ne 0 ]; then
1180
1227
  echo "tests-passed=false" >> $GITHUB_OUTPUT
package/agentic-lib.toml CHANGED
@@ -130,5 +130,12 @@ max-attempts-per-issue = 4 # max transform attempts before aband
130
130
  features-limit = 8 # max feature files in features/ directory
131
131
  library-limit = 64 # max library entries in library/ directory
132
132
 
133
+ [mission-complete]
134
+ # Thresholds for deterministic mission-complete declaration.
135
+ # All conditions must be met simultaneously.
136
+ min-resolved-issues = 3 # minimum closed-as-RESOLVED issues since init
137
+ require-dedicated-tests = true # require test files that import from src/lib/
138
+ max-source-todos = 0 # max TODO comments allowed in ./src (0 = none)
139
+
133
140
  [bot]
134
141
  log-file = "test/intentïon.md" #@dist "intentïon.md"
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.6",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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;