@xn-intenton-z2a/agentic-lib 7.2.16 → 7.2.18

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.
@@ -122,29 +122,12 @@ jobs:
122
122
  path: SCREENSHOT_INDEX.png
123
123
  if-no-files-found: ignore
124
124
 
125
- - name: Push screenshot on main
125
+ - name: Push screenshot to .logs branch
126
126
  if: |
127
127
  github.ref == 'refs/heads/main' &&
128
+ github.repository != 'xn-intenton-z2a/agentic-lib' &&
128
129
  (github.event_name == 'schedule' || inputs.push-screenshot == 'true' || inputs.push-screenshot == true)
129
- run: |
130
- git config --global --add safe.directory "$GITHUB_WORKSPACE"
131
- git config user.name "github-actions[bot]"
132
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
133
- git add SCREENSHOT_INDEX.png
134
- git diff --cached --quiet && echo "No screenshot changes" && exit 0
135
- git commit -m "test: update SCREENSHOT_INDEX.png [skip ci]"
136
- for attempt in 1 2 3; do
137
- git push origin HEAD:main && break
138
- echo "Push failed (attempt $attempt) — pulling and retrying"
139
- git pull --rebase origin main || {
140
- echo "Rebase conflict — aborting rebase and retrying"
141
- git rebase --abort 2>/dev/null || true
142
- }
143
- sleep $((attempt * 2))
144
- if [ "$attempt" -eq 3 ]; then
145
- echo "::warning::Failed to push screenshot after 3 attempts"
146
- fi
147
- done
130
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh SCREENSHOT_INDEX.png
148
131
 
149
132
  # ─── Dispatch fix workflow when unit tests fail on main ─────────────
150
133
  dispatch-fix:
@@ -428,12 +428,26 @@ jobs:
428
428
  const missionComplete = fs.existsSync('MISSION_COMPLETE.md');
429
429
  const missionFailed = fs.existsSync('MISSION_FAILED.md');
430
430
 
431
- // Activity log stats
431
+ // Activity log stats (fetched from .logs branch)
432
432
  let activityStats = null;
