@xn-intenton-z2a/agentic-lib 7.4.23 → 7.4.25

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.
@@ -85,6 +85,8 @@ When writing both tests and implementation:
85
85
  **Both unit tests AND behaviour tests must pass.** The project runs `npm test` (unit tests) and
86
86
  `npm run test:behaviour` (Playwright). Both are gated — your changes must pass both.
87
87
 
88
+ **Code coverage:** Aim for the coverage thresholds stated in the Constraints section of the prompt. Write tests that exercise the code paths you're adding or modifying.
89
+
88
90
  ### Test philosophy
89
91
 
90
92
  - **Unit tests** bind to the detail: exact return values, error types, edge cases, parameter validation.
@@ -68,7 +68,7 @@ If an **Implementation Review** section is present in the prompt, examine it car
68
68
 
69
69
  ## Priority Order
70
70
 
71
- 1. **Always strive to close gaps** — every action you take should aim to satisfy the remaining NOT MET metrics. If the code is already complete (see Source Exports and Recently Closed Issues), use `nop` and let the director evaluate. Otherwise, create one comprehensive issue that targets the entire mission (all acceptance criteria, tests, website, docs, README). Only create a second issue if the first transform couldn't complete everything, and scope it to the remaining work. Do not create issues just to fill a quota.
71
+ 1. **Always strive to close gaps** — every action you take should aim to satisfy the remaining NOT MET metrics. If the code is already complete (see Source Exports and Recently Closed Issues), use `nop` and let the director evaluate. Otherwise, assess the full gap between current state and mission, then create as many distinct issues as needed to cover the entire gap. Ideally one comprehensive issue covering the whole gap, but if the work is naturally separable (e.g. different features, different layers), create multiple focused issues. Create up to the WIP limit. Each issue should be self-contained and independently deliverable.
72
72
  2. **Dispatch transform when ready issues exist** — transform is where code gets written. Always prefer it over maintain when there are open issues with the `ready` label.
73
73
  3. **Dispatch review after transform** — when recent workflow runs show a transform completion, dispatch review to close resolved issues and add `ready` labels to new issues. This keeps the pipeline flowing.
74
74
  4. **Fix failing PRs** — dispatch fix-code for any PR with failing checks (include pr-number).
@@ -78,7 +78,7 @@ If an **Implementation Review** section is present in the prompt, examine it car
78
78
 
79
79
  1. **Check what's already in progress** — don't duplicate work. If the workflow is already running, don't dispatch another.
80
80
  2. **Prioritise code generation** — the goal is working code. Prefer actions that produce code (dev-only, fix) over metadata (maintain, label).
81
- 3. **Right-size the work** — break the mission into chunks just big enough to reliably deliver. One comprehensive issue is better than many small ones. Only create a follow-up issue when the previous transform has landed and gaps remain.
81
+ 3. **Right-size the work** — break the mission into the fewest chunks that can each be reliably delivered in a single transform. Create all the issues needed upfront rather than waiting for each to land before creating the next. Each issue should request maximum implementation in its scope.
82
82
  4. **Respect limits** — don't create issues beyond the WIP limit shown in the context. Don't dispatch workflows that will fail due to missing prerequisites.
83
83
 
84
84
  ## When to use each action
@@ -64,7 +64,7 @@ on:
64
64
  default: "max"
65
65
  options:
66
66
  - min
67
- - recommended
67
+ - med
68
68
  - max
69
69
  workflow-runs:
70
70
  description: "Number of workflow iterations (1-16)"
@@ -124,7 +124,7 @@ on:
124
124
  options:
125
125
  - ""
126
126
  - min
127
- - recommended
127
+ - med
128
128
  - max
129
129
  create-seed-issues:
130
130
  description: "Create initial seed issues after purge"
@@ -67,7 +67,7 @@ on:
67
67
  options:
68
68
  - ""
69
69
  - min
70
- - recommended
70
+ - med
71
71
  - max
72
72
  dry-run:
73
73
  description: "Skip commit and push"
@@ -68,7 +68,7 @@ on:
68
68
  options:
69
69
  - ""
70
70
  - min
71
- - recommended
71
+ - med
72
72
  - max
73
73
  mode:
74
74
  description: "Run mode"
@@ -140,7 +140,10 @@ jobs:
140
140
  - uses: actions/checkout@v6
141
141
  with:
142
142
  ref: ${{ inputs.ref || github.sha }}
143
- sparse-checkout: ${{ env.configPath }}
143
+ sparse-checkout: |
144
+ ${{ env.configPath }}
145
+ MISSION_COMPLETE.md
146
+ MISSION_FAILED.md
144
147
  sparse-checkout-cone-mode: false
145
148
  - name: Normalise params
146
149
  id: normalise
@@ -190,6 +193,23 @@ jobs:
190
193
  echo "log-prefix=${LOG_PREFIX:-agent-log-}" >> $GITHUB_OUTPUT
191
194
  echo "log-branch=${LOG_BRANCH:-agentic-lib-logs}" >> $GITHUB_OUTPUT
192
195
  echo "screenshot-file=${SCREENSHOT:-SCREENSHOT_INDEX.png}" >> $GITHUB_OUTPUT
196
+ - name: Check mission-complete signal (W4)
197
+ id: mission-check
198
+ shell: bash
199
+ run: |
200
+ SUPERVISOR=""
201
+ if [ -f "${{ env.configPath }}" ]; then
202
+ SUPERVISOR=$(grep '^\s*supervisor\s*=' "${{ env.configPath }}" 2>/dev/null | head -1 | sed 's/.*=\s*"\([^"]*\)".*/\1/' || true)
203
+ fi
204
+ if [ -f MISSION_COMPLETE.md ] && [ "$SUPERVISOR" != "maintenance" ]; then
205
+ echo "mission-complete=true" >> $GITHUB_OUTPUT
206
+ echo "::notice::Mission is complete — most jobs will be skipped"
207
+ elif [ -f MISSION_FAILED.md ]; then
208
+ echo "mission-complete=true" >> $GITHUB_OUTPUT
209
+ echo "::notice::Mission has failed — most jobs will be skipped"
210
+ else
211
+ echo "mission-complete=false" >> $GITHUB_OUTPUT
212
+ fi
193
213
  outputs:
194
214
  model: ${{ steps.normalise.outputs.model }}
195
215
  profile: ${{ steps.normalise.outputs.profile }}
@@ -203,6 +223,7 @@ jobs:
203
223
  log-prefix: ${{ steps.normalise.outputs.log-prefix }}
204
224
  log-branch: ${{ steps.normalise.outputs.log-branch }}
205
225
  screenshot-file: ${{ steps.normalise.outputs.screenshot-file }}
226
+ mission-complete: ${{ steps.mission-check.outputs.mission-complete }}
206
227
 
207
228
  # ─── PR Cleanup: merge/close/delete stale PRs and branches ─────────
208
229
  pr-cleanup:
@@ -280,7 +301,8 @@ jobs:
280
301
  behaviour-telemetry:
281
302
  needs: params
282
303
  if: |
283
- needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only' || needs.params.outputs.mode == 'review-only'
304
+ needs.params.outputs.mission-complete != 'true' &&
305
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only' || needs.params.outputs.mode == 'review-only')
284
306
  runs-on: ubuntu-latest
285
307
  container: mcr.microsoft.com/playwright:v1.58.2-noble
286
308
  steps:
@@ -323,6 +345,7 @@ jobs:
323
345
  needs: [params, behaviour-telemetry]
324
346
  if: |
325
347
  !cancelled() &&
348
+ needs.params.outputs.mission-complete != 'true' &&
326
349
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only' || needs.params.outputs.mode == 'review-only')
327
350
  runs-on: ubuntu-latest
328
351
  steps:
@@ -550,14 +573,25 @@ jobs:
550
573
  if (fs.existsSync('agentic-lib.toml')) {
551
574
  const toml = fs.readFileSync('agentic-lib.toml', 'utf8');
552
575
  const profileMatch = toml.match(/^\s*profile\s*=\s*"(\w+)"/m);
553
- const profile = profileMatch ? profileMatch[1] : 'recommended';
554
- const PROFILE_LIMITS = { min: 10000, recommended: 30000, max: 60000 };
576
+ const profile = profileMatch ? profileMatch[1] : 'med';
577
+ const PROFILE_LIMITS = { min: 10000, med: 30000, max: 60000 };
555
578
  maxTelemetryChars = PROFILE_LIMITS[profile] || 30000;
556
579
  }
