@ikon85/agent-workflow-kit 0.43.0 → 0.44.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.
Files changed (126) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +249 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -12
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +26 -9
  29. package/scripts/worktree-lifecycle/README.md +57 -160
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1429
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/workflow-capabilities.json +0 -28
  53. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  54. package/docs/research/benchlm-routing-source.md +0 -198
  55. package/docs/research/consumer-owned-protocol-files.md +0 -238
  56. package/docs/research/frontend-agent-benchmarks.md +0 -282
  57. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  58. package/docs/research/provider-neutral-agent-routing.md +0 -207
  59. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  60. package/docs/research/wave-43-script-hook-census.md +0 -128
  61. package/scripts/build-kit.mjs +0 -117
  62. package/scripts/build-kit.test.mjs +0 -207
  63. package/scripts/census/census.test.mjs +0 -252
  64. package/scripts/census/state.test.mjs +0 -15
  65. package/scripts/census/transaction.test.mjs +0 -103
  66. package/scripts/census-contract.test.mjs +0 -227
  67. package/scripts/check-kit-staleness.mjs +0 -45
  68. package/scripts/check-kit-staleness.test.mjs +0 -19
  69. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
  70. package/scripts/codex-exec.test.mjs +0 -871
  71. package/scripts/grill-census-wiring-guard.mjs +0 -87
  72. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  73. package/scripts/kit-release.test.mjs +0 -273
  74. package/scripts/kit-update-pr.test.mjs +0 -151
  75. package/scripts/lib/audit-refs.mjs +0 -146
  76. package/scripts/lib/scrub.mjs +0 -113
  77. package/scripts/lib/scrub.test.mjs +0 -187
  78. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  79. package/scripts/portability_profile_scan.py +0 -53
  80. package/scripts/release-delta-guard.test.mjs +0 -174
  81. package/scripts/release-parity.test.mjs +0 -90
  82. package/scripts/release-state.test.mjs +0 -360
  83. package/scripts/test_anchor_table.py +0 -69
  84. package/scripts/test_board_bootstrap.py +0 -348
  85. package/scripts/test_board_sync.py +0 -208
  86. package/scripts/test_board_sync_create_idempotency.py +0 -117
  87. package/scripts/test_board_sync_wave_title.py +0 -120
  88. package/scripts/test_census_backstop.py +0 -712
  89. package/scripts/test_census_forward_contract.py +0 -91
  90. package/scripts/test_census_update_contract.test.mjs +0 -454
  91. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  92. package/scripts/test_dist_kit_smoke.py +0 -220
  93. package/scripts/test_drift_guard_diagnostics.py +0 -295
  94. package/scripts/test_issue_claim_contract.py +0 -169
  95. package/scripts/test_kit_docs_language_census.py +0 -178
  96. package/scripts/test_marker_lib.py +0 -162
  97. package/scripts/test_orchestrate_wave_contract.py +0 -381
  98. package/scripts/test_pr_body_check.py +0 -264
  99. package/scripts/test_profile_globs.py +0 -280
  100. package/scripts/test_program_planning_contract.py +0 -133
  101. package/scripts/test_release_authorization_contract.py +0 -101
  102. package/scripts/test_render_anchor.py +0 -267
  103. package/scripts/test_retro_wrapup_contract.py +0 -153
  104. package/scripts/test_skill_code_review_seed.py +0 -89
  105. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  106. package/scripts/test_skill_frontmatter_lint.py +0 -234
  107. package/scripts/test_skill_gh_lint.py +0 -124
  108. package/scripts/test_skill_language_census.py +0 -254
  109. package/scripts/test_skill_optional_readiness.py +0 -171
  110. package/scripts/test_skill_portability_lint.py +0 -866
  111. package/scripts/test_skill_precommit_template.py +0 -30
  112. package/scripts/test_skill_publish_audit.py +0 -358
  113. package/scripts/test_skill_readiness_contract.py +0 -264
  114. package/scripts/test_skill_readiness_preflight.py +0 -180
  115. package/scripts/test_skill_required_readiness.py +0 -233
  116. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  117. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  118. package/scripts/test_skill_stale_name_lint.py +0 -126
  119. package/scripts/test_skill_surface_refs.py +0 -300
  120. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  121. package/scripts/test_tdd_contract.py +0 -78
  122. package/scripts/test_worktree_ignore_seed.py +0 -320
  123. package/scripts/test_worktree_setup_base_guard.py +0 -140
  124. package/scripts/test_worktree_wrapup_contract.py +0 -1737
  125. package/scripts/test_wrapup_land.py +0 -428
  126. package/scripts/worktree-lifecycle/session.py +0 -1857
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { access, readFile } from 'node:fs/promises';
4
- import { constants } from 'node:fs';
5
- import { dirname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- export const CENSUS_PREFLIGHT_INVOCATION =
9
- 'python3 .claude/hooks/drift-guard.py --census-status';
10
- export const CENSUS_PREFLIGHT_CONTRACT =
11
- 'For a cross-cutting plan, run `python3 .claude/hooks/drift-guard.py --census-status` before locking it. When an activated census reports `block_handoff: true` (including stale or open surfaces), stop the lock, run `$census-update`, resolve the findings, and retry. When the census is disabled or not activated, keep the status visible and perform the existing manual surface walk; do not replace that walk with census guesses.';
12
-
13
- const GRILL_FAMILY_NAME = /^grill-/;
14
- const SURFACE_TREES = { claude: '.claude', codex: '.agents' };
15
- const LOCAL_CENSUS_LOGIC = /\b(?:scanCensus|diffCensus|fingerprintCensus|resolveCensusState|activateCensus)\s*\(/;
16
-
17
- async function exists(path) {
18
- try {
19
- await access(path, constants.R_OK);
20
- return true;
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- export async function auditGrillCensusWiring(repoRoot) {
27
- const manifestPath = join(repoRoot, '.claude', 'skills', 'skill-manifest.json');
28
- const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
29
- const entries = Object.entries(manifest.skills ?? {})
30
- .filter(([name]) => GRILL_FAMILY_NAME.test(name));
31
- const problems = [];
32
- const physical = [];
33
-
34
- for (const [name, entry] of entries) {
35
- if (!Array.isArray(entry.surfaces) || entry.surfaces.length === 0) {
36
- problems.push(`manifest grill has no surfaces: ${name}`);
37
- continue;
38
- }
39
- for (const surface of entry.surfaces) {
40
- const tree = SURFACE_TREES[surface];
41
- if (!tree) {
42
- problems.push(`manifest names an unknown surface for ${name}: ${surface}`);
43
- continue;
44
- }
45
- const path = join(repoRoot, tree, 'skills', name, 'SKILL.md');
46
- physical.push({ name, surface, path });
47
- if (!(await exists(path))) {
48
- problems.push(`manifest surface has no skill file: ${surface}:${name}`);
49
- continue;
50
- }
51
- const prose = await readFile(path, 'utf8');
52
- const invocationCount = prose.split(CENSUS_PREFLIGHT_INVOCATION).length - 1;
53
- if (invocationCount !== 1) {
54
- problems.push(
55
- invocationCount === 0
56
- ? `missing census preflight invocation: ${surface}:${name}`
57
- : `duplicate census preflight invocation: ${surface}:${name}`,
58
- );
59
- }
60
- if (!prose.includes(CENSUS_PREFLIGHT_CONTRACT)) {
61
- problems.push(`census preflight contract drifted: ${surface}:${name}`);
62
- }
63
- if (LOCAL_CENSUS_LOGIC.test(prose)) {
64
- problems.push(`grill duplicates census engine logic: ${surface}:${name}`);
65
- }
66
- }
67
- }
68
-
69
- return {
70
- counts: { logical: entries.length, physical: physical.length },
71
- problems,
72
- };
73
- }
74
-
75
- async function main() {
76
- const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
77
- const result = await auditGrillCensusWiring(repoRoot);
78
- console.log(`grill census wiring: ${result.counts.logical} logical, ${result.counts.physical} physical`);
79
- if (result.problems.length > 0) {
80
- for (const problem of result.problems) console.error(`- ${problem}`);
81
- process.exitCode = 1;
82
- }
83
- }
84
-
85
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
86
- await main();
87
- }
@@ -1,107 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { dirname, join } from 'node:path';
5
- import test from 'node:test';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- import {
9
- CENSUS_PREFLIGHT_CONTRACT,
10
- auditGrillCensusWiring,
11
- } from './grill-census-wiring-guard.mjs';
12
-
13
- const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
14
- const realManifest = JSON.parse(await readFile(
15
- join(REPO, '.claude', 'skills', 'skill-manifest.json'),
16
- 'utf8',
17
- )).skills;
18
- const baselineGrills = Object.fromEntries(
19
- Object.entries(realManifest).filter(([name]) => name.startsWith('grill-')),
20
- );
21
- const baselineCounts = {
22
- logical: Object.keys(baselineGrills).length,
23
- physical: Object.values(baselineGrills)
24
- .reduce((count, entry) => count + entry.surfaces.length, 0),
25
- };
26
- const [baselineName, baselineEntry] = Object.entries(baselineGrills)[0];
27
- const baselineSurface = baselineEntry.surfaces[0];
28
- let extraGrillName = 'grill-fixture-extra';
29
- while (extraGrillName in baselineGrills) extraGrillName += '-next';
30
-
31
- async function fixture({ manifest = baselineGrills, omitInvocation, omitFile } = {}) {
32
- const root = await mkdtemp(join(tmpdir(), 'grill-census-guard-'));
33
- await mkdir(join(root, '.claude', 'skills'), { recursive: true });
34
- await writeFile(
35
- join(root, '.claude', 'skills', 'skill-manifest.json'),
36
- `${JSON.stringify({ skills: manifest })}\n`,
37
- );
38
- for (const [name, entry] of Object.entries(manifest)) {
39
- for (const surface of entry.surfaces) {
40
- const relative = `${surface}:${name}`;
41
- if (relative === omitFile) continue;
42
- const tree = surface === 'claude' ? '.claude' : '.agents';
43
- const directory = join(root, tree, 'skills', name);
44
- await mkdir(directory, { recursive: true });
45
- const invocation = relative === omitInvocation ? '' : CENSUS_PREFLIGHT_CONTRACT;
46
- await writeFile(join(directory, 'SKILL.md'), `# ${name}\n${invocation}\n`);
47
- }
48
- }
49
- return root;
50
- }
51
-
52
- test('fixture wiring covers every grill and surface in the real manifest', async () => {
53
- const result = await auditGrillCensusWiring(await fixture());
54
-
55
- assert.deepEqual(result.counts, baselineCounts);
56
- assert.deepEqual(result.problems, []);
57
- });
58
-
59
- test('a grill surface missing the shared preflight invocation is rejected', async () => {
60
- const result = await auditGrillCensusWiring(await fixture({
61
- omitInvocation: `${baselineSurface}:${baselineName}`,
62
- }));
63
-
64
- assert.match(
65
- result.problems.join('\n'),
66
- new RegExp(`missing census preflight invocation.*${baselineName}`, 'i'),
67
- );
68
- });
69
-
70
- test('a dead manifest surface reference is rejected', async () => {
71
- const result = await auditGrillCensusWiring(await fixture({
72
- omitFile: `${baselineSurface}:${baselineName}`,
73
- }));
74
-
75
- assert.match(
76
- result.problems.join('\n'),
77
- new RegExp(`manifest surface has no skill file.*${baselineName}`, 'i'),
78
- );
79
- });
80
-
81
- test('a newly manifested grill variant expands the family and cannot hide a missing surface', async () => {
82
- const addedSurfaces = ['claude', 'codex'];
83
- const manifest = {
84
- ...baselineGrills,
85
- [extraGrillName]: { surfaces: addedSurfaces },
86
- };
87
- const result = await auditGrillCensusWiring(await fixture({
88
- manifest,
89
- omitFile: `codex:${extraGrillName}`,
90
- }));
91
-
92
- assert.deepEqual(result.counts, {
93
- logical: baselineCounts.logical + 1,
94
- physical: baselineCounts.physical + addedSurfaces.length,
95
- });
96
- assert.match(
97
- result.problems.join('\n'),
98
- new RegExp(`manifest surface has no skill file.*${extraGrillName}`, 'i'),
99
- );
100
- });
101
-
102
- test('the repository wiring covers the complete manifest-derived grill family', async () => {
103
- const result = await auditGrillCensusWiring(REPO);
104
-
105
- assert.deepEqual(result.counts, baselineCounts);
106
- assert.deepEqual(result.problems, []);
107
- });
@@ -1,273 +0,0 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { execFile } from 'node:child_process';
4
- import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
- import { tmpdir } from 'node:os';
6
- import { dirname, join } from 'node:path';
7
- import { promisify } from 'node:util';
8
- import { fileURLToPath } from 'node:url';
9
- import { nextVersion, prepareRelease } from './kit-release.mjs';
10
-
11
- const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
12
- const exec = promisify(execFile);
13
-
14
- async function releaseWorkflow() {
15
- return readFile(join(REPO, '.github/workflows/release.yml'), 'utf8');
16
- }
17
-
18
- function workflowRunScript(workflow, stepName) {
19
- const lines = workflow.split('\n');
20
- const nameIndex = lines.findIndex((line) => line.trim() === `- name: ${stepName}`);
21
- assert.notEqual(nameIndex, -1, `workflow step not found: ${stepName}`);
22
- const runIndex = lines.findIndex(
23
- (line, index) => index > nameIndex && line.trim() === 'run: |',
24
- );
25
- assert.notEqual(runIndex, -1, `run block not found for workflow step: ${stepName}`);
26
- const indent = lines[runIndex].match(/^\s*/)[0].length + 2;
27
- const body = [];
28
- for (const line of lines.slice(runIndex + 1)) {
29
- if (line.trim() && line.match(/^\s*/)[0].length < indent) break;
30
- body.push(line.slice(indent));
31
- }
32
- return body.join('\n');
33
- }
34
-
35
- async function git(runCwd, args, options = {}) {
36
- return exec('git', args, { cwd: runCwd, ...options });
37
- }
38
-
39
- async function releaseIntentFixture({
40
- packageVersion = '1.2.3',
41
- annotated = true,
42
- onMain = true,
43
- mainAhead = false,
44
- } = {}) {
45
- const root = await mkdtemp(join(tmpdir(), 'kit-release-intent-'));
46
- const origin = join(root, 'origin.git');
47
- const repo = join(root, 'repo');
48
- await git(root, ['init', '--bare', origin]);
49
- await git(root, ['init', '--initial-branch=main', repo]);
50
- await git(repo, ['config', 'user.name', 'Release Test']);
51
- await git(repo, ['config', 'user.email', 'release-test@example.invalid']);
52
- await writeFile(join(repo, 'package.json'), `${JSON.stringify({
53
- name: '@ikon85/agent-workflow-kit',
54
- version: packageVersion,
55
- }, null, 2)}\n`);
56
- await git(repo, ['add', 'package.json']);
57
- await git(repo, ['commit', '-m', 'release fixture']);
58
- await git(repo, ['remote', 'add', 'origin', origin]);
59
- await git(repo, ['push', '-u', 'origin', 'main']);
60
- if (!onMain) {
61
- await git(repo, ['checkout', '-b', 'not-main']);
62
- await writeFile(join(repo, 'outside-main.txt'), 'not canonical\n');
63
- await git(repo, ['add', 'outside-main.txt']);
64
- await git(repo, ['commit', '-m', 'outside main']);
65
- }
66
- const tag = 'v1.2.3';
67
- await git(repo, annotated
68
- ? ['tag', '-a', tag, '-m', `Release ${tag}`]
69
- : ['tag', tag]);
70
- if (mainAhead) {
71
- await writeFile(join(repo, 'after-release.txt'), 'main moved\n');
72
- await git(repo, ['add', 'after-release.txt']);
73
- await git(repo, ['commit', '-m', 'move canonical main']);
74
- await git(repo, ['push', 'origin', 'main']);
75
- }
76
- return { root, repo, tag };
77
- }
78
-
79
- async function validateReleaseIntent(fixture, releaseTag = fixture.tag) {
80
- const workflow = await releaseWorkflow();
81
- const script = workflowRunScript(workflow, 'Validate release intent');
82
- const output = join(fixture.root, 'github-output');
83
- try {
84
- const result = await exec('bash', ['-euo', 'pipefail', '-c', script], {
85
- cwd: fixture.repo,
86
- env: {
87
- ...process.env,
88
- GITHUB_OUTPUT: output,
89
- RELEASE_TAG: releaseTag,
90
- },
91
- });
92
- return { ...result, output: await readFile(output, 'utf8') };
93
- } catch (error) {
94
- return { error, stderr: error.stderr ?? '', stdout: error.stdout ?? '' };
95
- }
96
- }
97
-
98
- test('release workflow treats an annotated version tag as normal intent and merge as integration only', async () => {
99
- const workflow = await releaseWorkflow();
100
- assert.match(workflow, /push:\s*\n\s+tags:\s*\n\s+- ['"]v\*\.\*\.\*['"]/);
101
- assert.doesNotMatch(workflow, /push:\s*\n\s+branches:\s*\[main\]/);
102
- assert.match(workflow, /workflow_dispatch:\s*\n\s+inputs:\s*\n\s+tag:/);
103
- assert.match(workflow, /tag:[\s\S]*?required:\s*true/);
104
- assert.match(
105
- workflow,
106
- /group:\s*release-\$\{\{ github\.event_name == 'workflow_dispatch' && inputs\.tag \|\| github\.ref_name \}\}/,
107
- );
108
- assert.doesNotMatch(workflow, /git diff --name-only[\s\S]*package\.json/);
109
- assert.ok(
110
- workflow.indexOf('- name: Validate release intent')
111
- < workflow.indexOf('node scripts/release-state.mjs'),
112
- 'release intent must be validated before the reconciler can publish',
113
- );
114
- assert.equal(workflow.match(/node scripts\/release-state\.mjs/g)?.length, 1);
115
- });
116
-
117
- test('the same pre-publish validator accepts a matching annotated tag', async () => {
118
- const fixture = await releaseIntentFixture();
119
- try {
120
- const result = await validateReleaseIntent(fixture);
121
- assert.equal(result.error, undefined, result.stderr);
122
- assert.match(result.output, /^tag=v1\.2\.3$/m);
123
- assert.match(result.output, /^commit=[0-9a-f]{40}$/m);
124
- } finally {
125
- await rm(fixture.root, { recursive: true, force: true });
126
- }
127
- });
128
-
129
- for (const scenario of [
130
- {
131
- name: 'missing manual recovery tag',
132
- options: {},
133
- tag: 'v9.9.9',
134
- error: /does not exist/,
135
- },
136
- {
137
- name: 'tag/package version mismatch',
138
- options: { packageVersion: '1.2.4' },
139
- error: /does not match package version/,
140
- },
141
- {
142
- name: 'lightweight tag',
143
- options: { annotated: false },
144
- error: /must be annotated/,
145
- },
146
- {
147
- name: 'tag outside canonical main ancestry',
148
- options: { onMain: false },
149
- error: /not an ancestor of origin\/main/,
150
- },
151
- {
152
- name: 'stale canonical main commit with the same package version',
153
- options: { mainAhead: true },
154
- error: /does not identify the current origin\/main commit/,
155
- },
156
- ]) {
157
- test(`the pre-publish validator rejects a ${scenario.name}`, async () => {
158
- const fixture = await releaseIntentFixture(scenario.options);
159
- try {
160
- const result = await validateReleaseIntent(fixture, scenario.tag);
161
- assert.ok(result.error, 'validation unexpectedly succeeded');
162
- assert.match(`${result.stderr}\n${result.stdout}`, scenario.error);
163
- } finally {
164
- await rm(fixture.root, { recursive: true, force: true });
165
- }
166
- });
167
- }
168
-
169
- test('the install manifest ships both release primitives named by the skill', async () => {
170
- const manifest = JSON.parse(await readFile(join(REPO, 'agent-workflow-kit.package.json')));
171
- const paths = manifest.files.map((entry) => entry.path);
172
- assert.ok(paths.includes('scripts/kit-release.mjs'));
173
- assert.ok(paths.includes('scripts/release-delta-guard.mjs'));
174
- assert.ok(paths.includes('scripts/release-parity.mjs'));
175
- assert.ok(paths.includes('scripts/release-state.mjs'));
176
- });
177
-
178
- test('both release skill surfaces name only the owned scoped npm package', async () => {
179
- const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
180
- const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
181
- assert.equal(codex, claude);
182
- for (const body of [claude, codex]) {
183
- assert.match(body, /`@ikon85\/agent-workflow-kit`/);
184
- }
185
- });
186
-
187
- // Amended by #257: one gate, at the Semver. The previous contract required a
188
- // second confirmation before the tag; that gate left prepared versions stranded
189
- // in `awaiting-tag` and is gone. What must survive is the narrowing rule — a
190
- // build-only request never becomes release authority.
191
- test('one confirmed Semver authorizes the release through tag and publish', async () => {
192
- const claude = await readFile(join(REPO, '.claude/skills/kit-release/SKILL.md'), 'utf8');
193
- const codex = await readFile(join(REPO, '.agents/skills/kit-release/SKILL.md'), 'utf8');
194
- assert.equal(codex.split('\n---\n')[1], claude.split('\n---\n')[1]);
195
- for (const body of [claude, codex]) {
196
- assert.match(body, /explicit AFK end-to-end mandate/i);
197
- assert.match(body, /deterministic recommendation/i);
198
- assert.match(body, /annotated\s+`v<version>` tag/i);
199
- assert.match(body, /confirmed Semver authorizes[\s\S]*without asking again/i);
200
- assert.match(body, /narrower build-only or single-action request/i);
201
- assert.doesNotMatch(body, /separate explicit confirmation/i);
202
- }
203
- });
204
-
205
- test('maintainer docs and the accepted ADR agree that merge integrates and an annotated tag publishes', async () => {
206
- const paths = [
207
- 'CLAUDE.md',
208
- 'AGENTS.md',
209
- 'README.md',
210
- '.claude/skills/kit-release/SKILL.md',
211
- '.agents/skills/kit-release/SKILL.md',
212
- 'docs/adr/0004-release-intent-is-a-version-tag.md',
213
- ];
214
- const bodies = await Promise.all(
215
- paths.map((path) => readFile(join(REPO, path), 'utf8')),
216
- );
217
- for (const [index, body] of bodies.entries()) {
218
- assert.match(body, /annotated\s+[`*]?v<version>[`*]?\s+tag/i, paths[index]);
219
- assert.match(body, /integrat/i, paths[index]);
220
- }
221
- assert.match(bodies.at(-1), /Status: accepted \(2026-07-22, issue #204\)/);
222
- });
223
-
224
- test('patch, minor, and major confirmations select exactly one target version', () => {
225
- assert.equal(nextVersion('1.2.3', 'patch'), '1.2.4');
226
- assert.equal(nextVersion('1.2.3', 'minor'), '1.3.0');
227
- assert.equal(nextVersion('1.2.3', 'major'), '2.0.0');
228
- });
229
-
230
- async function fixture() {
231
- const root = await mkdtemp(join(tmpdir(), 'kit-release-'));
232
- await mkdir(join(root, 'scripts'));
233
- await writeFile(join(root, 'package.json'), '{"name":"kit","version":"1.2.3"}\n');
234
- await writeFile(join(root, 'README.md'), '# Kit\n\n## Release notes\n\n### 1.2.3\n\n- Old.\n');
235
- await writeFile(join(root, 'agent-workflow-kit.package.json'), '{"kitVersion":"1.2.3","files":[]}\n');
236
- return root;
237
- }
238
-
239
- test('confirmed target updates package, release notes, and regenerated manifest', async () => {
240
- const root = await fixture();
241
- const commands = [];
242
- try {
243
- const result = await prepareRelease({
244
- repoRoot: root, targetVersion: '1.3.0',
245
- delta: { added: ['scripts/new.mjs'], removed: [], changed: ['README.md'] },
246
- buildManifest: async () => ({ kitVersion: '1.3.0', files: [{ path: 'scripts/new.mjs' }] }),
247
- run: async (command, args) => commands.push([command, ...args].join(' ')),
248
- });
249
- assert.equal(result.status, 'prepared');
250
- assert.equal(JSON.parse(await readFile(join(root, 'package.json'))).version, '1.3.0');
251
- assert.equal(JSON.parse(await readFile(join(root, 'agent-workflow-kit.package.json'))).kitVersion, '1.3.0');
252
- assert.match(await readFile(join(root, 'README.md'), 'utf8'), /### 1\.3\.0[\s\S]*scripts\/new\.mjs/);
253
- assert.deepEqual(commands, [
254
- 'npm run release:guard', 'npm test', 'npm pack --dry-run',
255
- ]);
256
- } finally { await rm(root, { recursive: true, force: true }); }
257
- });
258
-
259
- test('a failed gate leaves an explainable target that retries without another bump', async () => {
260
- const root = await fixture();
261
- const options = {
262
- repoRoot: root, targetVersion: '1.2.4',
263
- delta: { added: [], removed: [], changed: ['README.md'] },
264
- buildManifest: async () => ({ kitVersion: '1.2.4', files: [] }),
265
- };
266
- try {
267
- await assert.rejects(prepareRelease({ ...options, run: async () => { throw new Error('test red'); } }), /test red/);
268
- assert.equal(JSON.parse(await readFile(join(root, 'package.json'))).version, '1.2.4');
269
- const retried = await prepareRelease({ ...options, run: async () => {} });
270
- assert.equal(retried.status, 'resumed');
271
- assert.equal(JSON.parse(await readFile(join(root, 'package.json'))).version, '1.2.4');
272
- } finally { await rm(root, { recursive: true, force: true }); }
273
- });
@@ -1,151 +0,0 @@
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('automated update pull requests carry the behavior availability summary', async () => {
32
- const h = harness({ update: {
33
- exitCode: 0,
34
- stdout: [
35
- 'newly available: orchestrate-wave',
36
- 'newly degraded: orchestrate-wave.projectRecipe',
37
- 'newly blocked: none',
38
- 'still unresolved: orchestrateWaveRecipe:invalid',
39
- ].join('\n'),
40
- stderr: '',
41
- } });
42
-
43
- await orchestrateUpdatePullRequest(h.options);
44
-
45
- const created = h.calls.find((call) => Array.isArray(call) && call[0] === 'create')[1];
46
- assert.match(created.body, /## Availability/);
47
- assert.match(created.body, /newly degraded: orchestrate-wave\.projectRecipe/);
48
- assert.match(created.body, /still unresolved: orchestrateWaveRecipe:invalid/);
49
- assert.match(created.body, /built-in Kit invariants/);
50
- assert.doesNotMatch(created.body, /consumer test/i);
51
- assert.match(created.body, /never merged automatically/);
52
- });
53
-
54
- test('automated update pull requests carry the required consumer migrations', async () => {
55
- const h = harness({ update: {
56
- exitCode: 0,
57
- stdout: [
58
- 'newly available: none',
59
- 'required migration: wrapup-landing-artifact-policy · setup-workflow · '
60
- + 'docs/agents/workflow-capabilities.json · wrapup.landingGeneratedArtifactPatterns',
61
- ].join('\n'),
62
- stderr: '',
63
- } });
64
-
65
- await orchestrateUpdatePullRequest(h.options);
66
-
67
- const created = h.calls.find((call) => Array.isArray(call) && call[0] === 'create')[1];
68
- assert.match(created.body, /## Required migrations/);
69
- assert.match(created.body, /required migration: wrapup-landing-artifact-policy/);
70
- assert.match(created.body, /docs\/agents\/workflow-capabilities\.json/);
71
- assert.match(created.body, /wrapup\.landingGeneratedArtifactPatterns/);
72
- });
73
-
74
- test('a pull request without pending migrations says so explicitly', async () => {
75
- const h = harness({ update: { exitCode: 0, stdout: 'newly available: none', stderr: '' } });
76
-
77
- await orchestrateUpdatePullRequest(h.options);
78
-
79
- const created = h.calls.find((call) => Array.isArray(call) && call[0] === 'create')[1];
80
- assert.match(created.body, /## Required migrations\n\nNo required consumer migrations reported\./);
81
- });
82
-
83
- test('a conflict produces a structured report without touching the stable branch', async () => {
84
- const h = harness({ update: { exitCode: 2, stdout: 'conflicts: 1', stderr: '' } });
85
- const report = await orchestrateUpdatePullRequest(h.options);
86
-
87
- assert.deepEqual(report, {
88
- status: 'conflicted',
89
- branch: 'agent-workflow-kit/update',
90
- update: { exitCode: 2, stdout: 'conflicts: 1', stderr: '' },
91
- });
92
- assert.deepEqual(h.calls, ['update']);
93
- });
94
-
95
- test('a release mismatch fails without touching the consumer update branch', async () => {
96
- const h = harness({ update: { exitCode: 1, stdout: '', stderr: 'release mismatch' } });
97
- const report = await orchestrateUpdatePullRequest(h.options);
98
-
99
- assert.equal(report.status, 'failed');
100
- assert.equal(report.update.stderr, 'release mismatch');
101
- assert.deepEqual(h.calls, ['update']);
102
- });
103
-
104
- test('ambiguous existing pull requests fail before the last good branch is changed', async () => {
105
- const h = harness({ pulls: [{ number: 7 }, { number: 9 }] });
106
- const report = await orchestrateUpdatePullRequest(h.options);
107
-
108
- assert.equal(report.status, 'failed');
109
- assert.equal(report.reason, 'multiple-open-pull-requests');
110
- assert.deepEqual(h.calls.map((call) => Array.isArray(call) ? call[0] : call), [
111
- 'update', 'changes', 'list',
112
- ]);
113
- });
114
-
115
- test('an already current consumer is a no-op', async () => {
116
- const h = harness({ changed: false });
117
- const report = await orchestrateUpdatePullRequest(h.options);
118
-
119
- assert.equal(report.status, 'current');
120
- assert.deepEqual(h.calls, ['update', 'changes']);
121
- });
122
-
123
- test('a repeated run updates the one existing stable pull request', async () => {
124
- const h = harness({ pulls: [{ number: 7 }] });
125
- const report = await orchestrateUpdatePullRequest(h.options);
126
-
127
- assert.equal(report.status, 'updated');
128
- assert.equal(report.pullRequest, 7);
129
- assert.deepEqual(h.calls.map((call) => Array.isArray(call) ? call[0] : call), [
130
- 'update', 'changes', 'list', 'publish', 'upsert',
131
- ]);
132
- });
133
-
134
- test('publishing commits in place and lease-protects the stable remote branch', async () => {
135
- const commands = [];
136
- const execute = async (file, args) => {
137
- commands.push([file, ...args]);
138
- if (args[0] === 'ls-remote') return { stdout: 'abc123\trefs/heads/agent-workflow-kit/update\n', stderr: '' };
139
- return { stdout: '', stderr: '' };
140
- };
141
- const adapter = createSystemAdapters({ cwd: '/consumer', env: {}, execute });
142
-
143
- await adapter.publishBranch('agent-workflow-kit/update');
144
-
145
- const gitCommands = commands.map((command) => command.slice(1));
146
- assert.equal(gitCommands.some(([verb]) => ['switch', 'checkout', 'reset'].includes(verb)), false);
147
- assert.deepEqual(gitCommands.at(-1), [
148
- 'push', '--force-with-lease=refs/heads/agent-workflow-kit/update:abc123',
149
- 'origin', 'HEAD:refs/heads/agent-workflow-kit/update',
150
- ]);
151
- });