433
433
  try {
434
- const logPath = fs.existsSync('intenti\u00F6n.md') ? 'intenti\u00F6n.md' : (fs.existsSync('intention.md') ? 'intention.md' : null);
435
- if (logPath) {
436
- const log = fs.readFileSync(logPath, 'utf8');
434
+ // Try .logs branch first (new location), fall back to local file (legacy)
435
+ let log = null;
436
+ for (const logFile of ['intenti\u00F6n.md', 'intention.md']) {
437
+ try {
438
+ const { data } = await github.rest.repos.getContent({
439
+ owner, repo, path: logFile, ref: '.logs',
440
+ });
441
+ log = Buffer.from(data.content, 'base64').toString('utf8');
442
+ break;
443
+ } catch { /* try next */ }
444
+ }
445
+ // Fall back to local file (for repos not yet using .logs branch)
446
+ if (!log) {
447
+ const logPath = fs.existsSync('intenti\u00F6n.md') ? 'intenti\u00F6n.md' : (fs.existsSync('intention.md') ? 'intention.md' : null);
448
+ if (logPath) log = fs.readFileSync(logPath, 'utf8');
449
+ }
450
+ if (log) {
437
451
  const entries = log.split('\n## ').length - 1;
438
452
  const costMatches = [...log.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g)];
439
453
  const totalCost = costMatches.reduce((sum, m) => sum + parseInt(m[1], 10), 0);
@@ -526,6 +540,12 @@ jobs:
526
540
  with:
527
541
  fetch-depth: 0
528
542
 
543
+ - name: Fetch log from .logs branch
544
+ run: |
545
+ for f in "intentïon.md" "intention.md"; do
546
+ git show "origin/.logs:${f}" > "$f" 2>/dev/null || true
547
+ done
548
+
529
549
  - name: Check mission-complete signal
530
550
  id: mission-check
531
551
  run: |
@@ -622,6 +642,10 @@ jobs:
622
642
  commit-message: "agentic-step: maintain features and library"
623
643
  push-ref: ${{ github.ref_name }}
624
644
 
645
+ - name: Push log to .logs branch
646
+ if: github.repository != 'xn-intenton-z2a/agentic-lib' && needs.params.outputs.dry-run != 'true'
647
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh "intentïon.md" "intention.md"
648
+
625
649
  # ─── Director: LLM evaluates mission status (complete/failed/in-progress) ──
626
650
  director:
627
651
  needs: [params, telemetry, maintain]
@@ -635,6 +659,14 @@ jobs:
635
659
  analysis: ${{ steps.director.outputs.director-analysis }}
636
660
  steps:
637
661
  - uses: actions/checkout@v6
662
+ with:
663
+ fetch-depth: 0
664
+
665
+ - name: Fetch log from .logs branch
666
+ run: |
667
+ for f in "intentïon.md" "intention.md"; do
668
+ git show "origin/.logs:${f}" > "$f" 2>/dev/null || true
669
+ done
638
670
 
639
671
  - uses: actions/setup-node@v6
640
672
  with:
@@ -661,6 +693,10 @@ jobs:
661
693
  instructions: ".github/agentic-lib/agents/agent-director.md"
662
694
  model: ${{ needs.params.outputs.model }}
663
695
 
696
+ - name: Push log to .logs branch
697
+ if: github.repository != 'xn-intenton-z2a/agentic-lib' && needs.params.outputs.dry-run != 'true'
698
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh "intentïon.md" "intention.md"
699
+
664
700
  # ─── Supervisor: LLM decides what to do (after director evaluates) ──
665
701
  supervisor:
666
702
  needs: [params, pr-cleanup, telemetry, maintain, director]
@@ -673,6 +709,14 @@ jobs:
673
709
  runs-on: ubuntu-latest
674
710
  steps:
675
711
  - uses: actions/checkout@v6
712
+ with:
713
+ fetch-depth: 0
714
+
715
+ - name: Fetch log from .logs branch
716
+ run: |
717
+ for f in "intentïon.md" "intention.md"; do
718
+ git show "origin/.logs:${f}" > "$f" 2>/dev/null || true
719
+ done
676
720
 
677
721
  - uses: actions/setup-node@v6
678
722
  with:
@@ -699,6 +743,10 @@ jobs:
699
743
  instructions: ".github/agentic-lib/agents/agent-supervisor.md"
700
744
  model: ${{ needs.params.outputs.model }}
701
745
 
746
+ - name: Push log to .logs branch
747
+ if: github.repository != 'xn-intenton-z2a/agentic-lib' && needs.params.outputs.dry-run != 'true'
748
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh "intentïon.md" "intention.md"
749
+
702
750
  # ─── Fix stuck PRs with failing checks ─────────────────────────────
703
751
  fix-stuck:
704
752
  needs: [params, supervisor]
@@ -1079,6 +1127,12 @@ jobs:
1079
1127
  fetch-depth: 0
1080
1128
  token: ${{ secrets.GITHUB_TOKEN }}
1081
1129
 
1130
+ - name: Fetch log from .logs branch
1131
+ run: |
1132
+ for f in "intentïon.md" "intention.md"; do
1133
+ git show "origin/.logs:${f}" > "$f" 2>/dev/null || true
1134
+ done
1135
+
1082
1136
  - uses: actions/setup-node@v6
1083
1137
  with:
1084
1138
  node-version: "24"
@@ -1239,6 +1293,10 @@ jobs:
1239
1293
  commit-message: "agentic-step: transform issue #${{ steps.issue.outputs.issue-number }}"
1240
1294
  push-ref: ${{ steps.branch.outputs.branchName }}
1241
1295
 
1296
+ - name: Push log to .logs branch
1297
+ if: github.repository != 'xn-intenton-z2a/agentic-lib' && needs.params.outputs.dry-run != 'true'
1298
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh "intentïon.md" "intention.md"
1299
+
1242
1300
  - name: Create PR and attempt merge
1243
1301
  if: github.repository != 'xn-intenton-z2a/agentic-lib' && steps.issue.outputs.issue-number != '' && needs.params.outputs.dry-run != 'true' && steps.pre-commit-test.outputs.tests-passed == 'true' && steps.pre-commit-behaviour-test.outputs.tests-passed != 'false'
1244
1302
  uses: actions/github-script@v8
package/agentic-lib.toml CHANGED
@@ -134,7 +134,7 @@ library-limit = 64 # max library entries in library/ directo
134
134
  # Thresholds for deterministic mission-complete declaration.
135
135
  # All conditions must be met simultaneously.
136
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/
137
+ min-dedicated-tests = 1 # minimum test files that import from src/lib/
138
138
  max-source-todos = 0 # max TODO comments allowed in ./src (0 = none)
139
139
 
140
140
  [bot]
@@ -559,6 +559,7 @@ function initScripts(agenticDir) {
559
559
  "clean.sh",
560
560
  "initialise.sh",
561
561
  "md-to-html.js",
562
+ "push-to-logs.sh",
562
563
  "update.sh",
563
564
  ];
564
565
  if (!existsSync(scriptsDir)) return;
@@ -1133,6 +1134,32 @@ function initPurgeGitHub() {
1133
1134
  console.log(` SKIP: Could not create discussion (${err.message})`);
1134
1135
  }
1135
1136
 
1137
+ // ── Create/reset .logs orphan branch ─────────────────────────────
1138
+ console.log("\n--- .logs branch (activity log + screenshot) ---");
1139
+ try {
1140
+ if (!dryRun) {
1141
+ // Delete existing .logs branch if present
1142
+ try {
1143
+ ghExec(`gh api repos/${repoSlug}/git/refs/heads/.logs -X DELETE`);
1144
+ console.log(" DELETE: existing .logs branch");
1145
+ } catch { /* branch may not exist */ }
1146
+ // Create orphan .logs branch with an empty commit via the API
1147
+ // First get the empty tree SHA
1148
+ const emptyTree = JSON.parse(ghExec(`gh api repos/${repoSlug}/git/trees -X POST -f "tree[0][path]=.gitkeep" -f "tree[0][mode]=100644" -f "tree[0][type]=blob" -f "tree[0][content]="`));
1149
+ const commitData = JSON.parse(ghExec(
1150
+ `gh api repos/${repoSlug}/git/commits -X POST -f "message=init .logs branch" -f "tree=${emptyTree.sha}"`,
1151
+ ));
1152
+ ghExec(`gh api repos/${repoSlug}/git/refs -X POST -f "ref=refs/heads/.logs" -f "sha=${commitData.sha}"`);
1153
+ console.log(" CREATE: .logs orphan branch");
1154
+ initChanges++;
1155
+ } else {
1156
+ console.log(" CREATE: .logs orphan branch (dry run)");
1157
+ initChanges++;
1158
+ }
1159
+ } catch (err) {
1160
+ console.log(` SKIP: Could not create .logs branch (${err.message})`);
1161
+ }
1162
+
1136
1163
  // ── Enable GitHub Pages ───────────────────────────────────────────
1137
1164
  console.log("\n--- Enable GitHub Pages ---");
1138
1165
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.2.16",
3
+ "version": "7.2.18",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -261,7 +261,7 @@ export function loadConfig(configPath) {
261
261
  const mc = toml["mission-complete"] || {};
262
262
  const missionCompleteThresholds = {
263
263
  minResolvedIssues: mc["min-resolved-issues"] ?? 3,
264
- requireDedicatedTests: mc["require-dedicated-tests"] ?? true,
264
+ minDedicatedTests: mc["min-dedicated-tests"] ?? (mc["require-dedicated-tests"] === false ? 0 : 1),
265
265
  maxSourceTodos: mc["max-source-todos"] ?? 0,
266
266
  };
267
267
 
@@ -86,20 +86,20 @@ function buildMissionMetrics(config, result, limitsStatus, cumulativeCost, featu
86
86
  const srcRoot = sourceDir.includes("/") ? sourceDir.split("/").slice(0, -1).join("/") || "src" : "src";
87
87
  const todoCount = countSourceTodos(srcRoot);
88
88
 
89
- // W3: Check for dedicated test files
90
- const hasDedicatedTests = result.hasDedicatedTests ?? false;
89
+ // W3: Count dedicated test files
90
+ const dedicatedTestCount = result.dedicatedTestCount ?? 0;
91
91
 
92
92
  // W11: Thresholds from config
93
93
  const thresholds = config.missionCompleteThresholds || {};
94
94
  const minResolved = thresholds.minResolvedIssues ?? 3;
95
- const requireTests = thresholds.requireDedicatedTests ?? true;
95
+ const minTests = thresholds.minDedicatedTests ?? 1;
96
96
  const maxTodos = thresholds.maxSourceTodos ?? 0;
97
97
 
98
98
  const metrics = [
99
99
  { metric: "Open issues", value: String(openIssues), target: "0", status: openIssues === 0 ? "MET" : "NOT MET" },
100
100
  { metric: "Open PRs", value: String(openPrs), target: "0", status: openPrs === 0 ? "MET" : "NOT MET" },
101
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" },
102
+ { metric: "Dedicated test files", value: String(dedicatedTestCount), target: `>= ${minTests}`, status: dedicatedTestCount >= minTests ? "MET" : "NOT MET" },
103
103
  { metric: "Source TODO count", value: String(todoCount), target: `<= ${maxTodos}`, status: todoCount <= maxTodos ? "MET" : "NOT MET" },
104
104
  { metric: "Transformation budget used", value: `${cumulativeCost}/${budgetCap}`, target: budgetCap > 0 ? `< ${budgetCap}` : "unlimited", status: budgetCap > 0 && cumulativeCost >= budgetCap ? "EXHAUSTED" : "OK" },
105
105
  { metric: "Cumulative transforms", value: String(cumulativeCost), target: ">= 1", status: cumulativeCost >= 1 ? "MET" : "NOT MET" },
@@ -117,7 +117,7 @@ function buildMissionReadiness(metrics) {
117
117
  const openIssues = parseInt(metrics.find((m) => m.metric === "Open issues")?.value || "0", 10);
118
118
  const openPrs = parseInt(metrics.find((m) => m.metric === "Open PRs")?.value || "0", 10);
119
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";
120
+ const dedicatedTests = parseInt(metrics.find((m) => m.metric === "Dedicated test files")?.value || "0", 10);
121
121
  const todoCount = parseInt(metrics.find((m) => m.metric === "Source TODO count")?.value || "0", 10);
122
122
  const missionComplete = metrics.find((m) => m.metric === "Mission complete declared")?.value === "YES";
123
123
  const missionFailed = metrics.find((m) => m.metric === "Mission failed declared")?.value === "YES";
@@ -136,7 +136,7 @@ function buildMissionReadiness(metrics) {
136
136
 
137
137
  if (allMet) {
138
138
  parts.push("Mission complete conditions ARE met.");
139
- parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) resolved, dedicated tests: ${hasDedicatedTests ? "yes" : "no"}, TODOs: ${todoCount}.`);
139
+ parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) resolved, ${dedicatedTests} dedicated test(s), TODOs: ${todoCount}.`);
140
140
  } else {
141
141
  parts.push("Mission complete conditions are NOT met.");
142
142
  if (openIssues > 0) parts.push(`${openIssues} open issue(s) remain.`);
@@ -262,9 +262,9 @@ async function run() {
262
262
  ? readdirSync(libraryPath).filter((f) => f.endsWith(".md")).length
263
263
  : 0;
264
264
 
265
- // W3/W10: Detect dedicated test files (centrally, for all tasks)
266
- let hasDedicatedTests = result.hasDedicatedTests ?? false;
267
- if (!hasDedicatedTests) {
265
+ // W3/W10: Count dedicated test files (centrally, for all tasks)
266
+ let dedicatedTestCount = result.dedicatedTestCount ?? 0;
267
+ if (dedicatedTestCount === 0) {
268
268
  try {
269
269
  const { scanDirectory: scanDir } = await import("./copilot.js");
270
270
  const testDirs = ["tests", "__tests__"];
@@ -275,17 +275,14 @@ async function run() {
275
275
  if (/^(main|web|behaviour)\.test\.[jt]s$/.test(tf.name)) continue;
276
276
  const content = readFileSync(tf.path, "utf8");
277
277
  if (/from\s+['"].*src\/lib\//.test(content) || /require\s*\(\s*['"].*src\/lib\//.test(content)) {
278
- hasDedicatedTests = true;
279
- break;
278
+ dedicatedTestCount++;
280
279
  }
281
280
  }
282
- if (hasDedicatedTests) break;
283
281
  }
284
282
  }
285
283
  } catch { /* ignore — scanDirectory not available in test environment */ }
286
284
  }
287
- // Inject hasDedicatedTests into result for buildMissionMetrics
288
- result.hasDedicatedTests = hasDedicatedTests;
285
+ result.dedicatedTestCount = dedicatedTestCount;
289
286
 
290
287
  // Count open automated issues (feature vs maintenance)
291
288
  let featureIssueCount = 0;
@@ -40,7 +40,6 @@ function countTodos(dir) {
40
40
  * Detect dedicated test files that import from src/lib/.
41
41
  */
42
42
  function detectDedicatedTests() {
43
- let hasDedicatedTests = false;
44
43
  const dedicatedTestFiles = [];
45
44
  const testDirs = ["tests", "__tests__"];
46
45
  for (const dir of testDirs) {
@@ -51,14 +50,13 @@ function detectDedicatedTests() {
51
50
  if (/^(main|web|behaviour)\.test\.[jt]s$/.test(tf.name)) continue;
52
51
  const content = readFileSync(tf.path, "utf8");
53
52
  if (/from\s+['"].*src\/lib\//.test(content) || /require\s*\(\s*['"].*src\/lib\//.test(content)) {
54
- hasDedicatedTests = true;
55
53
  dedicatedTestFiles.push(tf.name);
56
54
  }
57
55
  }
58
56
  } catch { /* ignore */ }
59
57
  }
60
58
  }
61
- return { hasDedicatedTests, dedicatedTestFiles };
59
+ return { dedicatedTestCount: dedicatedTestFiles.length, dedicatedTestFiles };
62
60
  }
63
61
 
64
62
  /**
@@ -68,14 +66,14 @@ function detectDedicatedTests() {
68
66
  function buildMetricAssessment(ctx, config) {
69
67
  const thresholds = config.missionCompleteThresholds || {};
70
68
  const minResolved = thresholds.minResolvedIssues ?? 3;
71
- const requireTests = thresholds.requireDedicatedTests ?? true;
69
+ const minTests = thresholds.minDedicatedTests ?? 1;
72
70
  const maxTodos = thresholds.maxSourceTodos ?? 0;
73
71
 
74
72
  const metrics = [
75
73
  { metric: "Open issues", value: ctx.issuesSummary.length, target: 0, met: ctx.issuesSummary.length === 0 },
76
74
  { metric: "Open PRs", value: ctx.prsSummary.length, target: 0, met: ctx.prsSummary.length === 0 },
77
75
  { 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 },
76
+ { metric: "Dedicated tests", value: ctx.dedicatedTestCount, target: minTests, met: ctx.dedicatedTestCount >= minTests },
79
77
  { metric: "Source TODOs", value: ctx.sourceTodoCount, target: maxTodos, met: ctx.sourceTodoCount <= maxTodos },
80
78
  { metric: "Budget", value: ctx.cumulativeTransformationCost, target: ctx.transformationBudget || "unlimited", met: !(ctx.transformationBudget > 0 && ctx.cumulativeTransformationCost >= ctx.transformationBudget) },
81
79
  ];
@@ -86,7 +84,7 @@ function buildMetricAssessment(ctx, config) {
86
84
  const table = [
87
85
  "| Metric | Value | Target | Status |",
88
86
  "|--------|-------|--------|--------|",
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"} |`),
87
+ ...metrics.map((m) => `| ${m.metric} | ${m.value} | ${typeof m.target === "number" ? (m.metric.includes("TODO") ? `<= ${m.target}` : `>= ${m.target}`) : m.target} | ${m.met ? "MET" : "NOT MET"} |`),
90
88
  ].join("\n");
91
89
 
92
90
  let assessment;
@@ -134,8 +132,8 @@ function buildPrompt(ctx, agentInstructions, metricAssessment) {
134
132
  ]
135
133
  : []),
136
134
  `### Test Coverage`,
137
- ctx.hasDedicatedTests
138
- ? `Dedicated test files: ${ctx.dedicatedTestFiles.join(", ")}`
135
+ ctx.dedicatedTestCount > 0
136
+ ? `Dedicated test files (${ctx.dedicatedTestCount}): ${ctx.dedicatedTestFiles.join(", ")}`
139
137
  : "**No dedicated test files found.**",
140
138
  "",
141
139
  `### Source TODO Count: ${ctx.sourceTodoCount}`,
@@ -348,7 +346,7 @@ export async function direct(context) {
348
346
  } catch { /* ignore */ }
349
347
 
350
348
  // Dedicated tests
351
- const { hasDedicatedTests, dedicatedTestFiles } = detectDedicatedTests();
349
+ const { dedicatedTestCount, dedicatedTestFiles } = detectDedicatedTests();
352
350
 
353
351
  // TODO count
354
352
  const sourcePath = config.paths.source?.path || "src/lib/";
@@ -365,7 +363,7 @@ export async function direct(context) {
365
363
  resolvedCount,
366
364
  prsSummary,
367
365
  sourceExports,
368
- hasDedicatedTests,
366
+ dedicatedTestCount,
369
367
  dedicatedTestFiles,
370
368
  sourceTodoCount,
371
369
  cumulativeTransformationCost,
@@ -424,7 +422,7 @@ export async function direct(context) {
424
422
  narrative: `Director: ${reason}`,
425
423
  metricAssessment: metricAssessment.assessment,
426
424
  directorAnalysis: analysis,
427
- hasDedicatedTests,
425
+ dedicatedTestCount,
428
426
  resolvedCount,
429
427
  changes: outcome === "mission-complete"
430
428
  ? [{ action: "mission-complete", file: "MISSION_COMPLETE.md", sizeInfo: reason.substring(0, 100) }]
@@ -312,9 +312,9 @@ async function gatherContext(octokit, repo, config, t) {
312
312
  }
313
313
  } catch { /* ignore */ }
314
314
 
315
- // Check for dedicated test files (not just seed tests)
315
+ // Count dedicated test files (not just seed tests)
316
316
  // A dedicated test imports from the source directory (src/lib/) rather than being a seed test
317
- let hasDedicatedTests = false;
317
+ let dedicatedTestCount = 0;
318
318
  let dedicatedTestFiles = [];
319
319
  try {
320
320
  const testDirs = ["tests", "__tests__"];
@@ -327,7 +327,7 @@ async function gatherContext(octokit, repo, config, t) {
327
327
  const content = readFileSync(tf.path, "utf8");
328
328
  // Check if it imports from src/lib/ (mission-specific code)
329
329
  if (/from\s+['"].*src\/lib\//.test(content) || /require\s*\(\s*['"].*src\/lib\//.test(content)) {
330
- hasDedicatedTests = true;
330
+ dedicatedTestCount++;
331
331
  dedicatedTestFiles.push(tf.name);
332
332
  }
333
333
  }
@@ -395,7 +395,7 @@ async function gatherContext(octokit, repo, config, t) {
395
395
  cumulativeTransformationCost,
396
396
  recentlyClosedSummary,
397
397
  sourceExports,
398
- hasDedicatedTests,
398
+ dedicatedTestCount,
399
399
  dedicatedTestFiles,
400
400
  sourceTodoCount,
401
401
  };
@@ -434,8 +434,8 @@ function buildPrompt(ctx, agentInstructions, config) {
434
434
  ]
435
435
  : []),
436
436
  `### Test Coverage`,
437
- ctx.hasDedicatedTests
438
- ? `Dedicated test files: ${ctx.dedicatedTestFiles.join(", ")}`
437
+ ctx.dedicatedTestCount > 0
438
+ ? `Dedicated test files (${ctx.dedicatedTestCount}): ${ctx.dedicatedTestFiles.join(", ")}`
439
439
  : "**No dedicated test files found.** Only seed tests (main.test.js, web.test.js) exist. Mission-complete requires dedicated tests that import from src/lib/.",
440
440
  "",
441
441
  `### Source TODO Count: ${ctx.sourceTodoCount}`,
@@ -447,7 +447,7 @@ function buildPrompt(ctx, agentInstructions, config) {
447
447
  // W10: Build mission-complete metrics inline for the LLM
448
448
  const thresholds = config?.missionCompleteThresholds || {};
449
449
  const minResolved = thresholds.minResolvedIssues ?? 3;
450
- const requireTests = thresholds.requireDedicatedTests ?? true;
450
+ const minTests = thresholds.minDedicatedTests ?? 1;
451
451
  const maxTodos = thresholds.maxSourceTodos ?? 0;
452
452
  const resolvedCount = ctx.recentlyClosedSummary.filter((s) => s.includes("RESOLVED")).length;
453
453
  const rows = [
@@ -457,7 +457,7 @@ function buildPrompt(ctx, agentInstructions, config) {
457
457
  `| Open issues | ${ctx.issuesSummary.length} | 0 | ${ctx.issuesSummary.length === 0 ? "MET" : "NOT MET"} |`,
458
458
  `| Open PRs | ${ctx.prsSummary.length} | 0 | ${ctx.prsSummary.length === 0 ? "MET" : "NOT MET"} |`,
459
459
  `| Issues resolved (RESOLVED) | ${resolvedCount} | >= ${minResolved} | ${resolvedCount >= minResolved ? "MET" : "NOT MET"} |`,
460
- `| Dedicated test files | ${ctx.hasDedicatedTests ? "YES" : "NO"} | ${requireTests ? "YES" : "—"} | ${!requireTests || ctx.hasDedicatedTests ? "MET" : "NOT MET"} |`,
460
+ `| Dedicated test files | ${ctx.dedicatedTestCount} | >= ${minTests} | ${ctx.dedicatedTestCount >= minTests ? "MET" : "NOT MET"} |`,
461
461
  `| Source TODO count | ${ctx.sourceTodoCount} | <= ${maxTodos} | ${ctx.sourceTodoCount <= maxTodos ? "MET" : "NOT MET"} |`,
462
462
  `| Budget used | ${ctx.cumulativeTransformationCost}/${ctx.transformationBudget} | < ${ctx.transformationBudget || "unlimited"} | ${ctx.transformationBudget > 0 && ctx.cumulativeTransformationCost >= ctx.transformationBudget ? "EXHAUSTED" : "OK"} |`,
463
463
  "",
@@ -28,20 +28,10 @@ runs:
28
28
  git add -A
29
29
  # Unstage workflow files — GITHUB_TOKEN cannot push workflow changes
30
30
  git reset HEAD -- '.github/workflows/' 2>/dev/null || true
31
- # Unstage log files on non-default branches to avoid merge conflicts
32
- REF="${{ inputs.push-ref }}"
33
- if [ -n "$REF" ] && [ "$REF" != "main" ] && [ "$REF" != "master" ]; then
34
- git reset HEAD -- 'intentïon.md' 'intention.md' 2>/dev/null || true
35
- fi
31
+ # Unstage log/screenshot files these live on the .logs branch
32
+ git reset HEAD -- 'intentïon.md' 'intention.md' 'SCREENSHOT_INDEX.png' 2>/dev/null || true
36
33
  if git diff --cached --quiet; then
37
34
  echo "No changes to commit"
38
- elif git diff --cached --name-only | grep -qvE '(intentïon\.md|intention\.md)$'; then
39
- # At least one non-log file changed — proceed with commit
40
- true
41
- else
42
- echo "Only log files changed — skipping commit"
43
- git reset HEAD -- . > /dev/null 2>&1
44
- exit 0
45
35
  fi
46
36
  if ! git diff --cached --quiet; then
47
37
  git commit -m "${{ inputs.commit-message }}"
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (C) 2025-2026 Polycode Limited
4
+ # push-to-logs.sh — Push log/screenshot files to the .logs orphan branch.
5
+ #
6
+ # Usage: bash .github/agentic-lib/scripts/push-to-logs.sh [file ...]
7
+ # e.g. bash .github/agentic-lib/scripts/push-to-logs.sh "intentïon.md" SCREENSHOT_INDEX.png
8
+ #
9
+ # Creates the .logs branch if it doesn't exist. Uses rebase/retry for
10
+ # concurrent pushes (same strategy as commit-if-changed).
11
+
12
+ set -euo pipefail
13
+
14
+ BRANCH=".logs"
15
+ MAX_RETRIES=3
16
+
17
+ # Collect files that actually exist in the workspace
18
+ FILES=()
19
+ for arg in "$@"; do
20
+ if [ -f "$arg" ]; then
21
+ FILES+=("$arg")
22
+ fi
23
+ done
24
+
25
+ if [ ${#FILES[@]} -eq 0 ]; then
26
+ echo "push-to-logs: no files to push"
27
+ exit 0
28
+ fi
29
+
30
+ echo "push-to-logs: pushing ${FILES[*]} to ${BRANCH}"
31
+
32
+ # Configure git
33
+ git config --local user.email 'action@github.com'
34
+ git config --local user.name 'GitHub Actions[bot]'
35
+
36
+ # Save file contents to temp
37
+ TMPDIR=$(mktemp -d)
38
+ for f in "${FILES[@]}"; do
39
+ cp "$f" "${TMPDIR}/$(basename "$f")"
40
+ done
41
+
42
+ # Fetch the .logs branch (may not exist yet)
43
+ REMOTE_EXISTS=""
44
+ git fetch origin "${BRANCH}" 2>/dev/null && REMOTE_EXISTS="true" || true
45
+
46
+ if [ "$REMOTE_EXISTS" = "true" ]; then
47
+ # Check out existing .logs branch into a detached worktree-like state
48
+ git checkout "origin/${BRANCH}" -- . 2>/dev/null || true
49
+ git checkout -B "${BRANCH}" "origin/${BRANCH}"
50
+ else
51
+ # Create orphan branch
52
+ git checkout --orphan "${BRANCH}"
53
+ git rm -rf . 2>/dev/null || true
54
+ fi
55
+
56
+ # Copy files in
57
+ for f in "${FILES[@]}"; do
58
+ cp "${TMPDIR}/$(basename "$f")" "$f"
59
+ git add "$f"
60
+ done
61
+
62
+ # Commit if changed
63
+ if git diff --cached --quiet 2>/dev/null; then
64
+ echo "push-to-logs: no changes to commit"
65
+ else
66
+ git commit -m "logs: update ${FILES[*]} [skip ci]"
67
+
68
+ for attempt in $(seq 1 $MAX_RETRIES); do
69
+ git push origin "${BRANCH}" && break
70
+ echo "push-to-logs: push failed (attempt $attempt) — pulling and retrying"
71
+ git pull --rebase origin "${BRANCH}" || {
72
+ echo "push-to-logs: rebase conflict — aborting and retrying"
73
+ git rebase --abort 2>/dev/null || true
74
+ }
75
+ sleep $((attempt * 2))
76
+ if [ "$attempt" -eq "$MAX_RETRIES" ]; then
77
+ echo "::warning::push-to-logs: failed to push after $MAX_RETRIES attempts"
78
+ fi
79
+ done
80
+ fi
81
+
82
+ # Return to previous branch
83
+ git checkout - 2>/dev/null || git checkout main 2>/dev/null || true
84
+
85
+ # Clean up
86
+ rm -rf "$TMPDIR"
@@ -20,8 +20,12 @@ Thumbs.db
20
20
  *.swp
21
21
  *.swo
22
22
 
23
+ # Activity log and screenshot (live on .logs branch, not main)
24
+ /intentïon.md
25
+ /intention.md
26
+ /SCREENSHOT_INDEX.png
27
+
23
28
  # Generated files at repo root (agents should use examples/ instead)
24
- /*.png
25
29
  /*.svg
26
30
  /*.pdf
27
31
  /*.csv
@@ -17,7 +17,7 @@
17
17
  "author": "",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@xn-intenton-z2a/agentic-lib": "^7.2.16"
20
+ "@xn-intenton-z2a/agentic-lib": "^7.2.18"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@playwright/test": "^1.58.0",