557
580
  } catch (e) {}
558
581
  const summary = JSON.stringify(telemetry);
559
582
  core.setOutput('telemetry', summary.slice(0, maxTelemetryChars));
560
583
 
584
+ // W15: Output counts for downstream job gating
585
+ core.setOutput('open-issue-count', String(issuesSummary.length));
586
+ core.setOutput('open-pr-count', String(prsSummary.length));
587
+
588
+ // W19: Output unit test summary for transform prompt
589
+ const unitSummary = `exit=${telemetry.liveTests.unit.exitCode} pass=${telemetry.liveTests.unit.passCount} fail=${telemetry.liveTests.unit.failCount}`;
590
+ core.setOutput('unit-test-summary', unitSummary);
591
+ // Truncated unit test output for transform (first 4000 chars)
592
+ const unitOutputForTransform = (telemetry.liveTests?.unit?.output || '').substring(0, 4000);
593
+ core.setOutput('unit-test-output', unitOutputForTransform);
594
+
561
595
  - name: Output telemetry summary
562
596
  shell: bash
563
597
  run: |
@@ -567,6 +601,10 @@ jobs:
567
601
 
568
602
  outputs:
569
603
  telemetry: ${{ steps.gather.outputs.telemetry }}
604
+ open-issue-count: ${{ steps.gather.outputs.open-issue-count }}
605
+ open-pr-count: ${{ steps.gather.outputs.open-pr-count }}
606
+ unit-test-summary: ${{ steps.gather.outputs.unit-test-summary }}
607
+ unit-test-output: ${{ steps.gather.outputs.unit-test-output }}
570
608
 
571
609
  # ─── Maintain: features + library (push to main) ───────────────────
572
610
  # Runs early (parallel with pr-cleanup/telemetry) so supervisor sees features.
@@ -574,6 +612,7 @@ jobs:
574
612
  needs: [params]
575
613
  if: |
576
614
  !cancelled() &&
615
+ needs.params.outputs.mission-complete != 'true' &&
577
616
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'maintain-only') &&
578
617
  needs.params.result == 'success' &&
579
618
  inputs.skipMaintain != 'true' && inputs.skipMaintain != true
@@ -656,6 +695,7 @@ jobs:
656
695
  - name: Maintain library
657
696
  id: maintain-library
658
697
  if: steps.mission-check.outputs.mission-complete != 'true'
698
+ timeout-minutes: 10
659
699
  uses: ./.github/agentic-lib/actions/agentic-step
660
700
  env:
661
701
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -670,6 +710,7 @@ jobs:
670
710
  - name: Maintain features
671
711
  id: maintain-features
672
712
  if: steps.mission-check.outputs.mission-complete != 'true'
713
+ timeout-minutes: 10
673
714
  uses: ./.github/agentic-lib/actions/agentic-step
674
715
  env:
675
716
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -710,6 +751,7 @@ jobs:
710
751
  needs: [params]
711
752
  if: |
712
753
  !cancelled() &&
754
+ needs.params.outputs.mission-complete != 'true' &&
713
755
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'maintain-only') &&
714
756
  needs.params.result == 'success'
715
757
  runs-on: ubuntu-latest
@@ -755,6 +797,7 @@ jobs:
755
797
  - name: Run implementation review
756
798
  id: review
757
799
  if: github.repository != 'xn-intenton-z2a/agentic-lib'
800
+ timeout-minutes: 10
758
801
  uses: ./.github/agentic-lib/actions/agentic-step
759
802
  env:
760
803
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -776,6 +819,7 @@ jobs:
776
819
  needs: [params, telemetry, maintain, implementation-review]
777
820
  if: |
778
821
  !cancelled() &&
822
+ needs.params.outputs.mission-complete != 'true' &&
779
823
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
780
824
  needs.params.result == 'success'
781
825
  runs-on: ubuntu-latest
@@ -816,6 +860,7 @@ jobs:
816
860
  - name: Run director
817
861
  id: director
818
862
  if: github.repository != 'xn-intenton-z2a/agentic-lib'
863
+ timeout-minutes: 10
819
864
  uses: ./.github/agentic-lib/actions/agentic-step
820
865
  env:
821
866
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -903,6 +948,7 @@ jobs:
903
948
 
904
949
  - name: Run supervisor
905
950
  if: github.repository != 'xn-intenton-z2a/agentic-lib'
951
+ timeout-minutes: 10
906
952
  uses: ./.github/agentic-lib/actions/agentic-step
907
953
  env:
908
954
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1179,6 +1225,7 @@ jobs:
1179
1225
  env.FIX_PR_NUMBER != '' &&
1180
1226
  steps.fix-mission-check.outputs.mission-complete != 'true' &&
1181
1227
  env.FIX_REASON == 'requested'
1228
+ timeout-minutes: 10
1182
1229
  uses: ./.github/agentic-lib/actions/agentic-step
1183
1230
  env:
1184
1231
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1196,6 +1243,7 @@ jobs:
1196
1243
  if: |
1197
1244
  env.FIX_MAIN_BUILD == 'true' &&
1198
1245
  steps.fix-mission-check.outputs.mission-complete != 'true'
1246
+ timeout-minutes: 10
1199
1247
  uses: ./.github/agentic-lib/actions/agentic-step
1200
1248
  env:
1201
1249
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1229,6 +1277,63 @@ jobs:
1229
1277
  echo "No additional changes to push"
1230
1278
  fi
1231
1279
 
