@ikon85/agent-workflow-kit 0.12.0 → 0.13.0

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.
@@ -24,6 +24,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
24
24
  | `docs/agents/code-review.md` | Standards-source pointers + adjacent-review-tooling notes the `code-review` skill's Standards axis reads (Section I) |
25
25
  | `## Workflow` in CLAUDE.md **and** AGENTS.md | generic entry-point map seeded from [workflow-overview.md](./workflow-overview.md) (Section F + Write) |
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
+ | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
27
28
 
28
29
  ## Idempotency contract — read before writing anything
29
30
 
@@ -68,6 +69,40 @@ Default posture: these skills were designed for GitHub. If a `git remote` points
68
69
 
69
70
  Seed `docs/agents/issue-tracker.md` from the matching template in this folder: [issue-tracker-github.md](./issue-tracker-github.md), [issue-tracker-gitlab.md](./issue-tracker-gitlab.md), [issue-tracker-local.md](./issue-tracker-local.md). For "other", write it from the user's description.
70
71
 
72
+ ### 2a. Automatic Kit update pull requests (GitHub tracker only)
73
+
74
+ > A short scheduled check can keep the installed Kit current by opening one normal pull request after the candidate passes the consumer's own tests. It never merges the pull request for you.
75
+
76
+ Only after the user has confirmed a **GitHub tracker**, ask in plain language: *"Should GitHub check weekly for a tested Agent Workflow Kit update and keep one update pull request ready?"* Offer these explicit choices:
77
+
78
+ Before offering **Enable**, confirm the consumer has a committed `package-lock.json` and a usable `package.json` `npm test` command. The shipped workflow installs the locked dependencies with `npm ci --ignore-scripts`; without that lockfile/test contract it cannot prove the candidate in a clean checkout, so explain the prerequisite and do not create the workflow yet.
79
+
80
+ Also read the repository Actions policy before enabling:
81
+
82
+ ```bash
83
+ gh api repos/<owner>/<repo>/actions/permissions/workflow
84
+ ```
85
+
86
+ The response must report `"can_approve_pull_request_reviews": true`; this is GitHub's **Allow GitHub Actions to create and approve pull requests** gate used by the stable PR upsert. If it is false, or the policy cannot be read, do not seed the workflow yet. Guide the user to **Settings → Actions → General → Workflow permissions**, enable that checkbox, and rerun `setup-workflow`.
87
+
88
+ If the user prefers `gh`, first show the exact mutation and obtain **explicit confirmation**; never change repository settings merely because setup was invoked. After confirmation, preserve the currently reported `default_workflow_permissions` value (`read` or `write`) and set the gate with:
89
+
90
+ ```bash
91
+ gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow \
92
+ -f default_workflow_permissions=<current-read-or-write> \
93
+ -F can_approve_pull_request_reviews=true
94
+ ```
95
+
96
+ Read the policy back and offer **Enable** only after the field is actually true.
97
+
98
+ - **Enable** — seed `.github/workflows/agent-workflow-kit-update.yml` from [assets/agent-workflow-kit-update.yml](./assets/agent-workflow-kit-update.yml).
99
+ - **Opt out** — do not create a GitHub workflow, branch, or pull request.
100
+ - **Ask later** — do not create a GitHub workflow, branch, or pull request; explain that re-running `setup-workflow` can offer it again.
101
+
102
+ This workflow opt-in has its own file-level idempotency rule: if the destination is already present, leave it byte-for-byte unchanged and report `skipped (already present)`. On repeated setup runs, do not prompt again when it exists. The workflow consumes the scoped `@ikon85/agent-workflow-kit` release and its existing parity-checked transactional update command; do not reproduce either mechanism in setup prose or shell steps.
103
+
104
+ For GitLab, local, other, or an unknown provider, give only a provider-neutral explanation that automatic updates require provider-specific CI and pull-request support; **do not create a GitHub workflow**. Setup itself never creates the update branch or pull request — only an opted-in workflow run may do that later.
105
+
71
106
  **Conditional board-write note (only when the GitHub tracker uses a managed board):** if Section D ends with `board-sync.md` at `mode: github-projects-v2`, add one line to `issue-tracker.md`: *"Board writes (item-add, status/wave/cluster field edits, sub-issue links) go through the board-sync helper, not bare `gh issue create`/`gh project item-*`."* Do **not** add this for GitLab, local, other, or `mode: none`.
