@xn-intenton-z2a/agentic-lib 7.1.94 → 7.1.95

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.
@@ -107,6 +107,18 @@ jobs:
107
107
  let content = fs.readFileSync(workflowPath, 'utf8');
108
108
  const cron = SCHEDULE_MAP[frequency];
109
109
 
110
+ // Check if the frequency is already set — skip if no-op
111
+ const supervisorRegex2 = /^\s*supervisor\s*=\s*"([^"]*)"/m;
112
+ if (fs.existsSync(tomlPath)) {
113
+ const currentToml = fs.readFileSync(tomlPath, 'utf8');
114
+ const currentMatch = currentToml.match(supervisorRegex2);
115
+ const currentFreq = currentMatch ? currentMatch[1] : '';
116
+ if (currentFreq === frequency) {
117
+ core.info(`Schedule already set to ${frequency} — no change needed`);
118
+ return;
119
+ }
120
+ }
121
+
110
122
  // Remove any existing schedule block
111
123
  content = content.replace(/\n schedule:\n - cron: "[^"]*"\n/g, '\n');
112
124
 
@@ -11,6 +11,10 @@
11
11
  name: agentic-lib-test
12
12
  run-name: "agentic-lib-test [${{ github.ref_name }}]"
13
13
 
14
+ #@dist concurrency:
15
+ #@dist group: agentic-lib-test-${{ github.ref_name }}
16
+ #@dist cancel-in-progress: true
17
+
14
18
  on:
15
19
  schedule:
16
20
  - cron: "10 * * * *"
@@ -115,12 +119,33 @@ jobs:
115
119
  if: >-
116
120
  !cancelled()
117
121
  && github.ref == 'refs/heads/main'
118
- && github.event_name != 'pull_request'
122
+ && (github.event_name == 'push' || github.event_name == 'schedule')
119
123
  && github.repository != 'xn-intenton-z2a/agentic-lib'
120
124
  && (needs.test.result == 'failure' || needs.behaviour.result == 'failure')
121
125
  runs-on: ubuntu-latest
122
126
  steps:
127
+ - name: Check circuit breaker
128
+ id: breaker
129
+ env:
130
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
131
+ run: |
132
+ # Count agentic-lib-workflow dispatches in last 30 minutes
133
+ SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30M +%Y-%m-%dT%H:%M:%SZ)
134
+ COUNT=$(gh run list \
135
+ --repo "${{ github.repository }}" \
136
+ --workflow agentic-lib-workflow.yml \
137
+ --json createdAt \
138
+ --jq "[.[] | select(.createdAt >= \"$SINCE\")] | length")
139
+ echo "recent-dispatches=$COUNT"
140
+ if [ "$COUNT" -ge 3 ]; then
141
+ echo "Circuit breaker tripped: $COUNT dispatches in last 30 min"
142
+ echo "tripped=true" >> $GITHUB_OUTPUT
143
+ else
144
+ echo "tripped=false" >> $GITHUB_OUTPUT
145
+ fi
146
+
123
147
  - name: Dispatch agentic-lib-workflow to fix broken build
148
+ if: steps.breaker.outputs.tripped != 'true'
124
149
  env:
125
150
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126
151
  run: |
@@ -11,9 +11,9 @@
11
11
 
12
12
  name: agentic-lib-workflow
13
13
  run-name: "agentic-lib-workflow [${{ github.ref_name }}]"
14
- concurrency:
15
- group: agentic-lib-workflow
16
- cancel-in-progress: false
14
+ #@dist concurrency:
15
+ #@dist group: agentic-lib-workflow
16
+ #@dist cancel-in-progress: false
17
17
 
18
18
  on:
19
19
  #@dist schedule:
@@ -123,13 +123,25 @@ jobs:
123
123
  params:
124
124
  runs-on: ubuntu-latest
125
125
  steps:
126
+ - uses: actions/checkout@v6
127
+ with:
128
+ sparse-checkout: ${{ env.configPath }}
129
+ sparse-checkout-cone-mode: false
126
130
  - name: Normalise params
127
131
  id: normalise
128
132
  shell: bash
129
133
  run: |
130
134
  MODEL='${{ inputs.model }}'
