@xn-intenton-z2a/agentic-lib 7.1.51 → 7.1.53

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.
@@ -0,0 +1,708 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2025-2026 Polycode Limited
3
+ # .github/workflows/agentic-lib-workflow.yml
4
+ #
5
+ # Main autonomous pipeline: supervisor + maintain + review + dev + PR cleanup.
6
+ # Replaces agent-supervisor, agent-flow-transform, agent-flow-review,
7
+ # agent-flow-maintain, agent-flow-fix-code, and ci-automerge.
8
+
9
+ name: agentic-lib-workflow
10
+ run-name: "agentic-lib-workflow [${{ github.ref_name }}]"
11
+ concurrency:
12
+ group: agentic-lib-workflow
13
+ cancel-in-progress: false
14
+
15
+ on:
16
+ schedule:
17
+ - cron: "0 6 * * 1"
18
+ workflow_dispatch:
19
+ inputs:
20
+ model:
21
+ description: "Copilot SDK model to use"
22
+ type: choice
23
+ required: false
24
+ default: "gpt-5-mini"
25
+ options:
26
+ - gpt-5-mini
27
+ - claude-sonnet-4
28
+ - gpt-4.1
29
+ mode:
30
+ description: "Run mode"
31
+ type: choice
32
+ required: false
33
+ default: "full"
34
+ options:
35
+ - full
36
+ - dev-only
37
+ - maintain-only
38
+ - review-only
39
+ - pr-cleanup-only
40
+ message:
41
+ description: "Message from bot or human (context for supervisor)"
42
+ type: string
43
+ required: false
44
+ default: ""
45
+ issue-number:
46
+ description: "Target specific issue (dev-only mode)"
47
+ type: string
48
+ required: false
49
+ default: ""
50
+ schedule:
51
+ description: "Change schedule frequency after this run"
52
+ type: choice
53
+ required: false
54
+ default: ""
55
+ options:
56
+ - ""
57
+ - "off"
58
+ - "weekly"
59
+ - "daily"
60
+ - "hourly"
61
+ - "continuous"
62
+ pr-number:
63
+ description: "Target specific PR for fix"
64
+ type: string
65
+ required: false
66
+ default: ""
67
+
68
+ permissions:
69
+ actions: write
70
+ contents: write
71
+ issues: write
72
+ pull-requests: write
73
+ checks: read
74
+ discussions: write
75
+
76
+ env:
77
+ configPath: ".github/agentic-lib/agents/agentic-lib.yml"
78
+ maxFixAttempts: "3"
79
+ stalePrDays: "3"
80
+
81
+ jobs:
82
+ # ─── Normalise inputs ──────────────────────────────────────────────
83
+ params:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - name: Normalise params
87
+ id: normalise
88
+ shell: bash
89
+ run: |
90
+ MODEL='${{ inputs.model }}'
91
+ echo "model=${MODEL:-gpt-5-mini}" >> $GITHUB_OUTPUT
92
+ MODE='${{ inputs.mode }}'
93
+ echo "mode=${MODE:-full}" >> $GITHUB_OUTPUT
94
+ echo "message=${{ inputs.message }}" >> $GITHUB_OUTPUT
95
+ ISSUE='${{ inputs.issue-number }}'
96
+ echo "issue-number=${ISSUE}" >> $GITHUB_OUTPUT
97
+ SCHEDULE='${{ inputs.schedule }}'
98
+ echo "schedule=${SCHEDULE}" >> $GITHUB_OUTPUT
99
+ PR='${{ inputs.pr-number }}'
100
+ echo "pr-number=${PR}" >> $GITHUB_OUTPUT
101
+ outputs:
102
+ model: ${{ steps.normalise.outputs.model }}
103
+ mode: ${{ steps.normalise.outputs.mode }}
104
+ message: ${{ steps.normalise.outputs.message }}
105
+ issue-number: ${{ steps.normalise.outputs.issue-number }}
106
+ schedule: ${{ steps.normalise.outputs.schedule }}
107
+ pr-number: ${{ steps.normalise.outputs.pr-number }}
108
+
109
+ # ─── PR Cleanup: merge/close/delete stale PRs and branches ─────────
110
+ pr-cleanup:
111
+ needs: params
112
+ if: needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'pr-cleanup-only'
113
+ runs-on: ubuntu-latest
114
+ steps:
115
+ - name: PR cleanup
116
+ uses: actions/github-script@v7
117
+ with:
118
+ script: |
119
+ const owner = context.repo.owner;
120
+ const repo = context.repo.repo;
121
+ const stalePrDays = parseInt(process.env.stalePrDays) || 3;
122
+ const prCutoff = new Date(Date.now() - stalePrDays * 24 * 60 * 60 * 1000);
123
+ let mergedPRs = [];
124
+ let closedPRs = [];
125
+
126
+ // List open PRs with automerge label
127
+ const { data: openPRs } = await github.rest.pulls.list({
128
+ owner, repo, state: 'open', per_page: 20,
129
+ });
130
+
131
+ for (const pr of openPRs) {
132
+ const hasAutomerge = pr.labels.some(l => l.name === 'automerge');
133
+ if (!hasAutomerge) continue;
134
+
135
+ const { data: fullPr } = await github.rest.pulls.get({
136
+ owner, repo, pull_number: pr.number,
137
+ });
138
+
139
+ if (fullPr.mergeable && fullPr.mergeable_state === 'clean') {
140
+ // Merge it
141
+ try {
142
+ await github.rest.pulls.merge({
143
+ owner, repo, pull_number: pr.number, merge_method: 'squash',
144
+ });
145
+ core.info(`Merged PR #${pr.number}`);
146
+ // Delete branch
147
+ try {
148
+ await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}` });
149
+ } catch (e) { core.info(`Could not delete branch: ${e.message}`); }
150
+
151
+ // Label associated issue
152
+ const branchPrefix = 'agentic-lib-issue-';
153
+ if (pr.head.ref.startsWith(branchPrefix)) {
154
+ const issueNum = parseInt(pr.head.ref.replace(branchPrefix, ''));
155
+ if (issueNum) {
156
+ try {
157
+ await github.rest.issues.addLabels({ owner, repo, issue_number: issueNum, labels: ['merged'] });
158
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNum, name: 'in-progress' });
159
+ } catch (e) { /* label may not exist */ }
160
+ }
161
+ }
162
+ mergedPRs.push(pr.number);
163
+ } catch (e) {
164
+ core.warning(`Could not merge PR #${pr.number}: ${e.message}`);
165
+ }
166
+ } else if (fullPr.mergeable_state === 'dirty' || fullPr.mergeable === false) {
167
+ const isStale = new Date(pr.updated_at) < prCutoff;
168
+ if (isStale) {
169
+ // Close stale conflicting PR
170
+ core.info(`Closing stale conflicting PR #${pr.number}`);
171
+ await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' });
172
+ try {
173
+ await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}` });
174
+ } catch (e) { /* branch may already be gone */ }
175
+ closedPRs.push(pr.number);
176
+ } else {
177
+ // Remove automerge label from conflicting but fresh PR
178
+ try {
179
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: 'automerge' });
180
+ await github.rest.issues.createComment({ owner, repo, issue_number: pr.number,
181
+ body: 'Automerge label removed — PR has conflicts. Resolve and re-add label to retry.' });
182
+ } catch (e) { /* label may not exist */ }
183
+ }
184
+ }
185
+ // If unstable/unknown: skip, next run handles
186
+ }
187
+ core.setOutput('merged-prs', JSON.stringify(mergedPRs));
188
+ core.setOutput('closed-prs', JSON.stringify(closedPRs));
189
+ id: cleanup
190
+ outputs:
191
+ merged-prs: ${{ steps.cleanup.outputs.merged-prs }}
192
+ closed-prs: ${{ steps.cleanup.outputs.closed-prs }}
193
+
194
+ # ─── Telemetry: gather repo state for supervisor ───────────────────
195
+ telemetry:
196
+ needs: params
197
+ if: needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only' || needs.params.outputs.mode == 'review-only'
198
+ runs-on: ubuntu-latest
199
+ steps:
200
+ - uses: actions/checkout@v4
201
+ - name: Gather telemetry
202
+ id: gather
203
+ uses: actions/github-script@v7
204
+ with:
205
+ script: |
206
+ const fs = require('fs');
207
+ const owner = context.repo.owner;
208
+ const repo = context.repo.repo;
209
+
210
+ // Open issues
211
+ const { data: issues } = await github.rest.issues.listForRepo({
212
+ owner, repo, state: 'open', per_page: 20,
213
+ labels: 'automated',
214
+ });
215
+ const issuesSummary = issues.map(i => ({
216
+ number: i.number, title: i.title,
217
+ labels: i.labels.map(l => l.name),
218
+ }));
219
+
220
+ // Open PRs
221
+ const { data: prs } = await github.rest.pulls.list({
222
+ owner, repo, state: 'open', per_page: 10,
223
+ });
224
+ const prsSummary = prs.map(p => ({
225
+ number: p.number, title: p.title, branch: p.head.ref,
226
+ labels: p.labels.map(l => l.name),
227
+ }));
228
+
229
+ // Recent workflow runs
230
+ const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({
231
+ owner, repo, per_page: 10,
232
+ });
233
+ const runsSummary = runs.workflow_runs.map(r => ({
234
+ name: r.name, status: r.status, conclusion: r.conclusion,
235
+ created: r.created_at,
236
+ }));
237
+
238
+ // Read local files
239
+ let mission = '';
240
+ try { mission = fs.readFileSync('MISSION.md', 'utf8').slice(0, 2000); } catch (e) {}
241
+ let features = [];
242
+ try {
243
+ const featDir = 'features/';
244
+ if (fs.existsSync(featDir)) {
245
+ features = fs.readdirSync(featDir).filter(f => f.endsWith('.md'));
246
+ }
247
+ } catch (e) {}
248
+
249
+ // Message from bot/human
250
+ const message = '${{ needs.params.outputs.message }}';
251
+
252
+ const telemetry = {
253
+ issues: issuesSummary,
254
+ prs: prsSummary,
255
+ recentRuns: runsSummary,
256
+ mission: mission.slice(0, 500),
257
+ featureFiles: features,
258
+ message: message || null,
259
+ };
260
+
261
+ // Write to file for downstream jobs
262
+ fs.writeFileSync('/tmp/telemetry.json', JSON.stringify(telemetry, null, 2));
263
+ // Set as output (truncated for GitHub output limits)
264
+ const summary = JSON.stringify(telemetry);
265
+ core.setOutput('telemetry', summary.slice(0, 60000));
266
+ outputs:
267
+ telemetry: ${{ steps.gather.outputs.telemetry }}
268
+
269
+ # ─── Supervisor: LLM decides what to do ─────────────────────────────
270
+ supervisor:
271
+ needs: [params, pr-cleanup, telemetry]
272
+ if: |
273
+ always() &&
274
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
275
+ needs.params.result == 'success'
276
+ runs-on: ubuntu-latest
277
+ steps:
278
+ - uses: actions/checkout@v4
279
+
280
+ - uses: actions/setup-node@v4
281
+ with:
282
+ node-version: "24"
283
+
284
+ - name: Install agentic-step dependencies
285
+ working-directory: .github/agentic-lib/actions/agentic-step
286
+ run: npm ci
287
+
288
+ - name: Run supervisor
289
+ uses: ./.github/agentic-lib/actions/agentic-step
290
+ env:
291
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
292
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
293
+ with:
294
+ task: "supervise"
295
+ config: ${{ env.configPath }}
296
+ instructions: ".github/agentic-lib/agents/agent-supervisor.md"
297
+ model: ${{ needs.params.outputs.model }}
298
+
299
+ # ─── Maintain: features + library (push to main) ───────────────────
300
+ maintain:
301
+ needs: [params, supervisor]
302
+ if: |
303
+ always() &&
304
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'maintain-only') &&
305
+ needs.params.result == 'success'
306
+ runs-on: ubuntu-latest
307
+ steps:
308
+ - uses: actions/checkout@v4
309
+ with:
310
+ fetch-depth: 0
311
+
312
+ - uses: actions/setup-node@v4
313
+ with:
314
+ node-version: "24"
315
+
316
+ - name: Install agentic-step dependencies
317
+ working-directory: .github/agentic-lib/actions/agentic-step
318
+ run: npm ci
319
+
320
+ - name: Load config
321
+ id: config
322
+ run: |
323
+ CONFIG="${{ env.configPath }}"
324
+ FEATURES=$(yq -r '.paths.featuresPath.path // "features/"' "$CONFIG")
325
+ LIBRARY=$(yq -r '.paths.libraryDocumentsPath.path // "library/"' "$CONFIG")
326
+ SOURCES=$(yq -r '.paths.librarySourcesFilepath.path // "SOURCES.md"' "$CONFIG")
327
+ echo "featuresWritablePaths=${FEATURES}" >> $GITHUB_OUTPUT
328
+ echo "libraryWritablePaths=${LIBRARY};${SOURCES}" >> $GITHUB_OUTPUT
329
+
330
+ - name: Maintain features
331
+ uses: ./.github/agentic-lib/actions/agentic-step
332
+ env:
333
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
334
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
335
+ with:
336
+ task: "maintain-features"
337
+ config: ${{ env.configPath }}
338
+ instructions: ".github/agentic-lib/agents/agent-maintain-features.md"
339
+ writable-paths: ${{ steps.config.outputs.featuresWritablePaths }}
340
+ model: ${{ needs.params.outputs.model }}
341
+
342
+ - name: Maintain library
343
+ uses: ./.github/agentic-lib/actions/agentic-step
344
+ env:
345
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
346
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
347
+ with:
348
+ task: "maintain-library"
349
+ config: ${{ env.configPath }}
350
+ instructions: ".github/agentic-lib/agents/agent-maintain-library.md"
351
+ writable-paths: ${{ steps.config.outputs.libraryWritablePaths }}
352
+ model: ${{ needs.params.outputs.model }}
353
+
354
+ - name: Commit and push changes
355
+ uses: ./.github/agentic-lib/actions/commit-if-changed
356
+ with:
357
+ commit-message: "agentic-step: maintain features and library"
358
+ push-ref: ${{ github.ref_name }}
359
+
360
+ # ─── Fix stuck PRs with failing checks ─────────────────────────────
361
+ fix-stuck:
362
+ needs: [params, supervisor]
363
+ if: |
364
+ always() &&
365
+ needs.params.outputs.mode == 'full' &&
366
+ needs.params.result == 'success'
367
+ runs-on: ubuntu-latest
368
+ steps:
369
+ - uses: actions/checkout@v4
370
+ with:
371
+ fetch-depth: 0
372
+
373
+ - uses: actions/setup-node@v4
374
+ with:
375
+ node-version: "24"
376
+
377
+ - name: Install project dependencies
378
+ run: npm ci
379
+
380
+ - name: Install agentic-step dependencies
381
+ working-directory: .github/agentic-lib/actions/agentic-step
382
+ run: npm ci
383
+
384
+ - name: Find and fix stuck PRs
385
+ uses: actions/github-script@v7
386
+ env:
387
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
388
+ with:
389
+ script: |
390
+ const owner = context.repo.owner;
391
+ const repo = context.repo.repo;
392
+ const maxFixAttempts = parseInt(process.env.maxFixAttempts) || 3;
393
+ const prNumber = '${{ needs.params.outputs.pr-number }}';
394
+
395
+ // If specific PR requested, use that
396
+ if (prNumber) {
397
+ core.info(`Specific PR requested: #${prNumber}`);
398
+ core.exportVariable('FIX_PR_NUMBER', prNumber);
399
+ return;
400
+ }
401
+
402
+ // Find PRs with failing checks on agentic branches
403
+ const { data: openPRs } = await github.rest.pulls.list({
404
+ owner, repo, state: 'open', per_page: 10,
405
+ });
406
+
407
+ for (const pr of openPRs) {
408
+ const hasAutomerge = pr.labels.some(l => l.name === 'automerge');
409
+ if (!hasAutomerge) continue;
410
+ if (!pr.head.ref.startsWith('agentic-lib-') && !pr.head.ref.startsWith('copilot/')) continue;
411
+
412
+ // Check if checks are failing
413
+ const { data: checkRuns } = await github.rest.checks.listForRef({
414
+ owner, repo, ref: pr.head.sha,
415
+ });
416
+ const hasFailing = checkRuns.check_runs.some(c => c.conclusion === 'failure');
417
+ if (!hasFailing) continue;
418
+
419
+ // Check fix attempt count
420
+ const { data: fixRuns } = await github.rest.actions.listWorkflowRuns({
421
+ owner, repo, workflow_id: 'agentic-lib-workflow.yml',
422
+ branch: pr.head.ref, per_page: maxFixAttempts + 1,
423
+ });
424
+ if (fixRuns.total_count >= maxFixAttempts) {
425
+ core.info(`PR #${pr.number} exceeded fix attempts. Removing automerge.`);
426
+ try { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: 'automerge' }); } catch (e) {}
427
+ continue;
428
+ }
429
+
430
+ core.info(`Will attempt to fix PR #${pr.number}`);
431
+ core.exportVariable('FIX_PR_NUMBER', String(pr.number));
432
+ break;
433
+ }
434
+
435
+ - name: Checkout PR branch and fix
436
+ if: env.FIX_PR_NUMBER != ''
437
+ run: |
438
+ gh pr checkout ${{ env.FIX_PR_NUMBER }}
439
+ env:
440
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
441
+
442
+ - name: Fix failing code
443
+ if: env.FIX_PR_NUMBER != ''
444
+ uses: ./.github/agentic-lib/actions/agentic-step
445
+ env:
446
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
447
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
448
+ with:
449
+ task: "fix-code"
450
+ config: ${{ env.configPath }}
451
+ instructions: ".github/agentic-lib/agents/agent-apply-fix.md"
452
+ pr-number: ${{ env.FIX_PR_NUMBER }}
453
+ test-command: "npm test"
454
+ model: ${{ needs.params.outputs.model }}
455
+
456
+ - name: Commit and push fixes
457
+ if: env.FIX_PR_NUMBER != ''
458
+ uses: ./.github/agentic-lib/actions/commit-if-changed
459
+ with:
460
+ commit-message: "agentic-step: fix failing tests"
461
+
462
+ # ─── Review: close resolved issues, enhance with criteria ──────────
463
+ review-features:
464
+ needs: [params, maintain]
465
+ if: |
466
+ always() &&
467
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'review-only') &&
468
+ needs.params.result == 'success'
469
+ runs-on: ubuntu-latest
470
+ steps:
471
+ - uses: actions/checkout@v4
472
+
473
+ - uses: actions/setup-node@v4
474
+ with:
475
+ node-version: "24"
476
+
477
+ - name: Install agentic-step dependencies
478
+ working-directory: .github/agentic-lib/actions/agentic-step
479
+ run: npm ci
480
+
481
+ - name: Review issues
482
+ uses: ./.github/agentic-lib/actions/agentic-step
483
+ env:
484
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
485
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
486
+ with:
487
+ task: "review-issue"
488
+ config: ${{ env.configPath }}
489
+ instructions: ".github/agentic-lib/agents/agent-review-issue.md"
490
+ model: ${{ needs.params.outputs.model }}
491
+
492
+ - name: Enhance issues
493
+ uses: ./.github/agentic-lib/actions/agentic-step
494
+ env:
495
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
496
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
497
+ with:
498
+ task: "enhance-issue"
499
+ config: ${{ env.configPath }}
500
+ instructions: ".github/agentic-lib/agents/agent-ready-issue.md"
501
+ model: ${{ needs.params.outputs.model }}
502
+
503
+ # ─── Dev: sequential issue resolution ──────────────────────────────
504
+ dev:
505
+ needs: [params, review-features]
506
+ if: |
507
+ always() &&
508
+ (needs.params.outputs.mode == 'full' || needs.params.outputs.mode == 'dev-only') &&
509
+ needs.params.result == 'success'
510
+ runs-on: ubuntu-latest
511
+ steps:
512
+ - uses: actions/checkout@v4
513
+ with:
514
+ fetch-depth: 0
515
+ token: ${{ secrets.GITHUB_TOKEN }}
516
+
517
+ - uses: actions/setup-node@v4
518
+ with:
519
+ node-version: "24"
520
+
521
+ - name: Install project dependencies
522
+ run: npm ci
523
+
524
+ - name: Install agentic-step dependencies
525
+ working-directory: .github/agentic-lib/actions/agentic-step
526
+ run: npm ci
527
+
528
+ - name: Load config
529
+ id: config
530
+ run: |
531
+ CONFIG="${{ env.configPath }}"
532
+ SOURCE=$(yq -r '.paths.targetSourcePath.path // "src/lib/"' "$CONFIG")
533
+ TESTS=$(yq -r '.paths.targetTestsPath.path // "tests/unit/"' "$CONFIG")
534
+ FEATURES=$(yq -r '.paths.featuresPath.path // "features/"' "$CONFIG")
535
+ DOCS=$(yq -r '.paths.documentationPath.path // "docs/"' "$CONFIG")
536
+ LIBRARY=$(yq -r '.paths.libraryDocumentsPath.path // "library/"' "$CONFIG")
537
+ SOURCES=$(yq -r '.paths.librarySourcesFilepath.path // "SOURCES.md"' "$CONFIG")
538
+ README=$(yq -r '.paths.readmeFilepath.path // "README.md"' "$CONFIG")
539
+ DEPS=$(yq -r '.paths.dependenciesFilepath.path // "package.json"' "$CONFIG")
540
+ echo "writablePaths=${SOURCE};${TESTS};${FEATURES};${DOCS};${LIBRARY};${SOURCES};${README};${DEPS}" >> $GITHUB_OUTPUT
541
+
542
+ - name: Find target issue
543
+ id: issue
544
+ uses: actions/github-script@v7
545
+ with:
546
+ script: |
547
+ const specificIssue = '${{ needs.params.outputs.issue-number }}';
548
+ if (specificIssue) {
549
+ core.setOutput('issue-number', specificIssue);
550
+ return;
551
+ }
552
+ // Find oldest open issue with 'ready' label
553
+ const { data: issues } = await github.rest.issues.listForRepo({
554
+ ...context.repo, state: 'open', labels: 'ready',
555
+ sort: 'created', direction: 'asc', per_page: 1,
556
+ });
557
+ if (issues.length > 0) {
558
+ core.setOutput('issue-number', String(issues[0].number));
559
+ core.info(`Targeting issue #${issues[0].number}: ${issues[0].title}`);
560
+ } else {
561
+ core.setOutput('issue-number', '');
562
+ core.info('No ready issues found');
563
+ }
564
+
565
+ - name: Create branch
566
+ if: steps.issue.outputs.issue-number != ''
567
+ id: branch
568
+ run: |
569
+ ISSUE_NUMBER="${{ steps.issue.outputs.issue-number }}"
570
+ BRANCH="agentic-lib-issue-${ISSUE_NUMBER}"
571
+ git checkout -b "${BRANCH}" 2>/dev/null || git checkout "${BRANCH}"
572
+ echo "branchName=${BRANCH}" >> $GITHUB_OUTPUT
573
+
574
+ - name: Run transformation
575
+ if: steps.issue.outputs.issue-number != ''
576
+ uses: ./.github/agentic-lib/actions/agentic-step
577
+ env:
578
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
579
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
580
+ with:
581
+ task: "transform"
582
+ config: ${{ env.configPath }}
583
+ instructions: ".github/agentic-lib/agents/agent-issue-resolution.md"
584
+ test-command: "npm test"
585
+ model: ${{ needs.params.outputs.model }}
586
+ issue-number: ${{ steps.issue.outputs.issue-number }}
587
+ writable-paths: ${{ steps.config.outputs.writablePaths }}
588
+
589
+ - name: Commit and push
590
+ if: steps.issue.outputs.issue-number != ''
591
+ uses: ./.github/agentic-lib/actions/commit-if-changed
592
+ with:
593
+ commit-message: "agentic-step: transform issue #${{ steps.issue.outputs.issue-number }}"
594
+ push-ref: ${{ steps.branch.outputs.branchName }}
595
+
596
+ - name: Create PR and attempt merge
597
+ if: steps.issue.outputs.issue-number != ''
598
+ uses: actions/github-script@v7
599
+ with:
600
+ script: |
601
+ const owner = context.repo.owner;
602
+ const repo = context.repo.repo;
603
+ const branchName = '${{ steps.branch.outputs.branchName }}';
604
+ const issueNumber = '${{ steps.issue.outputs.issue-number }}';
605
+
606
+ if (!branchName) return;
607
+
608
+ // Check if branch has commits ahead of main
609
+ try {
610
+ const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
611
+ owner, repo, basehead: `main...${branchName}`,
612
+ });
613
+ if (comparison.ahead_by === 0) {
614
+ core.info('Branch has no new commits — skipping');
615
+ return;
616
+ }
617
+ } catch (err) {
618
+ core.info(`Branch comparison failed: ${err.message}`);
619
+ return;
620
+ }
621
+
622
+ // Check if PR already exists
623
+ const { data: existingPRs } = await github.rest.pulls.list({
624
+ owner, repo, state: 'open',
625
+ head: `${owner}:${branchName}`, per_page: 1,
626
+ });
627
+
628
+ let prNumber;
629
+ if (existingPRs.length > 0) {
630
+ prNumber = existingPRs[0].number;
631
+ core.info(`PR already exists: #${prNumber}`);
632
+ } else {
633
+ const { data: pr } = await github.rest.pulls.create({
634
+ owner, repo,
635
+ title: `fix: resolve issue #${issueNumber}`,
636
+ body: `Closes #${issueNumber}\n\nAutomated transformation.`,
637
+ head: branchName, base: 'main',
638
+ });
639
+ prNumber = pr.number;
640
+ core.info(`Created PR #${prNumber}`);
641
+ await github.rest.issues.addLabels({
642
+ owner, repo, issue_number: prNumber, labels: ['automerge'],
643
+ });
644
+ }
645
+
646
+ // Wait briefly for checks to register
647
+ await new Promise(r => setTimeout(r, 10000));
648
+
649
+ // Attempt merge
650
+ for (let attempt = 0; attempt < 2; attempt++) {
651
+ const { data: fullPr } = await github.rest.pulls.get({
652
+ owner, repo, pull_number: prNumber,
653
+ });
654
+
655
+ if (fullPr.mergeable && fullPr.mergeable_state === 'clean') {
656
+ try {
657
+ await github.rest.pulls.merge({
658
+ owner, repo, pull_number: prNumber, merge_method: 'squash',
659
+ });
660
+ core.info(`Merged PR #${prNumber}`);
661
+ try {
662
+ await github.rest.git.deleteRef({ owner, repo, ref: `heads/${branchName}` });
663
+ } catch (e) {}
664
+ if (issueNumber) {
665
+ try {
666
+ await github.rest.issues.addLabels({ owner, repo, issue_number: parseInt(issueNumber), labels: ['merged'] });
667
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: parseInt(issueNumber), name: 'in-progress' });
668
+ } catch (e) {}
669
+ }
670
+ return;
671
+ } catch (e) {
672
+ core.info(`Merge attempt ${attempt + 1} failed: ${e.message}`);
673
+ }
674
+ } else if (fullPr.mergeable_state === 'unstable' || fullPr.mergeable === null) {
675
+ core.info(`PR not ready yet (${fullPr.mergeable_state}), waiting...`);
676
+ await new Promise(r => setTimeout(r, 15000));
677
+ } else {
678
+ core.info(`PR not mergeable: ${fullPr.mergeable_state}`);
679
+ break;
680
+ }
681
+ }
682
+ core.info(`PR #${prNumber} left open for next run`);
683
+
684
+ # ─── Post-merge: stats, schedule, mission check ────────────────────
685
+ post-merge:
686
+ needs: [params, dev, pr-cleanup]
687
+ if: always() && needs.params.result == 'success'
688
+ runs-on: ubuntu-latest
689
+ steps:
690
+ - uses: actions/checkout@v4
691
+ with:
692
+ fetch-depth: 0
693
+
694
+ - name: Summary
695
+ run: |
696
+ echo "## agentic-lib-workflow run summary" >> $GITHUB_STEP_SUMMARY
697
+ echo "- Mode: ${{ needs.params.outputs.mode }}" >> $GITHUB_STEP_SUMMARY
698
+ echo "- Model: ${{ needs.params.outputs.model }}" >> $GITHUB_STEP_SUMMARY
699
+
700
+ # ─── Schedule change (if requested) ────────────────────────────────
701
+ update-schedule:
702
+ needs: [params, dev]
703
+ if: always() && needs.params.outputs.schedule != '' && needs.params.result == 'success'
704
+ uses: ./.github/workflows/agentic-lib-schedule.yml
705
+ with:
706
+ frequency: ${{ needs.params.outputs.schedule }}
707
+ model: ${{ needs.params.outputs.model }}
708
+ secrets: inherit