72
107
 
73
108
  ### 3. Section B — Triage labels
@@ -0,0 +1,31 @@
1
+ name: Agent Workflow Kit update
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '17 6 * * 1'
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: agent-workflow-kit-update
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ update:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 22.14
26
+ - name: Install locked consumer dependencies
27
+ run: npm ci --ignore-scripts
28
+ - name: Verify, test, and upsert the Kit update pull request
29
+ env:
30
+ GH_TOKEN: ${{ github.token }}
31
+ run: npx --yes --package=@ikon85/agent-workflow-kit@latest agent-workflow-kit-update-pr
@@ -24,6 +24,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
24
24
  | `docs/agents/code-review.md` | Standards-source pointers + adjacent-review-tooling notes the `code-review` skill's Standards axis reads (Section I) |
25
25
  | `## Workflow` in CLAUDE.md **and** AGENTS.md | generic entry-point map seeded from [workflow-overview.md](./workflow-overview.md) (Section F + Write) |
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
+ | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
27
28
 
28
29
  ## Idempotency contract — read before writing anything
29
30
 
@@ -68,6 +69,40 @@ Default posture: these skills were designed for GitHub. If a `git remote` points
68
69
 
69
70
  Seed `docs/agents/issue-tracker.md` from the matching template in this folder: [issue-tracker-github.md](./issue-tracker-github.md), [issue-tracker-gitlab.md](./issue-tracker-gitlab.md), [issue-tracker-local.md](./issue-tracker-local.md). For "other", write it from the user's description.
70
71
 
72
+ ### 2a. Automatic Kit update pull requests (GitHub tracker only)
73
+
74
+ > A short scheduled check can keep the installed Kit current by opening one normal pull request after the candidate passes the consumer's own tests. It never merges the pull request for you.
75
+
76
+ Only after the user has confirmed a **GitHub tracker**, ask in plain language: *"Should GitHub check weekly for a tested Agent Workflow Kit update and keep one update pull request ready?"* Offer these explicit choices:
77
+
78
+ Before offering **Enable**, confirm the consumer has a committed `package-lock.json` and a usable `package.json` `npm test` command. The shipped workflow installs the locked dependencies with `npm ci --ignore-scripts`; without that lockfile/test contract it cannot prove the candidate in a clean checkout, so explain the prerequisite and do not create the workflow yet.
79
+
80
+ Also read the repository Actions policy before enabling:
81
+
82
+ ```bash
83
+ gh api repos/<owner>/<repo>/actions/permissions/workflow
84
+ ```
85
+
86
+ The response must report `"can_approve_pull_request_reviews": true`; this is GitHub's **Allow GitHub Actions to create and approve pull requests** gate used by the stable PR upsert. If it is false, or the policy cannot be read, do not seed the workflow yet. Guide the user to **Settings → Actions → General → Workflow permissions**, enable that checkbox, and rerun `setup-workflow`.
87
+
88
+ If the user prefers `gh`, first show the exact mutation and obtain **explicit confirmation**; never change repository settings merely because setup was invoked. After confirmation, preserve the currently reported `default_workflow_permissions` value (`read` or `write`) and set the gate with:
89
+
90
+ ```bash
91
+ gh api --method PUT repos/<owner>/<repo>/actions/permissions/workflow \
92
+ -f default_workflow_permissions=<current-read-or-write> \
93
+ -F can_approve_pull_request_reviews=true
94
+ ```
95
+
96
+ Read the policy back and offer **Enable** only after the field is actually true.
97
+
98
+ - **Enable** — seed `.github/workflows/agent-workflow-kit-update.yml` from [assets/agent-workflow-kit-update.yml](./assets/agent-workflow-kit-update.yml).
99
+ - **Opt out** — do not create a GitHub workflow, branch, or pull request.
100
+ - **Ask later** — do not create a GitHub workflow, branch, or pull request; explain that re-running `setup-workflow` can offer it again.
101
+
102
+ This workflow opt-in has its own file-level idempotency rule: if the destination is already present, leave it byte-for-byte unchanged and report `skipped (already present)`. On repeated setup runs, do not prompt again when it exists. The workflow consumes the scoped `@ikon85/agent-workflow-kit` release and its existing parity-checked transactional update command; do not reproduce either mechanism in setup prose or shell steps.
103
+
104
+ For GitLab, local, other, or an unknown provider, give only a provider-neutral explanation that automatic updates require provider-specific CI and pull-request support; **do not create a GitHub workflow**. Setup itself never creates the update branch or pull request — only an opted-in workflow run may do that later.
105
+
71
106
  **Conditional board-write note (only when the GitHub tracker uses a managed board):** if Section D ends with `board-sync.md` at `mode: github-projects-v2`, add one line to `issue-tracker.md`: *"Board writes (item-add, status/wave/cluster field edits, sub-issue links) go through the board-sync helper, not bare `gh issue create`/`gh project item-*`."* Do **not** add this for GitLab, local, other, or `mode: none`.