135
+ if [ -z "$MODEL" ] && [ -f "${{ env.configPath }}" ]; then
136
+ TOML_MODEL=$(grep '^\s*model' "${{ env.configPath }}" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
137
+ MODEL="${TOML_MODEL}"
138
+ fi
131
139
  echo "model=${MODEL:-gpt-5-mini}" >> $GITHUB_OUTPUT
132
140
  PROFILE='${{ inputs.profile }}'
141
+ if [ -z "$PROFILE" ] && [ -f "${{ env.configPath }}" ]; then
142
+ TOML_PROFILE=$(grep '^\s*profile' "${{ env.configPath }}" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
143
+ PROFILE="${TOML_PROFILE}"
144
+ fi
133
145
  echo "profile=${PROFILE}" >> $GITHUB_OUTPUT
134
146
  MODE='${{ inputs.mode }}'
135
147
  echo "mode=${MODE:-full}" >> $GITHUB_OUTPUT
@@ -242,6 +254,52 @@ jobs:
242
254
  runs-on: ubuntu-latest
243
255
  steps:
244
256
  - uses: actions/checkout@v6
257
+
258
+ - uses: actions/setup-node@v6
259
+ with:
260
+ node-version: 24
261
+ cache: "npm"
262
+
263
+ - name: Install dependencies (non-blocking)
264
+ id: install-deps
265
+ run: npm ci 2>/dev/null || echo "npm ci failed (non-blocking)"
266
+
267
+ - name: Run unit tests (non-blocking)
268
+ id: unit-tests
269
+ run: |
270
+ set +e
271
+ TEST_CMD=""
272
+ if [ -f agentic-lib.toml ]; then
273
+ TEST_CMD=$(grep '^\s*test\s*=' agentic-lib.toml 2>/dev/null | head -1 | sed 's/.*=\s*"\([^"]*\)".*/\1/')
274
+ fi
275
+ TEST_CMD="${TEST_CMD:-npm test}"
276
+ OUTPUT=$(eval "$TEST_CMD" 2>&1)
277
+ EXIT_CODE=$?
278
+ echo "exit-code=$EXIT_CODE" >> $GITHUB_OUTPUT
279
+ PASS_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ pass' | head -1 | grep -oP '\d+' || echo "0")
280
+ FAIL_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ fail' | head -1 | grep -oP '\d+' || echo "0")
281
+ echo "pass-count=$PASS_COUNT" >> $GITHUB_OUTPUT
282
+ echo "fail-count=$FAIL_COUNT" >> $GITHUB_OUTPUT
283
+ echo "$OUTPUT" | tail -100 > /tmp/unit-test-output.txt
284
+ exit 0
285
+
286
+ - name: Run behaviour tests (non-blocking)
287
+ id: behaviour-tests
288
+ if: hashFiles('playwright.config.js') != '' || hashFiles('playwright.config.ts') != ''
289
+ run: |
290
+ set +e
291
+ npx playwright install --with-deps chromium 2>/dev/null || true
292
+ npm run build:web 2>/dev/null || true
293
+ OUTPUT=$(npm run --if-present test:behaviour 2>&1)
294
+ EXIT_CODE=$?
295
+ echo "exit-code=$EXIT_CODE" >> $GITHUB_OUTPUT
296
+ PASS_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ passed' | head -1 | grep -oP '\d+' || echo "0")
297
+ FAIL_COUNT=$(echo "$OUTPUT" | grep -oP '\d+ failed' | head -1 | grep -oP '\d+' || echo "0")
298
+ echo "pass-count=$PASS_COUNT" >> $GITHUB_OUTPUT
299
+ echo "fail-count=$FAIL_COUNT" >> $GITHUB_OUTPUT
300
+ echo "$OUTPUT" | tail -50 > /tmp/behaviour-test-output.txt
301
+ exit 0
302
+
245
303
  - name: Gather telemetry
246
304
  id: gather
247
305
  uses: actions/github-script@v8
@@ -293,6 +351,72 @@ jobs:
293
351
  // Message from bot/human
294
352
  const message = '${{ needs.params.outputs.message }}';
295
353
 
354
+ // Latest external test workflow run
355
+ let externalTestResults = null;
356
+ try {
357
+ const testRuns = await github.rest.actions.listWorkflowRuns({
358
+ owner, repo,
359
+ workflow_id: 'agentic-lib-test.yml',
360
+ branch: 'main',
361
+ per_page: 1,
362
+ });
363
+ const latestTest = testRuns.data.workflow_runs[0];
364
+ if (latestTest) {
365
+ const jobs = await github.rest.actions.listJobsForWorkflowRun({
366
+ owner, repo,
367
+ run_id: latestTest.id,
368
+ });
369
+ externalTestResults = {
370
+ runId: latestTest.id,
371
+ conclusion: latestTest.conclusion,
372
+ created: latestTest.created_at,
373
+ jobs: jobs.data.jobs.map(j => ({
374
+ name: j.name, conclusion: j.conclusion,
375
+ })),
376
+ };
377
+ }
378
+ } catch (e) { /* ignore */ }
379
+
380
+ // Source file stats
381
+ let sourceStats = null;
382
+ try {
383
+ const sourcePath = 'src/lib/';
384
+ if (fs.existsSync(sourcePath)) {
385
+ const files = fs.readdirSync(sourcePath).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
386
+ sourceStats = files.map(f => {
387
+ const content = fs.readFileSync(`${sourcePath}${f}`, 'utf8');
388
+ const lines = content.split('\n').length;
389
+ const exports = [...content.matchAll(/export\s+(?:async\s+)?(?:function|const|class)\s+(\w+)/g)].map(m => m[1]);
390
+ return { file: f, lines, exports };
391
+ });
392
+ }
393
+ } catch (e) {}
394
+
395
+ // Mission complete/failed signals
396
+ const missionComplete = fs.existsSync('MISSION_COMPLETE.md');
397
+ const missionFailed = fs.existsSync('MISSION_FAILED.md');
398
+
399
+ // Activity log stats
400
+ let activityStats = null;
401
+ try {
402
+ const logPath = fs.existsSync('intenti\u00F6n.md') ? 'intenti\u00F6n.md' : (fs.existsSync('intention.md') ? 'intention.md' : null);
403
+ if (logPath) {
404
+ const log = fs.readFileSync(logPath, 'utf8');
405
+ const entries = log.split('\n## ').length - 1;
406
+ const costMatches = [...log.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g)];
407
+ const totalCost = costMatches.reduce((sum, m) => sum + parseInt(m[1], 10), 0);
408
+ activityStats = { entries, totalTransformCost: totalCost };
409
+ }
410
+ } catch (e) {}
411
+
412
+ // Live test results from earlier steps
413
+ const unitOutput = fs.existsSync('/tmp/unit-test-output.txt')
414
+ ? fs.readFileSync('/tmp/unit-test-output.txt', 'utf8').slice(0, 5000)
415
+ : '';
416
+ const behaviourOutput = fs.existsSync('/tmp/behaviour-test-output.txt')
417
+ ? fs.readFileSync('/tmp/behaviour-test-output.txt', 'utf8').slice(0, 2500)
418
+ : '';
419
+
296
420
  const telemetry = {
297
421
  issues: issuesSummary,
298
422
  prs: prsSummary,
@@ -300,13 +424,42 @@ jobs:
300
424
  mission: mission.slice(0, 500),
301
425
  featureFiles: features,
302
426
  message: message || null,
427
+ liveTests: {
428
+ unit: {
429
+ exitCode: parseInt('${{ steps.unit-tests.outputs.exit-code }}' || '0'),
430
+ passCount: parseInt('${{ steps.unit-tests.outputs.pass-count }}' || '0'),
431
+ failCount: parseInt('${{ steps.unit-tests.outputs.fail-count }}' || '0'),
432
+ output: unitOutput,
433
+ },
434
+ behaviour: {
435
+ exitCode: parseInt('${{ steps.behaviour-tests.outputs.exit-code }}' || '-1'),
436
+ passCount: parseInt('${{ steps.behaviour-tests.outputs.pass-count }}' || '0'),
437
+ failCount: parseInt('${{ steps.behaviour-tests.outputs.fail-count }}' || '0'),
438
+ output: behaviourOutput,
439
+ },
440
+ },
441
+ externalTestResults,
442
+ sourceStats,
443
+ activityStats,
444
+ missionComplete,
445
+ missionFailed,
303
446
  };
304
447
 
305
448
  // Write to file for downstream jobs
306
449
  fs.writeFileSync('/tmp/telemetry.json', JSON.stringify(telemetry, null, 2));
307
- // Set as output (truncated for GitHub output limits)
450
+ // Set as output (clipped by tuning profile)
451
+ let maxTelemetryChars = 30000;
452
+ try {
453
+ if (fs.existsSync('agentic-lib.toml')) {
454
+ const toml = fs.readFileSync('agentic-lib.toml', 'utf8');
455
+ const profileMatch = toml.match(/^\s*profile\s*=\s*"(\w+)"/m);
456
+ const profile = profileMatch ? profileMatch[1] : 'recommended';
457
+ const PROFILE_LIMITS = { min: 10000, recommended: 30000, max: 60000 };
458
+ maxTelemetryChars = PROFILE_LIMITS[profile] || 30000;
459
+ }
460
+ } catch (e) {}
308
461
  const summary = JSON.stringify(telemetry);
309
- core.setOutput('telemetry', summary.slice(0, 60000));
462
+ core.setOutput('telemetry', summary.slice(0, maxTelemetryChars));
310
463
  outputs:
311
464
  telemetry: ${{ steps.gather.outputs.telemetry }}
312
465
 
@@ -688,6 +841,8 @@ jobs:
688
841
  echo "No changes to commit"
689
842
  exit 0
690
843
  fi
844
+ git config --local user.email 'action@github.com'
845
+ git config --local user.name 'GitHub Actions[bot]'
691
846
  git add -A
692
847
  git commit -m "agentic-step: fix broken main build (run ${{ env.FIX_RUN_ID }})"
693
848
  git push -u origin agentic-lib-fix-main-build
@@ -987,6 +1142,16 @@ jobs:
987
1142
  echo "- Dry-run: ${{ needs.params.outputs.dry-run }}" >> $GITHUB_STEP_SUMMARY
988
1143
  echo "- Website: [${SITE_URL}](${SITE_URL})" >> $GITHUB_STEP_SUMMARY
989
1144
 
1145
+ # ─── Post-commit validation: call test workflow to verify branch health ───
1146
+ post-commit-test:
1147
+ needs: [params, dev, fix-stuck, post-merge]
1148
+ if: >-
1149
+ !cancelled()
1150
+ && needs.params.outputs.dry-run != 'true'
1151
+ && needs.params.result == 'success'
1152
+ uses: ./.github/workflows/agentic-lib-test.yml
1153
+ secrets: inherit
1154
+
990
1155
  # ─── Schedule change (if requested) ────────────────────────────────
991
1156
  update-schedule:
992
1157
  needs: [params, dev]
package/agentic-lib.toml CHANGED
@@ -12,6 +12,7 @@ supervisor = "daily" # off | weekly | daily | hourly | continuous
12
12
  mission = "test/MISSION.md" #@dist "MISSION.md"
13
13
  source = "test/src/lib/" #@dist "src/lib/"
14
14
  tests = "test/tests/unit/" #@dist "tests/unit/"
15
+ behaviour = "test/tests/behaviour/" #@dist "tests/behaviour/"
15
16
  features = "test/features/" #@dist "features/"
16
17
  library = "test/library/" #@dist "library/"
17
18
  web = "test/src/web/" #@dist "src/web/"
@@ -905,6 +905,7 @@ function initReseed(initTimestamp) {
905
905
  function readTomlPaths() {
906
906
  let sourcePath = "src/lib/";
907
907
  let testsPath = "tests/unit/";
908
+ let behaviourPath = "tests/behaviour/";
908
909
  let examplesPath = "examples/";
909
910
  let webPath = "src/web/";
910
911
  const tomlTarget = resolve(target, "agentic-lib.toml");
@@ -913,17 +914,19 @@ function readTomlPaths() {
913
914
  const tomlContent = readFileSync(tomlTarget, "utf8");
914
915
  const sourceMatch = tomlContent.match(/^source\s*=\s*"([^"]+)"/m);
915
916
  const testsMatch = tomlContent.match(/^tests\s*=\s*"([^"]+)"/m);
917
+ const behaviourMatch = tomlContent.match(/^behaviour\s*=\s*"([^"]+)"/m);
916
918
  const examplesMatch = tomlContent.match(/^examples\s*=\s*"([^"]+)"/m);