1280
+ # W20: Immediately attempt merge after fix-stuck resolves conflicts
1281
+ - name: "W20: Immediate merge attempt after fix"
1282
+ if: |
1283
+ github.repository != 'xn-intenton-z2a/agentic-lib' &&
1284
+ env.FIX_PR_NUMBER != '' &&
1285
+ steps.fix-mission-check.outputs.mission-complete != 'true' &&
1286
+ needs.params.outputs.dry-run != 'true'
1287
+ uses: actions/github-script@v8
1288
+ with:
1289
+ script: |
1290
+ const owner = context.repo.owner;
1291
+ const repo = context.repo.repo;
1292
+ const prNumber = parseInt('${{ env.FIX_PR_NUMBER }}');
1293
+ if (!prNumber) return;
1294
+
1295
+ // Wait for checks to register
1296
+ await new Promise(r => setTimeout(r, 15000));
1297
+
1298
+ for (let attempt = 0; attempt < 3; attempt++) {
1299
+ const { data: pr } = await github.rest.pulls.get({
1300
+ owner, repo, pull_number: prNumber,
1301
+ });
1302
+
1303
+ if (pr.mergeable && pr.mergeable_state === 'clean') {
1304
+ try {
1305
+ await github.rest.pulls.merge({
1306
+ owner, repo, pull_number: prNumber, merge_method: 'squash',
1307
+ });
1308
+ core.info(`W20: Merged PR #${prNumber} immediately after fix`);
1309
+ try {
1310
+ await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}` });
1311
+ } catch (e) { /* branch may already be deleted */ }
1312
+ // Label associated issue
1313
+ const branchPrefix = 'agentic-lib-issue-';
1314
+ if (pr.head.ref.startsWith(branchPrefix)) {
1315
+ const issueNum = parseInt(pr.head.ref.replace(branchPrefix, ''));
1316
+ if (issueNum) {
1317
+ try {
1318
+ await github.rest.issues.addLabels({ owner, repo, issue_number: issueNum, labels: ['merged'] });
1319
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNum, name: 'in-progress' });
1320
+ } catch (e) { /* label may not exist */ }
1321
+ }
1322
+ }
1323
+ return;
1324
+ } catch (e) {
1325
+ core.info(`W20: Merge attempt ${attempt + 1} failed: ${e.message}`);
1326
+ }
1327
+ } else if (pr.mergeable_state === 'unstable' || pr.mergeable === null) {
1328
+ core.info(`W20: PR not ready yet (${pr.mergeable_state}), waiting...`);
1329
+ await new Promise(r => setTimeout(r, 15000));
1330
+ } else {
1331
+ core.info(`W20: PR not mergeable (${pr.mergeable_state}), leaving for pr-cleanup`);
1332
+ break;
1333
+ }
1334
+ }
1335
+ core.info(`W20: PR #${prNumber} left open for next cycle`);
1336
+
1232
1337
  - name: Commit, push, and open PR for main build fix
1233
1338
  if: github.repository != 'xn-intenton-z2a/agentic-lib' && env.FIX_MAIN_BUILD == 'true' && steps.fix-mission-check.outputs.mission-complete != 'true'
1234
1339
  env:
@@ -1249,12 +1354,14 @@ jobs:
1249
1354
  --label automerge
1250
1355
 
1251
1356
  # ─── Review: close resolved issues, enhance with criteria ──────────
1357
+ # W15: Skip review when there are no open issues to review
1252
1358
  review-features:
1253
- needs: [params, maintain, supervisor]
1359
+ needs: [params, maintain, supervisor, telemetry]
1254
1360
  if: |
1255
1361
  !cancelled() &&
1256
1362
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'review-only') &&
1257
- needs.params.result == 'success'
1363
+ needs.params.result == 'success' &&
1364
+ needs.telemetry.outputs.open-issue-count != '0'
1258
1365
  runs-on: ubuntu-latest
1259
1366
  steps:
1260
1367
  - uses: actions/checkout@v6
@@ -1278,6 +1385,7 @@ jobs:
1278
1385
  fi
1279
1386
 
1280
1387
  - name: Review issues
1388
+ timeout-minutes: 10
1281
1389
  uses: ./.github/agentic-lib/actions/agentic-step
1282
1390
  env:
1283
1391
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1289,6 +1397,7 @@ jobs:
1289
1397
  model: ${{ needs.params.outputs.model }}
1290
1398
 
1291
1399
  - name: Enhance issues
1400
+ timeout-minutes: 10
1292
1401
  uses: ./.github/agentic-lib/actions/agentic-step
1293
1402
  env:
1294
1403
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1299,9 +1408,9 @@ jobs:
1299
1408
  instructions: ".github/agents/agent-ready-issue.md"
1300
1409
  model: ${{ needs.params.outputs.model }}
1301
1410
 
1302
- # ─── Dev: sequential issue resolution ──────────────────────────────
1411
+ # ─── Dev: issue resolution (W7: multiple issues per session) ──────
1303
1412
  dev:
1304
- needs: [params, maintain, review-features]
1413
+ needs: [params, maintain, review-features, telemetry, implementation-review]
1305
1414
  if: |
1306
1415
  !cancelled() &&
1307
1416
  (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
@@ -1383,25 +1492,32 @@ jobs:
1383
1492
  core.setOutput('issue-number', specificIssue);
1384
1493
  return;
1385
1494
  }
1386
- // W7: Mechanical instability override prioritise instability issues
1387
- // before any other ready issues, regardless of supervisor decisions
1495
+ // W7: Collect ALL ready issues for concurrent resolution in one session
1496
+ const collected = [];
1497
+ // Instability issues first (mechanical priority override)
1388
1498
  const { data: instabilityIssues } = await github.rest.issues.listForRepo({
1389
1499
  ...context.repo, state: 'open', labels: 'instability',
1390
- sort: 'created', direction: 'asc', per_page: 1,
1500
+ sort: 'created', direction: 'asc', per_page: 10,
1391
1501
  });
1392
- if (instabilityIssues.length > 0) {
1393
- core.setOutput('issue-number', String(instabilityIssues[0].number));
1394
- core.info(`Instability override: targeting issue #${instabilityIssues[0].number}: ${instabilityIssues[0].title}`);
1395
- return;
1502
+ for (const i of instabilityIssues) {
1503
+ collected.push(i.number);
1504
+ core.info(`Instability issue: #${i.number}: ${i.title}`);
1396
1505
  }
1397
- // Find oldest open issue with 'ready' label
1398
- const { data: issues } = await github.rest.issues.listForRepo({
1506
+ // Then ready issues
1507
+ const { data: readyIssues } = await github.rest.issues.listForRepo({
1399
1508
  ...context.repo, state: 'open', labels: 'ready',
1400
- sort: 'created', direction: 'asc', per_page: 1,
1509
+ sort: 'created', direction: 'asc', per_page: 10,
1401
1510
  });
1402
- if (issues.length > 0) {
1403
- core.setOutput('issue-number', String(issues[0].number));
1404
- core.info(`Targeting issue #${issues[0].number}: ${issues[0].title}`);
1511
+ for (const i of readyIssues) {
1512
+ if (!collected.includes(i.number)) {
1513
+ collected.push(i.number);
1514
+ core.info(`Ready issue: #${i.number}: ${i.title}`);
1515
+ }
1516
+ }
1517
+ if (collected.length > 0) {
1518
+ // W7: Pass all issues as comma-separated list
1519
+ core.setOutput('issue-number', collected.join(','));
1520
+ core.info(`Targeting ${collected.length} issue(s): ${collected.join(', ')}`);
1405
1521
  } else {
1406
1522
  core.setOutput('issue-number', '');
1407
1523
  core.info('No ready issues found');
@@ -1411,17 +1527,24 @@ jobs:
1411
1527
  if: steps.issue.outputs.issue-number != ''
1412
1528
  id: branch
1413
1529
  run: |
1414
- ISSUE_NUMBER="${{ steps.issue.outputs.issue-number }}"
1530
+ # W7: Use first issue number for branch name (may be comma-separated list)
1531
+ ISSUE_LIST="${{ steps.issue.outputs.issue-number }}"
1532
+ ISSUE_NUMBER="${ISSUE_LIST%%,*}"
1415
1533
  BRANCH="agentic-lib-issue-${ISSUE_NUMBER}"
1416
1534
  git checkout -b "${BRANCH}" 2>/dev/null || git checkout "${BRANCH}"
1417
1535
  echo "branchName=${BRANCH}" >> $GITHUB_OUTPUT
1418
1536
 
1419
1537
  - name: Run transformation
1420
1538
  if: steps.issue.outputs.issue-number != ''
1539
+ timeout-minutes: 10
1421
1540
  uses: ./.github/agentic-lib/actions/agentic-step
1422
1541
  env:
1423
1542
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1424
1543
  COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
1544
+ REVIEW_ADVICE: ${{ needs.implementation-review.outputs.review-advice }}
1545
+ REVIEW_GAPS: ${{ needs.implementation-review.outputs.review-gaps }}
1546
+ TELEMETRY_UNIT_TEST_SUMMARY: ${{ needs.telemetry.outputs.unit-test-summary }}
1547
+ TELEMETRY_UNIT_TEST_OUTPUT: ${{ needs.telemetry.outputs.unit-test-output }}
1425
1548
  with:
1426
1549
  task: "transform"
1427
1550
  config: ${{ needs.params.outputs.config-path }}
@@ -1501,7 +1624,10 @@ jobs:
1501
1624
  const owner = context.repo.owner;
1502
1625
  const repo = context.repo.repo;
1503
1626
  const branchName = '${{ steps.branch.outputs.branchName }}';
1504
- const issueNumber = '${{ steps.issue.outputs.issue-number }}';
1627
+ const issueList = '${{ steps.issue.outputs.issue-number }}';
1628
+ // W7: issueNumber may be comma-separated list
1629
+ const issueNumbers = issueList.split(',').map(n => n.trim()).filter(Boolean);
1630
+ const issueNumber = issueNumbers[0] || '';
1505
1631
 
1506
1632
  if (!branchName) return;
1507
1633
 
@@ -1525,6 +1651,12 @@ jobs:
1525
1651
  head: `${owner}:${branchName}`, per_page: 1,
1526
1652
  });
1527
1653
 
1654
+ // W7: Build PR body with Closes for all issues
1655
+ const closesLines = issueNumbers.map(n => `Closes #${n}`).join('\n');
1656
+ const prTitle = issueNumbers.length > 1
1657
+ ? `fix: resolve issues ${issueNumbers.map(n => '#' + n).join(', ')}`
1658
+ : `fix: resolve issue #${issueNumber}`;
1659
+
1528
1660
  let prNumber;
1529
1661
  if (existingPRs.length > 0) {
1530
1662
  prNumber = existingPRs[0].number;
@@ -1532,8 +1664,8 @@ jobs:
1532
1664
  } else {
1533
1665
  const { data: pr } = await github.rest.pulls.create({
1534
1666
  owner, repo,
1535
- title: `fix: resolve issue #${issueNumber}`,
1536
- body: `Closes #${issueNumber}\n\nAutomated transformation.`,
1667
+ title: prTitle,
1668
+ body: `${closesLines}\n\nAutomated transformation.`,
1537
1669
  head: branchName, base: 'main',
1538
1670
  });
1539
1671
  prNumber = pr.number;
@@ -1583,7 +1715,7 @@ jobs:
1583
1715
 
1584
1716
  # ─── Post-merge: stats, schedule, mission check ────────────────────
1585
1717
  post-merge:
1586
- needs: [params, maintain, dev, pr-cleanup]
1718
+ needs: [params, maintain, dev, pr-cleanup, implementation-review]
1587
1719
  if: ${{ !cancelled() && needs.params.result == 'success' }}
1588
1720
  runs-on: ubuntu-latest
1589
1721
  steps:
@@ -1605,6 +1737,75 @@ jobs:
1605
1737
  echo "- Dry-run: ${{ needs.params.outputs.dry-run }}" >> $GITHUB_STEP_SUMMARY
1606
1738
  echo "- Website: [${SITE_URL}](${SITE_URL})" >> $GITHUB_STEP_SUMMARY
1607
1739
 
1740
+ # W14: Post-merge director check — re-evaluate mission status after dev/PR merges
1741
+ - name: Fetch log and screenshot from log branch (post-merge director)
1742
+ if: |
1743
+ needs.params.outputs.mission-complete != 'true' &&
1744
+ needs.params.outputs.dry-run != 'true' &&
1745
+ github.repository != 'xn-intenton-z2a/agentic-lib'
1746
+ env:
1747
+ LOG_BRANCH: ${{ needs.params.outputs.log-branch }}
1748
+ SCREENSHOT_FILE: ${{ needs.params.outputs.screenshot-file }}
1749
+ run: |
1750
+ git fetch origin "${LOG_BRANCH}" 2>/dev/null || true
1751
+ git show "origin/${LOG_BRANCH}:${SCREENSHOT_FILE}" > "${SCREENSHOT_FILE}" 2>/dev/null || true
1752
+ git show "origin/${LOG_BRANCH}:agentic-lib-state.toml" > "agentic-lib-state.toml" 2>/dev/null || true
1753
+
1754
+ - uses: actions/setup-node@v6
1755
+ if: |
1756
+ needs.params.outputs.mission-complete != 'true' &&
1757
+ needs.params.outputs.dry-run != 'true' &&
1758
+ github.repository != 'xn-intenton-z2a/agentic-lib'
1759
+ with:
1760
+ node-version: "24"
1761
+
1762
+ - name: Self-init (agentic-lib dev only)
1763
+ if: |
1764
+ needs.params.outputs.mission-complete != 'true' &&
1765
+ needs.params.outputs.dry-run != 'true' &&
1766
+ hashFiles('scripts/self-init.sh') != '' && hashFiles('.github/agentic-lib/actions/agentic-step/package.json') == ''
1767
+ run: bash scripts/self-init.sh
1768
+
1769
+ - name: Install agentic-step dependencies (post-merge director)
1770
+ if: |
1771
+ needs.params.outputs.mission-complete != 'true' &&
1772
+ needs.params.outputs.dry-run != 'true' &&
1773
+ github.repository != 'xn-intenton-z2a/agentic-lib'
1774
+ working-directory: .github/agentic-lib/actions/agentic-step
1775
+ run: |
1776
+ npm ci
1777
+ if [ -d "../../copilot" ]; then
1778
+ ln -sf "$(pwd)/node_modules" ../../copilot/node_modules
1779
+ fi
1780
+
1781
+ - name: "W14: Post-merge director evaluation"
1782
+ id: post-merge-director
1783
+ if: |
1784
+ needs.params.outputs.mission-complete != 'true' &&
1785
+ needs.params.outputs.dry-run != 'true' &&
1786
+ github.repository != 'xn-intenton-z2a/agentic-lib'
1787
+ timeout-minutes: 10
1788
+ uses: ./.github/agentic-lib/actions/agentic-step
1789
+ env:
1790
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1791
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
1792
+ REVIEW_ADVICE: ${{ needs.implementation-review.outputs.review-advice }}
1793
+ REVIEW_GAPS: ${{ needs.implementation-review.outputs.review-gaps }}
1794
+ with:
1795
+ task: "direct"
1796
+ config: ${{ needs.params.outputs.config-path }}
1797
+ instructions: ".github/agents/agent-director.md"
1798
+ model: ${{ needs.params.outputs.model }}
1799
+
1800
+ - name: Push log to log branch (post-merge director)
1801
+ if: |
1802
+ needs.params.outputs.mission-complete != 'true' &&
1803
+ needs.params.outputs.dry-run != 'true' &&
1804
+ github.repository != 'xn-intenton-z2a/agentic-lib'
1805
+ env:
1806
+ LOG_BRANCH: ${{ needs.params.outputs.log-branch }}
1807
+ run: bash .github/agentic-lib/scripts/push-to-logs.sh agent-log-*.md agentic-lib-state.toml
1808
+
1608
1809
  # ─── Post-commit validation: call test workflow to verify branch health ───
1609
1810
  post-commit-test:
1610
1811
  needs: [params, maintain, dev, fix-stuck, post-merge]
package/agentic-lib.toml CHANGED
@@ -36,9 +36,9 @@ test = "npm ci && npm test"
36
36
  # library-limit = 32
37
37
 
38
38
  [tuning]
39
- # Profile sets defaults for all tuning and limit knobs: min | recommended | max
39
+ # Profile sets defaults for all tuning and limit knobs: min | med | max
40
40
  # Profile definitions live in [profiles.*] sections below.
41
- profile = "min" #@dist "recommended"
41
+ profile = "min" #@dist "max"
42
42
  #
43
43
  # Model selection — each has different strengths:
44
44
  # gpt-5-mini — Fast, cheap, supports reasoning-effort. Best for CI and iteration.
@@ -55,6 +55,13 @@ infinite-sessions = false # set to true for long sessions with compaction
55
55
  # max-issues = 5
56
56
  # stale-days = 14
57
57
  # max-discussion-comments = 5
58
+ # session-timeout-ms = 480000 # LLM session timeout in ms (should be < workflow step timeout)
59
+ # max-tokens = 200000 # token budget — controls max tool calls (tokens / 5000)
60
+ # max-read-chars = 20000 # max chars per read_file result
61
+ # max-test-output = 4000 # max chars of test output in prompts
62
+ # max-file-listing = 30 # max files in directory listings (0 = unlimited)
63
+ # max-library-index = 2000 # max chars for library index summary
64
+ # max-fix-test-output = 8000 # max chars of failed run log in fix-code
58
65
 
59
66
  # ─── Profile Definitions ────────────────────────────────────────────
60
67
  # Each profile defines tuning and limits defaults. The active profile
@@ -75,9 +82,16 @@ max-attempts-per-branch = 2 # max transform attempts before abando
75
82
  max-attempts-per-issue = 1 # max transform attempts before abandoning an issue
76
83
  features-limit = 2 # max feature files in features/ directory
77
84
  library-limit = 8 # max library entries in library/ directory
85
+ session-timeout-ms = 480000 # LLM session timeout in ms (8 min, below 10-min workflow step)
86
+ max-tokens = 200000 # token budget for tool-call cap calculation
87
+ max-read-chars = 20000 # max chars returned from read_file tool
88
+ max-test-output = 4000 # max chars of test output in prompts
89
+ max-file-listing = 30 # max files in directory listings (0 = unlimited)
90
+ max-library-index = 2000 # max chars for library index in prompts
91
+ max-fix-test-output = 8000 # max chars of failed run log in fix-code
78
92
 
79
- [profiles.recommended]
80
- # Balanced — good results, default for consumer repos.
93
+ [profiles.med]
94
+ # Balanced — good results, middle ground.
81
95
  reasoning-effort = "medium" # low | medium | high | none
82
96
  infinite-sessions = true # enable session compaction for long runs
83
97
  transformation-budget = 32 # max code-changing cycles per run
@@ -90,6 +104,13 @@ max-attempts-per-branch = 3 # max transform attempts before abando
90
104
  max-attempts-per-issue = 2 # max transform attempts before abandoning an issue
91
105
  features-limit = 4 # max feature files in features/ directory
92
106
  library-limit = 32 # max library entries in library/ directory
107
+ session-timeout-ms = 480000 # LLM session timeout in ms (8 min, below 10-min workflow step)
108
+ max-tokens = 200000 # token budget for tool-call cap calculation
109
+ max-read-chars = 50000 # max chars returned from read_file tool
110
+ max-test-output = 10000 # max chars of test output in prompts
111
+ max-file-listing = 100 # max files in directory listings (0 = unlimited)
112
+ max-library-index = 5000 # max chars for library index in prompts
113
+ max-fix-test-output = 15000 # max chars of failed run log in fix-code
93
114
 
94
115
  [profiles.max]
95
116
  # Thorough — maximum context for complex missions.
@@ -105,6 +126,18 @@ max-attempts-per-branch = 5 # max transform attempts before abando
105
126
  max-attempts-per-issue = 4 # max transform attempts before abandoning an issue
106
127
  features-limit = 8 # max feature files in features/ directory
107
128
  library-limit = 64 # max library entries in library/ directory
129
+ session-timeout-ms = 480000 # LLM session timeout in ms (8 min, below 10-min workflow step)
130
+ max-tokens = 500000 # token budget for tool-call cap calculation
131
+ max-read-chars = 100000 # max chars returned from read_file tool
132
+ max-test-output = 20000 # max chars of test output in prompts
133
+ max-file-listing = 0 # max files in directory listings (0 = unlimited)
134
+ max-library-index = 10000 # max chars for library index in prompts
135
+ max-fix-test-output = 30000 # max chars of failed run log in fix-code
136
+
137
+ [goals]
138
+ # W12/W13: Code coverage thresholds — stated in all code-changing prompts
139
+ min-line-coverage = 50 # minimum % line coverage required
140
+ min-branch-coverage = 30 # minimum % branch coverage required
108
141
 
109
142
  [mission-complete]
110
143
  # Thresholds for deterministic mission-complete declaration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.4.23",
3
+ "version": "7.4.25",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -213,6 +213,45 @@ async function executeMissionComplete(octokit, repo, reason) {
213
213
  } catch (err) {
214
214
  core.warning(`Could not commit MISSION_COMPLETE.md: ${err.message}`);
215
215
  }
216
+
217
+ // W2: Update persistent state (Benchmark 011 FINDING-3)
218
+ try {
219
+ const { readState, writeState } = await import("../../../copilot/state.js");
220
+ const state = readState(".");
221
+ state.status["mission-complete"] = true;
222
+ state.schedule["auto-disabled"] = true;
223
+ state.schedule["auto-disabled-reason"] = "mission-complete";
224
+ writeState(".", state);
225
+ core.info("State updated: mission-complete, schedule auto-disabled");
226
+ } catch (err) {
227
+ core.warning(`Could not update state for mission-complete: ${err.message}`);
228
+ }
229
+
230
+ // W3: Disable schedule on mission-complete (Benchmark 011 FINDING-4)
231
+ try {
232
+ await octokit.rest.actions.createWorkflowDispatch({
233
+ ...repo,
234
+ workflow_id: "agentic-lib-schedule.yml",
235
+ ref: "main",
236
+ inputs: { frequency: "off" },
237
+ });
238
+ core.info("Dispatched schedule change to off after mission-complete");
239
+ } catch (err) {
240
+ core.warning(`Could not dispatch schedule change: ${err.message}`);
241
+ }
242
+
243
+ // W16: Notify bot about mission-complete
244
+ try {
245
+ await octokit.rest.actions.createWorkflowDispatch({
246
+ ...repo,
247
+ workflow_id: "agentic-lib-bot.yml",
248
+ ref: "main",
249
+ inputs: { message: `Mission complete: ${reason.substring(0, 200)}` },
250
+ });
251
+ core.info("Dispatched bot notification for mission-complete");
252
+ } catch (err) {
253
+ core.warning(`Could not dispatch bot notification: ${err.message}`);
254
+ }
216
255
  }
217
256
 
218
257
  /**
@@ -287,6 +326,19 @@ async function executeMissionFailed(octokit, repo, reason, metricAssessment) {
287
326
  } catch (err) {
288
327
  core.warning(`Could not dispatch schedule change: ${err.message}`);
289
328
  }
329
+
330
+ // W16: Notify bot about mission-failed
331
+ try {
332
+ await octokit.rest.actions.createWorkflowDispatch({
333
+ ...repo,
334
+ workflow_id: "agentic-lib-bot.yml",
335
+ ref: "main",
336
+ inputs: { message: `Mission failed: ${metricDetail.substring(0, 200)}` },
337
+ });
338
+ core.info("Dispatched bot notification for mission-failed");
339
+ } catch (err) {
340
+ core.warning(`Could not dispatch bot notification: ${err.message}`);
341
+ }
290
342
  }
291
343
 
292
344
  /**
@@ -6,7 +6,7 @@
6
6
  // failures, writes fixes, and runs tests via tools.
7
7
 
8
8
  import * as core from "@actions/core";
9
- import { readFileSync } from "fs";
9
+ import { readFileSync, existsSync, readdirSync } from "fs";
10
10
  import { execSync } from "child_process";
11
11
  import { formatPathsSection, extractNarrative, NARRATIVE_INSTRUCTION } from "../copilot.js";
12
12
  import { runCopilotSession } from "../../../copilot/copilot-session.js";
@@ -25,14 +25,15 @@ function extractRunId(detailsUrl) {
25
25
  /**
26
26
  * Fetch actual test output from a GitHub Actions run log.
27
27
  */
28
- function fetchRunLog(runId) {
28
+ // W22: maxChars configurable via profile
29
+ function fetchRunLog(runId, maxChars = 8000) {
29
30
  try {
30
31
  const output = execSync(`gh run view ${runId} --log-failed`, {
31
32
  encoding: "utf8",
32
33
  timeout: 30000,
33
34
  env: { ...process.env },
34
35
  });
35
- return output.substring(0, 8000);
36
+ return output.substring(0, maxChars);
36
37
  } catch (err) {
37
38
  core.debug(`[fix-code] Could not fetch log for run ${runId}: ${err.message}`);
38
39
  return null;
@@ -138,7 +139,8 @@ async function resolveConflicts({ config, pr, prNumber, instructions, model, wri
138
139
  * Fix a broken main branch build.
139
140
  */
140
141
  async function fixMainBuild({ config, runId, instructions, model, writablePaths, testCommand, octokit, repo, logFilePath, screenshotFilePath }) {
141
- const logContent = fetchRunLog(runId);
142
+ const t = config.tuning || {};
143
+ const logContent = fetchRunLog(runId, t.maxFixTestOutput || 8000);
142
144
  if (!logContent) {
143
145
  core.info(`Could not fetch log for run ${runId}. Returning nop.`);
144
146
  return { outcome: "nop", details: `Could not fetch log for run ${runId}` };
@@ -169,7 +171,6 @@ async function fixMainBuild({ config, runId, instructions, model, writablePaths,
169
171
  "- Do not introduce new features — focus on making the build green",
170
172
  ].join("\n");
171
173
 
172
- const t = config.tuning || {};
173
174
  const systemPrompt =
174
175
  `You are an autonomous coding agent fixing a broken build on the main branch. The test/build workflow has failed. Analyze the error log and make minimal, targeted changes to fix it.` +
175
176
  NARRATIVE_INSTRUCTION;
@@ -254,7 +255,7 @@ export async function fixCode(context) {
254
255
  const runId = extractRunId(cr.details_url);
255
256
  let logContent = null;
256
257
  if (runId) {
257
- logContent = fetchRunLog(runId);
258
+ logContent = fetchRunLog(runId, (config.tuning || {}).maxFixTestOutput || 8000);
258
259
  }
259
260
  const detail = logContent || cr.output?.summary || "Failed";
260
261
  return `**${cr.name}:**\n${detail}`;
@@ -16,11 +16,12 @@ import { checkWipLimit } from "../safety.js";
16
16
  /**
17
17
  * Build a file listing summary (names + sizes, not content).
18
18
  */
19
- function buildFileListing(dirPath, extension) {
19
+ // W22: maxFiles configurable via profile (0 = unlimited)
20
+ function buildFileListing(dirPath, extension, maxFiles = 30) {
20
21
  if (!dirPath || !existsSync(dirPath)) return [];
21
22
  try {
22
23
  const files = readdirSync(dirPath, { recursive: true });
23
- return files
24
+ const filtered = files
24
25
  .filter((f) => String(f).endsWith(extension))
25
26
  .map((f) => {
26
27
  const fullPath = join(dirPath, String(f));
@@ -30,8 +31,8 @@ function buildFileListing(dirPath, extension) {
30
31
  } catch {
31
32
  return String(f);
32
33
  }
33
- })
34
- .slice(0, 30);
34
+ });
35
+ return maxFiles > 0 ? filtered.slice(0, maxFiles) : filtered;
35
36
  } catch {
36
37
  return [];
37
38
  }
@@ -14,11 +14,12 @@ import { runCopilotSession } from "../../../copilot/copilot-session.js";
14
14
  /**
15
15
  * Build a file listing summary (names + sizes, not content).
16
16
  */
17
- function buildFileListing(dirPath, extension) {
17
+ // W22: maxFiles configurable via profile (0 = unlimited)
18
+ function buildFileListing(dirPath, extension, maxFiles = 30) {
18
19
  if (!dirPath || !existsSync(dirPath)) return [];
19
20
  try {
20
21
  const files = readdirSync(dirPath, { recursive: true });
21
- return files
22
+ const filtered = files
22
23
  .filter((f) => String(f).endsWith(extension))
23
24
  .map((f) => {
24
25
  const fullPath = join(dirPath, String(f));
@@ -28,8 +29,8 @@ function buildFileListing(dirPath, extension) {
28
29
  } catch {
29
30
  return String(f);
30
31
  }
31
- })
32
- .slice(0, 30);
32
+ });
33
+ return maxFiles > 0 ? filtered.slice(0, maxFiles) : filtered;
33
34
  } catch {
34
35
  return [];
35
36
  }
@@ -651,6 +651,31 @@ async function executeCreateIssue(octokit, repo, params, ctx) {
651
651
  }
652
652
  const body = bodyParts.join("\n");
653
653
 
654
+ // W5: Dedup guard against open issues — skip if a similarly-titled issue already exists
655
+ try {
656
+ const { data: openIssues } = await octokit.rest.issues.listForRepo({
657
+ ...repo,
658
+ state: "open",
659
+ labels: "automated",
660
+ sort: "created",
661
+ direction: "desc",
662
+ per_page: 20,
663
+ });
664
+ const titleLower = title.toLowerCase();
665
+ const titlePrefix = titleLower.substring(0, 30);
666
+ const openDuplicate = openIssues.find(
667
+ (i) =>
668
+ !i.pull_request &&
669
+ (i.title.toLowerCase().includes(titlePrefix) || titleLower.includes(i.title.toLowerCase().substring(0, 30))),
670
+ );
671
+ if (openDuplicate) {
672
+ core.info(`Skipping duplicate issue (similar to open #${openDuplicate.number}: "${openDuplicate.title}")`);
673
+ return `skipped:duplicate-open-#${openDuplicate.number}`;
674
+ }
675
+ } catch (err) {
676
+ core.warning(`Open issue dedup check failed: ${err.message}`);
677
+ }
678
+
654
679
  // Dedup guard: skip if a similarly-titled issue was closed in the last hour
655
680
  // Exclude issues closed before the init timestamp (cross-scenario protection)
656
681
  try {
@@ -8,19 +8,21 @@
8
8
  import * as core from "@actions/core";
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
10
10
  import { join, resolve } from "path";
11
+ import { execSync } from "child_process";
11
12
  import { readOptionalFile, formatPathsSection, extractNarrative, NARRATIVE_INSTRUCTION } from "../copilot.js";
12
13
  import { runCopilotSession } from "../../../copilot/copilot-session.js";
13
14
  import { createGitHubTools, createGitTools } from "../../../copilot/github-tools.js";
14
15
 
15
16
  /**
16
17
  * Build a file listing summary (names + sizes, not content) for the lean prompt.
18
+ * W22: maxFiles configurable via profile (0 = unlimited).
17
19
  */
18
- function buildFileListing(dirPath, extensions) {
20
+ function buildFileListing(dirPath, extensions, maxFiles = 30) {
19
21
  if (!dirPath || !existsSync(dirPath)) return [];
20
22
  const exts = Array.isArray(extensions) ? extensions : [extensions];
21
23
  try {
22
24
  const files = readdirSync(dirPath, { recursive: true });
23
- return files
25
+ const filtered = files
24
26
  .filter((f) => exts.some((ext) => String(f).endsWith(ext)))
25
27
  .map((f) => {
26
28
  const fullPath = join(dirPath, String(f));
@@ -31,17 +33,18 @@ function buildFileListing(dirPath, extensions) {
31
33
  } catch {
32
34
  return String(f);
33
35
  }
34
- })
35
- .slice(0, 30); // cap listing at 30 files
36
+ });
37
+ return maxFiles > 0 ? filtered.slice(0, maxFiles) : filtered;
36
38
  } catch {
37
39
  return [];
38
40
  }
39
41
  }
40
42
 
41
43
  /**
42
- * Build a library index: filename + first 2 lines of each library doc, capped at 2000 chars.
44
+ * Build a library index: filename + first 2 lines of each library doc.
45
+ * W22: maxChars configurable via profile.
43
46
  */
44
- function buildLibraryIndex(libraryPath) {
47
+ function buildLibraryIndex(libraryPath, maxChars = 2000) {
45
48
  if (!libraryPath || !existsSync(libraryPath)) return "";
46
49
  try {
47
50
  const files = readdirSync(libraryPath).filter((f) => f.endsWith(".md")).sort();
@@ -54,7 +57,7 @@ function buildLibraryIndex(libraryPath) {
54
57
  const content = readFileSync(fullPath, "utf8");
55
58
  const lines = content.split("\n").slice(0, 2).join(" ").trim();
56
59
  const entry = `- ${f}: ${lines}`;
57
- if (totalLen + entry.length > 2000) break;
60
+ if (totalLen + entry.length > maxChars) break;
58
61
  entries.push(entry);
59
62
  totalLen += entry.length;
60
63
  } catch {
@@ -67,6 +70,18 @@ function buildLibraryIndex(libraryPath) {
67
70
  }
68
71
  }
69
72
 
73
+ /**
74
+ * W9: Get worktree file listing via git ls-files.
75
+ */
76
+ function getWorktreeFiles() {
77
+ try {
78
+ const gitFiles = execSync("git ls-files", { encoding: "utf8", timeout: 10000 }).trim();
79
+ return gitFiles.split("\n").filter(Boolean);
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
70
85
  /**
71
86
  * Run the full transformation pipeline from mission to code.
72
87
  *
@@ -76,6 +91,8 @@ function buildLibraryIndex(libraryPath) {
76
91
  export async function transform(context) {
77
92
  const { config, instructions, writablePaths, testCommand, model, octokit, repo, issueNumber, logFilePath, screenshotFilePath } = context;
78
93
  const t = config.tuning || {};
94
+ const maxFileListing = t.maxFileListing ?? 30;
95
+ const maxLibraryIdx = t.maxLibraryIndex || 2000;
79
96
 
80
97
  // Read mission (required)
81
98
  const mission = readOptionalFile(config.paths.mission.path);
@@ -90,23 +107,24 @@ export async function transform(context) {
90
107
  return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
91
108
  }
92
109
 
93
- // Fetch target issue if specified
94
- let targetIssueSection = "";
95
- if (issueNumber) {
110
+ // W7: Fetch all target issues (supports comma-separated list)
111
+ const issueNumbers = issueNumber
112
+ ? String(issueNumber).split(",").map((n) => n.trim()).filter(Boolean)
113
+ : [];
114
+ const targetIssueSections = [];
115
+ for (const num of issueNumbers) {
96
116
  try {
97
117
  const { data: issue } = await octokit.rest.issues.get({
98
118
  ...repo,
99
- issue_number: Number(issueNumber),
119
+ issue_number: Number(num),
100
120
  });
101
- targetIssueSection = [
102
- `## Target Issue #${issue.number}: ${issue.title}`,
121
+ targetIssueSections.push([
122
+ `### Issue #${issue.number}: ${issue.title}`,
103
123
  issue.body || "(no description)",
104
124
  `Labels: ${issue.labels.map((l) => l.name).join(", ") || "none"}`,
105
- "",
106
- "**Focus your transformation on resolving this specific issue.**",
107
- ].join("\n");
125
+ ].join("\n"));
108
126
  } catch (err) {
109
- core.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
127
+ core.warning(`Could not fetch target issue #${num}: ${err.message}`);
110
128
  }
111
129
  }
112
130
 
@@ -114,17 +132,36 @@ export async function transform(context) {
114
132
  instructions || "Transform the repository toward its mission by identifying the next best action.";
115
133
 
116
134
  // ── Build lean prompt (structure + mission, not file contents) ──────
117
- const sourceFiles = buildFileListing(config.paths.source.path, [".js", ".ts"]);
118
- const testFiles = buildFileListing(config.paths.tests.path, [".js", ".ts"]);
119
- const webFiles = buildFileListing(config.paths.web?.path || "src/web/", [".html", ".css", ".js"]);
120
- const featureFiles = buildFileListing(config.paths.features.path, [".md"]);
121
- const libraryIndex = buildLibraryIndex(config.paths.library?.path || "library/");
135
+ const sourceFiles = buildFileListing(config.paths.source.path, [".js", ".ts"], maxFileListing);
136
+ const testFiles = buildFileListing(config.paths.tests.path, [".js", ".ts"], maxFileListing);
137
+ const webFiles = buildFileListing(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], maxFileListing);
138
+ const featureFiles = buildFileListing(config.paths.features.path, [".md"], maxFileListing);
139
+ const libraryIndex = buildLibraryIndex(config.paths.library?.path || "library/", maxLibraryIdx);
140
+
141
+ // W9: worktree file listing
142
+ const worktreeFiles = getWorktreeFiles();
143
+
144
+ // W17: Implementation review results from upstream
145
+ const reviewAdvice = process.env.REVIEW_ADVICE || "";
146
+ const reviewGapsRaw = process.env.REVIEW_GAPS || "";
147
+
148
+ // W19: Telemetry test output from upstream
149
+ const telemetryTestSummary = process.env.TELEMETRY_UNIT_TEST_SUMMARY || "";
150
+ const telemetryTestOutput = process.env.TELEMETRY_UNIT_TEST_OUTPUT || "";
122
151
 
123
152
  const prompt = [
124
153
  "## Instructions",
125
154
  agentInstructions,
126
155
  "",
127
- ...(targetIssueSection ? [targetIssueSection, ""] : []),
156
+ // W7: Multiple target issues
157
+ ...(targetIssueSections.length > 0 ? [
158
+ `## Target Issues (${targetIssueSections.length})`,
159
+ ...targetIssueSections.map((s) => s + "\n"),
160
+ targetIssueSections.length > 1
161
+ ? "**Resolve as many of these issues as you can in this session. Address them all if possible.**"
162
+ : "**Focus your transformation on resolving this specific issue.**",
163
+ "",
164
+ ] : []),
128
165
  "## Mission",
129
166
  mission,
130
167
  "",
@@ -143,12 +180,47 @@ export async function transform(context) {
143
180
  "Reference documents available in `library/` (use read_file for full content):",
144
181
  libraryIndex,
145
182
  ] : []),
183
+ // W9: worktree file listing
184
+ ...(worktreeFiles.length > 0 ? [
185
+ "",
186
+ `## Worktree Files (${worktreeFiles.length} non-ignored files)`,
187
+ worktreeFiles.join("\n"),
188
+ ] : []),
189
+ // W19: Current test state from telemetry
190
+ ...(telemetryTestSummary ? [
191
+ "",
192
+ "## Current Test State (from telemetry)",
193
+ `Summary: ${telemetryTestSummary}`,
194
+ ...(telemetryTestOutput ? [`\`\`\`\n${telemetryTestOutput}\n\`\`\``] : []),
195
+ ] : []),
196
+ // W17: Implementation review
197
+ ...(reviewAdvice ? [
198
+ "",
199
+ "## Implementation Review",
200
+ `**Completeness:** ${reviewAdvice}`,
201
+ ...((() => {
202
+ try {
203
+ const gaps = JSON.parse(reviewGapsRaw || "[]");
204
+ if (gaps.length > 0) {
205
+ return [
206
+ "",
207
+ "**Gaps Found:**",
208
+ ...gaps.map((g) => `- [${g.severity}] ${g.element}: ${g.description} (${g.gapType})`),
209
+ "",
210
+ "Address these gaps in your transformation if they fall within the target issues.",
211
+ ];
212
+ }
213
+ } catch { /* ignore */ }
214
+ return [];
215
+ })()),
216
+ ] : []),
146
217
  "",
147
218
  "## Your Task",
148
219
  "Analyze the mission and open issues (use list_issues tool).",
149
220
  "Read the source files you need (use read_file tool).",
150
- "Determine the single most impactful next step to transform this repository.",
151
- "Then implement that step, writing files and running run_tests to verify.",
221
+ issueNumbers.length > 1
222
+ ? "Resolve all target issues listed above. Implement all changes, write tests, update the website, and run run_tests to verify."
223
+ : "Determine the single most impactful next step to transform this repository.\nThen implement that step, writing files and running run_tests to verify.",
152
224
  "",
153
225
  "## When NOT to make changes",
154
226
  "If the existing code already satisfies all requirements in MISSION.md and all open issues have been addressed:",
@@ -162,6 +234,9 @@ export async function transform(context) {
162
234
  `- Run \`${testCommand}\` via run_tests to validate your changes`,
163
235
  "- Use list_issues to see open issues, get_issue for full details",
164
236
  "- Use read_file to read source files you need (don't guess at contents)",
237
+ ...(config.coverageGoals ? [
238
+ `- Required code coverage: ≥${config.coverageGoals.minLineCoverage}% lines, ≥${config.coverageGoals.minBranchCoverage}% branches`,
239
+ ] : []),
165
240
  ].join("\n");
166
241
 
167
242
  core.info(`Transform lean prompt length: ${prompt.length} chars`);
@@ -179,11 +254,61 @@ export async function transform(context) {
179
254
  const systemPrompt =
180
255
  "You are an autonomous code transformation agent. Your goal is to advance the repository toward its mission by making the most impactful change possible in a single step." + NARRATIVE_INSTRUCTION;
181
256
 
182
- // ── Create custom tools (GitHub API + git) ─────────────────────────
257
+ // ── Create custom tools (GitHub API + git + W8 behaviour dry-run) ──
183
258
  const createTools = (defineTool, _wp, logger) => {
184
259
  const ghTools = createGitHubTools(octokit, repo, defineTool, logger);
185
260
  const gitTools = createGitTools(defineTool, logger);
186
- return [...ghTools, ...gitTools];
261
+
262
+ // W8: Dry-run behaviour test tool — reads test specs and source code,
263
+ // returns them to the LLM for reasoning about whether code would pass
264
+ const dryRunBehaviourTests = defineTool("dry_run_behaviour_tests", {
265
+ description: "Read behaviour test specifications and the source code they test, then return both for analysis. Use this to check if your code changes would pass behaviour tests without running Playwright. Call this after making code changes but before committing.",
266
+ parameters: { type: "object", properties: {}, required: [] },
267
+ handler: async () => {
268
+ const behaviourPath = config.paths.behaviour?.path || "tests/behaviour/";
269
+ const sourcePath = config.paths.source?.path || "src/lib/";
270
+ const webPath = config.paths.web?.path || "src/web/";
271
+
272
+ const readDir = (dir, exts) => {
273
+ if (!existsSync(dir)) return [];
274
+ try {
275
+ return readdirSync(dir)
276
+ .filter((f) => exts.some((e) => f.endsWith(e)))
277
+ .slice(0, 10)
278
+ .map((f) => {
279
+ try {
280
+ return { file: f, content: readFileSync(join(dir, f), "utf8") };
281
+ } catch { return { file: f, content: "(unreadable)" }; }
282
+ });
283
+ } catch { return []; }
284
+ };
285
+
286
+ const specs = readDir(behaviourPath, [".spec.js", ".spec.ts", ".test.js", ".test.ts"]);
287
+ const sources = readDir(sourcePath, [".js", ".ts"]);
288
+ const webFilesLocal = readDir(webPath, [".html", ".js"]);
289
+
290
+ if (specs.length === 0) {
291
+ return { textResultForLlm: "No behaviour test files found. Behaviour tests are not configured for this project." };
292
+ }
293
+
294
+ const parts = [
295
+ "## Behaviour Test Specifications",
296
+ ...specs.map((s) => `### ${s.file}\n\`\`\`\n${s.content}\n\`\`\``),
297
+ "",
298
+ "## Source Code Under Test",
299
+ ...sources.map((s) => `### ${s.file}\n\`\`\`\n${s.content}\n\`\`\``),
300
+ ];
301
+ if (webFilesLocal.length > 0) {
302
+ parts.push("", "## Website Files", ...webFilesLocal.map((s) => `### ${s.file}\n\`\`\`\n${s.content}\n\`\`\``));
303
+ }
304
+ parts.push("", "## Your Analysis", "Analyze whether the current source code and website would pass these behaviour tests. Report any gaps.");
305
+
306
+ logger.info(`[tool] dry_run_behaviour_tests: ${specs.length} specs, ${sources.length} sources, ${webFilesLocal.length} web files`);
307
+ return { textResultForLlm: parts.join("\n") };
308
+ },
309
+ });
310
+
311
+ return [...ghTools, ...gitTools, dryRunBehaviourTests];
187
312
  };
188
313
 
189
314
  // ── Run hybrid session ─────────────────────────────────────────────
@@ -92,6 +92,13 @@ function parseTuningProfile(profileSection) {
92
92
  issuesScan: profileSection["max-issues"] || 20,
93
93
  staleDays: profileSection["stale-days"] || 30,
94
94
  discussionComments: profileSection["max-discussion-comments"] || 10,
95
+ sessionTimeoutMs: profileSection["session-timeout-ms"] || 480000,
96
+ maxTokens: profileSection["max-tokens"] || 200000,
97
+ maxReadChars: profileSection["max-read-chars"] || 20000,
98
+ maxTestOutput: profileSection["max-test-output"] || 4000,
99
+ maxFileListing: profileSection["max-file-listing"] ?? 30,
100
+ maxLibraryIndex: profileSection["max-library-index"] || 2000,
101
+ maxFixTestOutput: profileSection["max-fix-test-output"] || 8000,
95
102
  };
96
103
  }
97
104
 
@@ -132,7 +139,7 @@ function readPackageJson(tomlPath, depsRelPath) {
132
139
  * @param {Object} [profilesSection] - The [profiles] section from TOML (source of truth)
133
140
  */
134
141
  function resolveTuning(tuningSection, profilesSection) {
135
- const profileName = tuningSection.profile || "recommended";
142
+ const profileName = tuningSection.profile || "med";
136
143
  const tomlProfile = profilesSection?.[profileName];
137
144
  const profile = parseTuningProfile(tomlProfile) || FALLBACK_TUNING;
138
145
  const tuning = { ...profile, profileName };
@@ -149,6 +156,13 @@ function resolveTuning(tuningSection, profilesSection) {
149
156
  "max-issues": "issuesScan",
150
157
  "stale-days": "staleDays",
151
158
  "max-discussion-comments": "discussionComments",
159
+ "session-timeout-ms": "sessionTimeoutMs",
160
+ "max-tokens": "maxTokens",
161
+ "max-read-chars": "maxReadChars",
162
+ "max-test-output": "maxTestOutput",
163
+ "max-file-listing": "maxFileListing",
164
+ "max-library-index": "maxLibraryIndex",
165
+ "max-fix-test-output": "maxFixTestOutput",
152
166
  };
153
167
  for (const [tomlKey, jsKey] of Object.entries(numericOverrides)) {
154
168
  if (tuningSection[tomlKey] > 0) tuning[jsKey] = tuningSection[tomlKey];
@@ -239,6 +253,13 @@ export function loadConfig(configPath) {
239
253
  const execution = toml.execution || {};
240
254
  const bot = toml.bot || {};
241
255
 
256
+ // W13: Code coverage goals
257
+ const goals = toml.goals || {};
258
+ const coverageGoals = {
259
+ minLineCoverage: goals["min-line-coverage"] ?? 50,
260
+ minBranchCoverage: goals["min-branch-coverage"] ?? 30,
261
+ };
262
+
242
263
  // Mission-complete thresholds (with safe defaults)
243
264
  // C6: Removed minDedicatedTests and requireDedicatedTests
244
265
  const mc = toml["mission-complete"] || {};
@@ -267,6 +288,7 @@ export function loadConfig(configPath) {
267
288
  init: toml.init || null,
268
289
  tdd: toml.tdd === true,
269
290
  missionCompleteThresholds,
291
+ coverageGoals,
270
292
  maxTokensPerMaintain: resolvedLimits.maxTokensPerMaintain || 200000,
271
293
  writablePaths,
272
294
  readOnlyPaths,
@@ -74,7 +74,7 @@ export async function runCopilotSession({
74
74
  model = "gpt-5-mini",
75
75
  githubToken,
76
76
  tuning = {},
77
- timeoutMs = 600000,
77
+ timeoutMs,
78
78
  agentPrompt,
79
79
  userPrompt,
80
80
  writablePaths,
@@ -94,6 +94,11 @@ export async function runCopilotSession({
94
94
 
95
95
  const wsPath = resolve(workspacePath);
96
96
 
97
+ // W11: Session timeout — defaults to 480s (8 min), leaving 2 min headroom
98
+ // below the 10-min workflow step timeout for graceful shutdown.
99
+ // Callers can override via timeoutMs parameter or tuning.sessionTimeoutMs.
100
+ const effectiveTimeoutMs = timeoutMs || tuning.sessionTimeoutMs || 480000;
101
+
97
102
  // ── Writable paths ──────────────────────────────────────────────────
98
103
  // Default: entire workspace is writable (local CLI mode)
99
104
  const effectiveWritablePaths = writablePaths || [wsPath + "/"];
@@ -154,7 +159,7 @@ export async function runCopilotSession({
154
159
  const systemPrompt = basePrompt + NARRATIVE_INSTRUCTION;
155
160
 
156
161
  // ── Session config ─────────────────────────────────────────────────
157
- logger.info(`[agentic-lib] Creating session (model=${model}, workspace=${wsPath})`);
162
+ logger.info(`[agentic-lib] Creating session (model=${model}, workspace=${wsPath}, timeout=${Math.round(effectiveTimeoutMs / 1000)}s)`);
158
163
 
159
164
  const client = new CopilotClient({
160
165
  env: { ...process.env, GITHUB_TOKEN: copilotToken, GH_TOKEN: copilotToken },
@@ -192,7 +197,7 @@ export async function runCopilotSession({
192
197
  // Truncate large read_file results to prevent context overflow
193
198
  if (input.toolName === "read_file" || input.toolName === "view") {
194
199
  const resultText = input.toolResult?.textResultForLlm || "";
195
- const MAX_READ_CHARS = 20000;
200
+ const MAX_READ_CHARS = tuning.maxReadChars || 20000;
196
201
  if (resultText.length > MAX_READ_CHARS) {
197
202
  hookOutput.modifiedResult = {
198
203
  ...input.toolResult,
@@ -296,7 +301,7 @@ export async function runCopilotSession({
296
301
 
297
302
  const prompt = userPrompt || [
298
303
  `# Mission\n\n${missionText}`,
299
- `# Current test state\n\n\`\`\`\n${initialTestOutput.substring(0, 4000)}\n\`\`\``,
304
+ `# Current test state\n\n\`\`\`\n${initialTestOutput.substring(0, tuning.maxTestOutput || 4000)}\n\`\`\``,
300
305
  "",
301
306
  "Implement this mission. Read the existing source code and tests,",
302
307
  "make the required changes, run run_tests to verify, and iterate until all tests pass.",
@@ -315,7 +320,7 @@ export async function runCopilotSession({
315
320
  }
316
321
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
317
322
  try {
318
- response = await session.sendAndWait(sendOptions, timeoutMs);
323
+ response = await session.sendAndWait(sendOptions, effectiveTimeoutMs);
319
324
  break;
320
325
  } catch (err) {
321
326
  if (isRateLimitError(err) && attempt < maxRetries) {
package/src/iterate.js CHANGED
@@ -108,7 +108,7 @@ export function readTransformationCost(targetPath) {
108
108
 
109
109
  /**
110
110
  * Read transformation budget from agentic-lib.toml.
111
- * Falls back to 8 (the "recommended" profile default).
111
+ * Falls back to 8 (the "med" profile default).
112
112
  */
113
113
  export function readBudget(targetPath) {
114
114
  const tomlPath = resolve(targetPath, "agentic-lib.toml");
@@ -17,7 +17,7 @@
17
17
  "author": "",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@xn-intenton-z2a/agentic-lib": "^7.4.23"
20
+ "@xn-intenton-z2a/agentic-lib": "^7.4.25"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@playwright/test": "^1.58.0",