72
107
 
73
108
  ### 3. Section B — Triage labels
@@ -0,0 +1,31 @@
1
+ name: Agent Workflow Kit update
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '17 6 * * 1'
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: agent-workflow-kit-update
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ update:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 22.14
26
+ - name: Install locked consumer dependencies
27
+ run: npm ci --ignore-scripts
28
+ - name: Verify, test, and upsert the Kit update pull request
29
+ env:
30
+ GH_TOKEN: ${{ github.token }}
31
+ run: npx --yes --package=@ikon85/agent-workflow-kit@latest agent-workflow-kit-update-pr
package/README.md CHANGED
@@ -332,6 +332,14 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.13.0
336
+
337
+ - added: `.agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml`
338
+ - added: `.claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml`
339
+ - added: `scripts/kit-update-pr.mjs`
340
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
341
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
342
+
335
343
  ### 0.12.0
336
344
 
337
345
  - added: `.agents/skills/kit-update/SKILL.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.12.0",
2
+ "kitVersion": "0.13.0",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -460,6 +460,15 @@
460
460
  "mode": 420,
461
461
  "origin": "kit"
462
462
  },
463
+ {
464
+ "path": ".agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml",
465
+ "kind": "skill",
466
+ "ownerSkill": "setup-workflow",
467
+ "surface": "codex",
468
+ "sha256": "d8abf995d67184b4459b9565c9062319fc42e8a685042b288b3066a07a6fda59",
469
+ "mode": 420,
470
+ "origin": "kit"
471
+ },
463
472
  {
464
473
  "path": ".agents/skills/setup-workflow/board-sync.md",
465
474
  "kind": "skill",
@@ -528,7 +537,7 @@
528
537
  "kind": "skill",
529
538
  "ownerSkill": "setup-workflow",
530
539
  "surface": "codex",
531
- "sha256": "ea0b75ccd1579820a09e7c8d4c151126aa0be7a0be6b76b23599fadebb44b554",
540
+ "sha256": "efc7818d98158a85a79f3a652787d2e557552fa5175b32b24916c894388b9b74",
532
541
  "mode": 420,
533
542
  "origin": "kit"
534
543
  },
@@ -1433,6 +1442,15 @@
1433
1442
  "mode": 420,
1434
1443
  "origin": "kit"
1435
1444
  },
1445
+ {
1446
+ "path": ".claude/skills/setup-workflow/assets/agent-workflow-kit-update.yml",
1447
+ "kind": "skill",
1448
+ "ownerSkill": "setup-workflow",
1449
+ "surface": "claude",
1450
+ "sha256": "d8abf995d67184b4459b9565c9062319fc42e8a685042b288b3066a07a6fda59",
1451
+ "mode": 420,
1452
+ "origin": "kit"
1453
+ },
1436
1454
  {
1437
1455
  "path": ".claude/skills/setup-workflow/board-sync.md",
1438
1456
  "kind": "skill",
@@ -1501,7 +1519,7 @@
1501
1519
  "kind": "skill",
1502
1520
  "ownerSkill": "setup-workflow",
1503
1521
  "surface": "claude",
1504
- "sha256": "ea0b75ccd1579820a09e7c8d4c151126aa0be7a0be6b76b23599fadebb44b554",
1522
+ "sha256": "efc7818d98158a85a79f3a652787d2e557552fa5175b32b24916c894388b9b74",
1505
1523
  "mode": 420,
1506
1524
  "origin": "kit"
1507
1525
  },
@@ -1883,6 +1901,13 @@
1883
1901
  "mode": 420,
1884
1902
  "origin": "kit"
1885
1903
  },
1904
+ {
1905
+ "path": "scripts/kit-update-pr.mjs",
1906
+ "kind": "script",
1907
+ "sha256": "64e7818003d7c6db227eadad0b114e199399a52a96965ab2f0f496c0a1e5c79c",
1908
+ "mode": 493,
1909
+ "origin": "kit"
1910
+ },
1886
1911
  {
1887
1912
  "path": "scripts/loc_offender_core.py",
1888
1913
  "kind": "script",
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "agent-workflow-kit": "src/cli.mjs"
7
+ "agent-workflow-kit": "src/cli.mjs",
8
+ "agent-workflow-kit-update-pr": "scripts/kit-update-pr.mjs"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "npm run test:node && npm run test:python",
@@ -37,6 +37,9 @@ test('current build contains post-tag public files and repository metadata', asy
37
37
  const pkg = JSON.parse(await readFile(join(dist, 'package.json'), 'utf8'));
38
38
  assert.equal(pkg.name, '@ikon85/agent-workflow-kit');
39
39
  assert.equal(pkg.repository.url, 'git+https://github.com/iKon85/agent-workflow-kit.git');
40
+ assert.equal(pkg.bin['agent-workflow-kit'], 'src/cli.mjs');
41
+ assert.equal(pkg.bin['agent-workflow-kit-update-pr'], 'scripts/kit-update-pr.mjs');
42
+ await readFile(join(dist, 'scripts/kit-update-pr.mjs'));
40
43
  });
41
44
  });
42
45
 
@@ -52,9 +55,31 @@ test('npm pack keeps the complete scripts tree but excludes Python caches', () =
52
55
  const files = JSON.parse(output)[0].files.map((file) => file.path);
53
56
  assert.ok(files.includes('scripts/build-kit.mjs'));
54
57
  assert.ok(files.includes('scripts/board-sync.py'));
58
+ assert.ok(files.includes('scripts/kit-update-pr.mjs'));
55
59
  assert.ok(files.every((path) => !path.includes('__pycache__') && !path.endsWith('.pyc')));
56
60
  const pkg = JSON.parse(execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
57
61
  cwd: REPO, encoding: 'utf8',
58
62
  }));
59
63
  assert.ok(pkg.files.includes('scripts/'), 'package must ship future script file types');
60
64
  });
65
+
66
+ test('packed scoped artifact keeps the existing npx default-bin inference', async () => {
67
+ const destination = await mkdtemp(join(tmpdir(), 'awkit-pack-'));
68
+ try {
69
+ const packed = JSON.parse(execFileSync('npm', [
70
+ 'pack', '--pack-destination', destination, '--json',
71
+ ], { cwd: REPO, encoding: 'utf8' }));
72
+ const output = execFileSync('npx', [
73
+ '--yes', '--offline', `./${packed[0].filename}`,
74
+ ], { cwd: destination, encoding: 'utf8' });
75
+ assert.match(output, /Usage: agent-workflow-kit/);
76
+
77
+ const updater = execFileSync('npm', [
78
+ 'exec', '--yes', '--offline', `--package=./${packed[0].filename}`,
79
+ '--', 'agent-workflow-kit-update-pr', 'help',
80
+ ], { cwd: destination, encoding: 'utf8' });
81
+ assert.match(updater, /Usage: agent-workflow-kit-update-pr/);
82
+ } finally {
83
+ await rm(destination, { recursive: true, force: true });
84
+ }
85
+ });
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from 'node:child_process';
3
+ import { realpathSync } from 'node:fs';
4
+ import { appendFile } from 'node:fs/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+
9
+ export const UPDATE_BRANCH = 'agent-workflow-kit/update';
10
+ export const UPDATE_TITLE = 'chore: update agent workflow kit';
11
+ const UPDATE_BODY = [
12
+ 'Automated, parity-verified update of `@ikon85/agent-workflow-kit`.',
13
+ '',
14
+ 'The transactional update candidate passed the consumer test suite before this branch was published.',
15
+ '',
16
+ 'This pull request is never merged automatically.',
17
+ ].join('\n');
18
+ const exec = promisify(execFile);
19
+
20
+ export async function orchestrateUpdatePullRequest(options) {
21
+ const {
22
+ runUpdate, hasChanges, listPullRequests, publishBranch,
23
+ createPullRequest, updatePullRequest, branch = UPDATE_BRANCH,
24
+ } = options;
25
+ const update = await runUpdate();
26
+ if (update.exitCode !== 0) {
27
+ return { status: update.exitCode === 2 ? 'conflicted' : 'failed', branch, update };
28
+ }
29
+ if (!await hasChanges()) return { status: 'current', branch };
30
+
31
+ const pulls = await listPullRequests(branch);
32
+ if (pulls.length > 1) {
33
+ return { status: 'failed', branch, reason: 'multiple-open-pull-requests' };
34
+ }
35
+ await publishBranch(branch);
36
+ const input = { title: UPDATE_TITLE, body: UPDATE_BODY, branch };
37
+ if (pulls.length === 1) {
38
+ await updatePullRequest(pulls[0].number, input);
39
+ return { status: 'updated', branch, pullRequest: pulls[0].number };
40
+ }
41
+ await createPullRequest(input);
42
+ return { status: 'created', branch };
43
+ }
44
+
45
+ export function createSystemAdapters({
46
+ cwd = process.cwd(), env = process.env, execute = exec,
47
+ } = {}) {
48
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
49
+ const command = async (file, args) => {
50
+ try {
51
+ const result = await execute(file, args, { cwd, env, encoding: 'utf8' });
52
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
53
+ } catch (error) {
54
+ if (typeof error.code !== 'number') throw error;
55
+ return { exitCode: error.code, stdout: error.stdout || '', stderr: error.stderr || '' };
56
+ }
57
+ };
58
+ const checked = async (file, args) => {
59
+ const result = await command(file, args);
60
+ if (result.exitCode !== 0) throw new Error(result.stderr || `${file} exited ${result.exitCode}`);
61
+ return result.stdout.trim();
62
+ };
63
+
64
+ return {
65
+ runUpdate: () => command(process.execPath, [join(packageRoot, 'src/cli.mjs'), 'update', '--yes']),
66
+ hasChanges: async () => Boolean(await checked('git', ['status', '--porcelain', '--untracked-files=all'])),
67
+ listPullRequests: async (branch) => JSON.parse(await checked('gh', [
68
+ 'pr', 'list', '--state', 'open', '--head', branch, '--json', 'number,url',
69
+ ]) || '[]'),
70
+ publishBranch: async (branch) => {
71
+ await checked('git', ['config', 'user.name', 'agent-workflow-kit[bot]']);
72
+ await checked('git', ['config', 'user.email', 'agent-workflow-kit[bot]@users.noreply.github.com']);
73
+ await checked('git', ['add', '--all']);
74
+ await checked('git', ['commit', '-m', UPDATE_TITLE]);
75
+ const remote = await checked('git', ['ls-remote', 'origin', `refs/heads/${branch}`]);
76
+ const expected = remote.split(/\s+/)[0] || '';
77
+ await checked('git', [
78
+ 'push', `--force-with-lease=refs/heads/${branch}:${expected}`,
79
+ 'origin', `HEAD:refs/heads/${branch}`,
80
+ ]);
81
+ },
82
+ createPullRequest: ({ title, body, branch }) => checked('gh', [
83
+ 'pr', 'create', '--head', branch, '--title', title, '--body', body,
84
+ ]),
85
+ updatePullRequest: (number, { title, body }) => checked('gh', [
86
+ 'pr', 'edit', String(number), '--title', title, '--body', body,
87
+ ]),
88
+ };
89
+ }
90
+
91
+ export async function runCli(options = {}) {
92
+ let report;
93
+ try {
94
+ report = await orchestrateUpdatePullRequest(createSystemAdapters(options));
95
+ } catch (error) {
96
+ report = { status: 'failed', branch: UPDATE_BRANCH, reason: error.message };
97
+ }
98
+ const rendered = `${JSON.stringify(report, null, 2)}\n`;
99
+ process.stdout.write(rendered);
100
+ if (process.env.GITHUB_STEP_SUMMARY) {
101
+ await appendFile(process.env.GITHUB_STEP_SUMMARY, `## Agent Workflow Kit update\n\n\`\`\`json\n${rendered}\`\`\`\n`);
102
+ }
103
+ if (report.status === 'conflicted') process.exitCode = 2;
104
+ else if (report.status === 'failed') process.exitCode = 1;
105
+ return report;
106
+ }
107
+
108
+ const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : '';
109
+ if (invokedPath === fileURLToPath(import.meta.url)) {
110
+ if (process.argv.includes('--help') || process.argv.includes('-h') || process.argv.includes('help')) {
111
+ process.stdout.write('Usage: agent-workflow-kit-update-pr\n');
112
+ } else {
113
+ await runCli();
114
+ }
115
+ }
@@ -0,0 +1,99 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createSystemAdapters, orchestrateUpdatePullRequest } from './kit-update-pr.mjs';
4
+
5
+ function harness({ update = { exitCode: 0 }, changed = true, pulls = [] } = {}) {
6
+ const calls = [];
7
+ return {
8
+ calls,
9
+ options: {
10
+ runUpdate: async () => (calls.push('update'), update),
11
+ hasChanges: async () => (calls.push('changes'), changed),
12
+ listPullRequests: async (branch) => (calls.push(['list', branch]), pulls),
13
+ publishBranch: async (branch) => calls.push(['publish', branch]),
14
+ createPullRequest: async (input) => calls.push(['create', input]),
15
+ updatePullRequest: async (number, input) => calls.push(['upsert', number, input]),
16
+ },
17
+ };
18
+ }
19
+
20
+ test('a successful update creates one stable update pull request', async () => {
21
+ const h = harness();
22
+ const report = await orchestrateUpdatePullRequest(h.options);
23
+
24
+ assert.equal(report.status, 'created');
25
+ assert.equal(report.branch, 'agent-workflow-kit/update');
26
+ assert.deepEqual(h.calls.map((call) => Array.isArray(call) ? call[0] : call), [
27
+ 'update', 'changes', 'list', 'publish', 'create',
28
+ ]);
29
+ });
30
+
31
+ test('a conflict produces a structured report without touching the stable branch', async () => {
32
+ const h = harness({ update: { exitCode: 2, stdout: 'conflicts: 1', stderr: '' } });
33
+ const report = await orchestrateUpdatePullRequest(h.options);
34
+
35
+ assert.deepEqual(report, {
36
+ status: 'conflicted',
37
+ branch: 'agent-workflow-kit/update',
38
+ update: { exitCode: 2, stdout: 'conflicts: 1', stderr: '' },
39
+ });
40
+ assert.deepEqual(h.calls, ['update']);
41
+ });
42
+
43
+ test('a release mismatch fails without touching the consumer update branch', async () => {
44
+ const h = harness({ update: { exitCode: 1, stdout: '', stderr: 'release mismatch' } });
45
+ const report = await orchestrateUpdatePullRequest(h.options);
46
+
47
+ assert.equal(report.status, 'failed');
48
+ assert.equal(report.update.stderr, 'release mismatch');
49
+ assert.deepEqual(h.calls, ['update']);
50
+ });
51
+
52
+ test('ambiguous existing pull requests fail before the last good branch is changed', async () => {
53
+ const h = harness({ pulls: [{ number: 7 }, { number: 9 }] });
54
+ const report = await orchestrateUpdatePullRequest(h.options);
55
+
56
+ assert.equal(report.status, 'failed');
57
+ assert.equal(report.reason, 'multiple-open-pull-requests');
58
+ assert.deepEqual(h.calls.map((call) => Array.isArray(call) ? call[0] : call), [
59
+ 'update', 'changes', 'list',
60
+ ]);
61
+ });
62
+
63
+ test('an already current consumer is a no-op', async () => {
64
+ const h = harness({ changed: false });
65
+ const report = await orchestrateUpdatePullRequest(h.options);
66
+
67
+ assert.equal(report.status, 'current');
68
+ assert.deepEqual(h.calls, ['update', 'changes']);
69
+ });
70
+
71
+ test('a repeated run updates the one existing stable pull request', async () => {
72
+ const h = harness({ pulls: [{ number: 7 }] });
73
+ const report = await orchestrateUpdatePullRequest(h.options);
74
+
75
+ assert.equal(report.status, 'updated');
76
+ assert.equal(report.pullRequest, 7);
77
+ assert.deepEqual(h.calls.map((call) => Array.isArray(call) ? call[0] : call), [
78
+ 'update', 'changes', 'list', 'publish', 'upsert',
79
+ ]);
80
+ });
81
+
82
+ test('publishing commits in place and lease-protects the stable remote branch', async () => {
83
+ const commands = [];
84
+ const execute = async (file, args) => {
85
+ commands.push([file, ...args]);
86
+ if (args[0] === 'ls-remote') return { stdout: 'abc123\trefs/heads/agent-workflow-kit/update\n', stderr: '' };
87
+ return { stdout: '', stderr: '' };
88
+ };
89
+ const adapter = createSystemAdapters({ cwd: '/consumer', env: {}, execute });
90
+
91
+ await adapter.publishBranch('agent-workflow-kit/update');
92
+
93
+ const gitCommands = commands.map((command) => command.slice(1));
94
+ assert.equal(gitCommands.some(([verb]) => ['switch', 'checkout', 'reset'].includes(verb)), false);
95
+ assert.deepEqual(gitCommands.at(-1), [
96
+ 'push', '--force-with-lease=refs/heads/agent-workflow-kit/update:abc123',
97
+ 'origin', 'HEAD:refs/heads/agent-workflow-kit/update',
98
+ ]);
99
+ });
@@ -45,6 +45,16 @@ def classify(first_line, is_empty):
45
45
  return "create" if is_empty else "skip"