917
919
  const webMatch = tomlContent.match(/^web\s*=\s*"([^"]+)"/m);
918
920
  if (sourceMatch) sourcePath = sourceMatch[1];
919
921
  if (testsMatch) testsPath = testsMatch[1];
922
+ if (behaviourMatch) behaviourPath = behaviourMatch[1];
920
923
  if (examplesMatch) examplesPath = examplesMatch[1];
921
924
  if (webMatch) webPath = webMatch[1];
922
925
  } catch (err) {
923
926
  console.log(` WARN: Could not read TOML for paths, using defaults: ${err.message}`);
924
927
  }
925
928
  }
926
- return { sourcePath, testsPath, examplesPath, webPath };
929
+ return { sourcePath, testsPath, behaviourPath, examplesPath, webPath };
927
930
  }
928
931
 
929
932
  function clearAndRecreateDir(dirPath, label) {
@@ -939,9 +942,10 @@ function clearAndRecreateDir(dirPath, label) {
939
942
  function initPurge(seedsDir, missionName, initTimestamp) {
940
943
  console.log("\n--- Purge: Reset Source Files to Seed State ---");
941
944
 
942
- const { sourcePath, testsPath, examplesPath, webPath } = readTomlPaths();
945
+ const { sourcePath, testsPath, behaviourPath, examplesPath, webPath } = readTomlPaths();
943
946
  clearAndRecreateDir(sourcePath, sourcePath);
944
947
  clearAndRecreateDir(testsPath, testsPath);
948
+ clearAndRecreateDir(behaviourPath, behaviourPath);
945
949
  clearAndRecreateDir(examplesPath, examplesPath);
946
950
  clearAndRecreateDir(webPath, webPath);
947
951
  clearAndRecreateDir("docs", "docs");
@@ -1097,25 +1101,53 @@ function initPurgeGitHub() {
1097
1101
  }
1098
1102
  if (openIssues.length === 0) console.log(" No open issues to close");
1099
1103
 
1100
- // Lock ALL issues (open and closed) to prevent bleed
1104
+ // Blank + lock ALL issues (open and closed) to prevent bleed from old missions
1101
1105
  const allIssuesJson = ghExec(`gh api repos/${repoSlug}/issues?state=all&per_page=100`);
1102
- const allIssues = JSON.parse(allIssuesJson || "[]").filter((i) => !i.pull_request && !i.locked);
1106
+ const allIssues = JSON.parse(allIssuesJson || "[]").filter((i) => !i.pull_request);
1107
+ let blanked = 0;
1103
1108
  for (const issue of allIssues) {
1104
- console.log(` LOCK: issue #${issue.number} ${issue.title}`);
1105
- if (!dryRun) {
1106
- try {
1107
- ghExec(
1108
- `gh api repos/${repoSlug}/issues/${issue.number}/lock -X PUT -f lock_reason=resolved`,
1109
- );
1109
+ const needsBlank = issue.title !== "unused github issue";
1110
+ const needsLock = !issue.locked;
1111
+ if (!needsBlank && !needsLock) continue;
1112
+ if (needsBlank) {
1113
+ console.log(` BLANK: issue #${issue.number} — "${issue.title}" "unused github issue"`);
1114
+ if (!dryRun) {
1115
+ try {
1116
+ ghExec(
1117
+ `gh api repos/${repoSlug}/issues/${issue.number} -X PATCH -f title="unused github issue" -f body="unused github issue"`,
1118
+ );
1119
+ // Remove all labels
1120
+ try {
1121
+ ghExec(`gh api repos/${repoSlug}/issues/${issue.number}/labels -X DELETE`);
1122
+ } catch { /* no labels to remove */ }
1123
+ blanked++;
1124
+ initChanges++;
1125
+ } catch (err) {
1126
+ console.log(` WARN: Failed to blank issue #${issue.number}: ${err.message}`);
1127
+ }
1128
+ } else {
1129
+ blanked++;
1130
+ initChanges++;
1131
+ }
1132
+ }
1133
+ if (needsLock) {
1134
+ console.log(` LOCK: issue #${issue.number}`);
1135
+ if (!dryRun) {
1136
+ try {
1137
+ ghExec(
1138
+ `gh api repos/${repoSlug}/issues/${issue.number}/lock -X PUT -f lock_reason=resolved`,
1139
+ );
1140
+ initChanges++;
1141
+ } catch (err) {
1142
+ console.log(` WARN: Failed to lock issue #${issue.number}: ${err.message}`);
1143
+ }
1144
+ } else {
1110
1145
  initChanges++;
1111
- } catch (err) {
1112
- console.log(` WARN: Failed to lock issue #${issue.number}: ${err.message}`);
1113
1146
  }
1114
- } else {
1115
- initChanges++;
1116
1147
  }
1117
1148
  }
1118
- if (allIssues.length === 0) console.log(" No unlocked issues to lock");
1149
+ if (blanked > 0) console.log(` Blanked ${blanked} issue(s)`);
1150
+ if (allIssues.length === 0) console.log(" No issues to process");
1119
1151
  } catch (err) {
1120
1152
  console.log(` WARN: Issue cleanup failed: ${err.message}`);
1121
1153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.1.94",
3
+ "version": "7.1.95",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -35,13 +35,14 @@ import { parse as parseToml } from "smol-toml";
35
35
  */
36
36
 
37
37
  // Keys whose paths are writable by agents
38
- const WRITABLE_KEYS = ["source", "tests", "features", "dependencies", "docs", "readme", "examples", "web"];
38
+ const WRITABLE_KEYS = ["source", "tests", "behaviour", "features", "dependencies", "docs", "readme", "examples", "web"];
39
39
 
40
40
  // Default paths — every key that task handlers might access
41
41
  const PATH_DEFAULTS = {
42
42
  mission: "MISSION.md",
43
43
  source: "src/lib/",
44
44
  tests: "tests/unit/",
45
+ behaviour: "tests/behaviour/",
45
46
  features: "features/",
46
47
  docs: "docs/",
47
48
  examples: "examples/",
@@ -34,6 +34,65 @@ const TASKS = {
34
34
  "supervise": supervise,
35
35
  };
36
36
 
37
+ /**
38
+ * Build mission-complete metrics array for the intentïon.md dashboard.
39
+ */
40
+ function buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount) {
41
+ const openIssues = featureIssueCount + maintenanceIssueCount;
42
+ const budgetCap = config.transformationBudget || 0;
43
+ const resolvedCount = result.resolvedCount || 0;
44
+ const missionComplete = existsSync("MISSION_COMPLETE.md");
45
+ const missionFailed = existsSync("MISSION_FAILED.md");
46
+
47
+ // Count open PRs from result if available
48
+ const openPrs = result.openPrCount || 0;
49
+
50
+ const metrics = [
51
+ { metric: "Open issues", value: String(openIssues), target: "0", status: openIssues === 0 ? "MET" : "NOT MET" },
52
+ { metric: "Open PRs", value: String(openPrs), target: "0", status: openPrs === 0 ? "MET" : "NOT MET" },
53
+ { metric: "Issues closed by review (RESOLVED)", value: String(resolvedCount), target: ">= 1", status: resolvedCount >= 1 ? "MET" : "NOT MET" },
54
+ { metric: "Transformation budget used", value: `${cumulativeCost}/${budgetCap}`, target: budgetCap > 0 ? `< ${budgetCap}` : "unlimited", status: budgetCap > 0 && cumulativeCost >= budgetCap ? "EXHAUSTED" : "OK" },
55
+ { metric: "Cumulative transforms", value: String(cumulativeCost), target: ">= 1", status: cumulativeCost >= 1 ? "MET" : "NOT MET" },
56
+ { metric: "Mission complete declared", value: missionComplete ? "YES" : "NO", target: "—", status: "—" },
57
+ { metric: "Mission failed declared", value: missionFailed ? "YES" : "NO", target: "—", status: "—" },
58
+ ];
59
+
60
+ return metrics;
61
+ }
62
+
63
+ /**
64
+ * Build mission-complete readiness narrative from metrics.
65
+ */
66
+ function buildMissionReadiness(metrics) {
67
+ const openIssues = parseInt(metrics.find((m) => m.metric === "Open issues")?.value || "0", 10);
68
+ const openPrs = parseInt(metrics.find((m) => m.metric === "Open PRs")?.value || "0", 10);
69
+ const resolved = parseInt(metrics.find((m) => m.metric === "Issues closed by review (RESOLVED)")?.value || "0", 10);
70
+ const missionComplete = metrics.find((m) => m.metric === "Mission complete declared")?.value === "YES";
71
+ const missionFailed = metrics.find((m) => m.metric === "Mission failed declared")?.value === "YES";
72
+
73
+ if (missionComplete) {
74
+ return "Mission has been declared complete.";
75
+ }
76
+ if (missionFailed) {
77
+ return "Mission has been declared failed.";
78
+ }
79
+
80
+ const conditionsMet = openIssues === 0 && openPrs === 0 && resolved >= 1;
81
+ const parts = [];
82
+
83
+ if (conditionsMet) {
84
+ parts.push("Mission complete conditions ARE met.");
85
+ parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) closed by review as RESOLVED.`);
86
+ } else {
87
+ parts.push("Mission complete conditions are NOT met.");
88
+ if (openIssues > 0) parts.push(`${openIssues} open issue(s) remain.`);
89
+ if (openPrs > 0) parts.push(`${openPrs} open PR(s) remain.`);
90
+ if (resolved < 1) parts.push("No issues have been closed by review as RESOLVED yet.");
91
+ }
92
+
93
+ return parts.join(" ");
94
+ }
95
+
37
96
  async function run() {
38
97
  try {
39
98
  // Parse inputs
@@ -173,6 +232,10 @@ async function run() {
173
232
 
174
233
  const closingNotes = result.closingNotes || generateClosingNotes(limitsStatus);
175
234
 
235
+ // Build mission-complete metrics and readiness narrative
236
+ const missionMetrics = buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount);
237
+ const missionReadiness = buildMissionReadiness(missionMetrics);
238
+
176
239
  // Log to intentïon.md (commit-if-changed excludes this on non-default branches)
177
240
  if (intentionFilepath) {
178
241
  logActivity({
@@ -195,6 +258,8 @@ async function run() {
195
258
  contextNotes: result.contextNotes,
196
259
  limitsStatus,
197
260
  promptBudget: result.promptBudget,
261
+ missionReadiness,
262
+ missionMetrics,
198
263
  closingNotes,
199
264
  transformationCost,
200
265
  narrative: result.narrative,
@@ -35,6 +35,8 @@ import * as core from "@actions/core";
35
35
  * @param {string} [options.closingNotes] - Auto-generated limit concern notes
36
36
  * @param {number} [options.transformationCost] - Transformation cost for this entry (0 or 1)
37
37
  * @param {string} [options.narrative] - LLM-generated narrative description of the change
38
+ * @param {string} [options.missionReadiness] - Mission-complete readiness narrative
39
+ * @param {Array} [options.missionMetrics] - Mission metrics entries { metric, value, target, status }
38
40
  */
39
41
  export function logActivity({
40
42
  filepath,
@@ -56,6 +58,8 @@ export function logActivity({
56
58
  contextNotes,
57
59
  limitsStatus,
58
60
  promptBudget,
61
+ missionReadiness,
62
+ missionMetrics,
59
63
  closingNotes,
60
64
  transformationCost,
61
65
  narrative,
@@ -108,6 +112,18 @@ export function logActivity({
108
112
  parts.push(`| ${pb.section} | ${pb.size} chars | ${pb.files || "—"} | ${pb.notes || ""} |`);
109
113
  }
110
114
  }
115
+ if (missionReadiness) {
116
+ parts.push("", "### Mission-Complete Readiness");
117
+ parts.push(missionReadiness);
118
+ }
119
+ if (missionMetrics && missionMetrics.length > 0) {
120
+ parts.push("", "### Mission Metrics");
121
+ parts.push("| Metric | Value | Target | Status |");
122
+ parts.push("|--------|-------|--------|--------|");
123
+ for (const m of missionMetrics) {
124
+ parts.push(`| ${m.metric} | ${m.value} | ${m.target} | ${m.status} |`);
125
+ }
126
+ }
111
127
  if (closingNotes) {
112
128
  parts.push("", "### Closing Notes");
113
129
  parts.push(closingNotes);
@@ -520,11 +520,12 @@ async function executeDispatch(octokit, repo, actionName, params, ctx) {
520
520
  return `dispatched:${workflowFile}`;
521
521
  }
522
522
 
523
- async function executeCreateIssue(octokit, repo, params) {
523
+ async function executeCreateIssue(octokit, repo, params, ctx) {
524
524
  const title = params.title || "Untitled issue";
525
525
  const labels = params.labels ? params.labels.split(",").map((l) => l.trim()) : ["automated"];
526
526
 
527
527
  // Dedup guard: skip if a similarly-titled issue was closed in the last hour
528
+ // Exclude issues closed before the init timestamp (cross-scenario protection)
528
529
  try {
529
530
  const { data: recent } = await octokit.rest.issues.listForRepo({
530
531
  ...repo,
@@ -533,12 +534,14 @@ async function executeCreateIssue(octokit, repo, params) {
533
534
  direction: "desc",
534
535
  per_page: 5,
535
536
  });
537
+ const initTimestamp = ctx?.initTimestamp;
536
538
  const titlePrefix = title.toLowerCase().substring(0, 30);
537
539
  const duplicate = recent.find(
538
540
  (i) =>
539
541
  !i.pull_request &&
540
542
  i.title.toLowerCase().includes(titlePrefix) &&
541
- Date.now() - new Date(i.closed_at).getTime() < 3600000,
543
+ Date.now() - new Date(i.closed_at).getTime() < 3600000 &&
544
+ (!initTimestamp || new Date(i.closed_at) > new Date(initTimestamp)),
542
545
  );
543
546
  if (duplicate) {
544
547
  core.info(`Skipping duplicate issue (similar to recently closed #${duplicate.number})`);
@@ -31,6 +31,10 @@ regardless — each failed attempt consumes transformation budget, so get it rig
31
31
  **Both unit tests AND behaviour tests must pass.** The project runs `npm test` (unit tests) and
32
32
  `npm run test:behaviour` (Playwright). Both are gated — your fix must pass both.
33
33
 
34
+ **IMPORTANT**: The project uses `"type": "module"` in package.json. All files must use ESM syntax:
35
+ - `import { test, expect } from "@playwright/test"` (NOT `const { test, expect } = require(...)`)
36
+ - `import { execSync } from "child_process"` (NOT `const { execSync } = require(...)`)
37
+
34
38
  ## Merge Conflict Resolution
35
39
 
36
40
  When resolving merge conflicts (files containing <<<<<<< / ======= / >>>>>>> markers):
@@ -30,6 +30,10 @@ the website code that uses it, the web tests that check for its output, and any
30
30
  that depend on it. A partial change that updates the library but not the tests will fail — and there will
31
31
  be a full test run after your changes regardless, consuming budget on each failure.
32
32
 
33
+ **IMPORTANT**: The project uses `"type": "module"` in package.json. All files must use ESM syntax:
34
+ - `import { test, expect } from "@playwright/test"` (NOT `const { test, expect } = require(...)`)
35
+ - `import { execSync } from "child_process"` (NOT `const { execSync } = require(...)`)
36
+
33
37
  ## Tests Must Pass
34
38
 
35
39
  Your changes MUST leave all existing tests passing. If you change function signatures, return values, or
@@ -17,7 +17,7 @@
17
17
  "author": "",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@xn-intenton-z2a/agentic-lib": "^7.1.94"
20
+ "@xn-intenton-z2a/agentic-lib": "^7.1.95"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@playwright/test": "^1.58.0",