46
46
 
47
47
 
48
+ def update_workflow_action(
49
+ provider, choice, destination_exists, prerequisites=True,
50
+ pull_requests_allowed=True,
51
+ ):
52
+ """Reference decision table for the prompt-driven setup contract."""
53
+ if provider != "github" or destination_exists or choice != "enable":
54
+ return "skip"
55
+ return "create" if prerequisites and pull_requests_allowed else "skip"
56
+
57
+
48
58
  class IdempotencyRule(unittest.TestCase):
49
59
  CASES = [
50
60
  # (first_line, is_empty, expected)
@@ -71,6 +81,61 @@ class IdempotencyRule(unittest.TestCase):
71
81
 
72
82
 
73
83
  class SeedTemplatesValid(unittest.TestCase):
84
+ def test_update_workflow_provider_and_choice_fixtures(self):
85
+ fixtures = [
86
+ ("github", "enable", False, True, True, "create"),
87
+ ("github", "opt-out", False, True, True, "skip"),
88
+ ("github", "later", False, True, True, "skip"),
89
+ ("github", "enable", True, True, True, "skip"),
90
+ ("github", "enable", False, False, True, "skip"),
91
+ ("github", "enable", False, True, False, "skip"),
92
+ ("gitlab", "enable", False, True, True, "skip"),
93
+ ("local", "enable", False, True, True, "skip"),
94
+ ]
95
+ for provider, choice, exists, prerequisites, allowed, expected in fixtures:
96
+ self.assertEqual(
97
+ update_workflow_action(
98
+ provider, choice, exists, prerequisites, allowed,
99
+ ), expected,
100
+ )
101
+
102
+ def test_github_update_opt_in_is_provider_aware_and_idempotent(self):
103
+ skill = (SKILL / "SKILL.md").read_text(encoding="utf-8")
104
+ for token in (
105
+ "Automatic Kit update pull requests",
106
+ ".github/workflows/agent-workflow-kit-update.yml",
107
+ "Opt out",
108
+ "Ask later",
109
+ "GitHub tracker",
110
+ "skipped (already present)",
111
+ "package-lock.json",
112
+ "npm test",
113
+ "can_approve_pull_request_reviews",
114
+ "Allow GitHub Actions to create and approve pull requests",
115
+ "explicit confirmation",
116
+ ):
117
+ self.assertIn(token, skill)
118
+ self.assertIn("do not create a GitHub workflow", skill)
119
+
120
+ def test_github_update_workflow_has_safe_triggers_and_scoped_runner(self):
121
+ workflow = (SKILL / "assets/agent-workflow-kit-update.yml").read_text(encoding="utf-8")
122
+ for token in (
123
+ "schedule:", "workflow_dispatch:", "contents: write",
124
+ "pull-requests: write", "agent-workflow-kit-update-pr",
125
+ "@ikon85/agent-workflow-kit@latest", "fetch-depth: 0",
126
+ "node-version: 22.14", "npm ci --ignore-scripts",
127
+ ):
128
+ self.assertIn(token, workflow)
129
+ for forbidden in ("npm_token", "NPM_TOKEN", "auto-merge", "gh pr merge"):
130
+ self.assertNotIn(forbidden, workflow)
131
+
132
+ mirror = (REPO / ".agents/skills/setup-workflow/assets/agent-workflow-kit-update.yml")
133
+ self.assertEqual(workflow, mirror.read_text(encoding="utf-8"))
134
+
135
+ def test_update_pr_runner_is_an_installed_package_binary(self):
136
+ package = json.loads((REPO / "package.json").read_text(encoding="utf-8"))
137
+ self.assertEqual(package["bin"]["agent-workflow-kit-update-pr"], "scripts/kit-update-pr.mjs")
138
+
74
139
  def test_spec_completeness_seed_has_valid_self_critique_block(self):
75
140
  """A convention without a valid Trigger/Check/Korrektur block makes
76
141
  spec-self-critique point 8 warn — the seed must carry one (Codex R1 #14)."""
@@ -47,6 +47,9 @@ export const HELPER_FILES = [
47
47
  // the parity primitive rather than growing a second registry/GitHub comparison.
48
48
  { path: 'scripts/release-parity.mjs', kind: 'script', mode: 0o644 },
49
49
  { path: 'scripts/release-state.mjs', kind: 'script', mode: 0o644 },
50
+ // GitHub-consumer automation: invokes the existing update command, then owns
51
+ // only the stable tested branch/pull-request upsert.
52
+ { path: 'scripts/kit-update-pr.mjs', kind: 'script', mode: 0o755 },
50
53
  // Shared hook utility imported by the shipped hooks (drift-guard,
51
54
  // sync-board-status). Library (imported, not run) → 0o644. MUST ship or those
52
55
  // hooks ImportError on arrival.