@ikon85/agent-workflow-kit 0.20.0 → 0.21.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.
- package/.agents/skills/project-release/SKILL.md +71 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +1 -0
- package/.claude/skills/project-release/SKILL.md +71 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +1 -0
- package/.claude/skills/skill-manifest.json +10 -0
- package/PROVENANCE.md +1 -1
- package/README.md +13 -2
- package/agent-workflow-kit.package.json +36 -4
- package/package.json +1 -1
- package/scripts/kit-release.mjs +15 -10
- package/scripts/project-release.mjs +70 -0
- package/scripts/test_skill_publish_audit.py +28 -9
- package/src/lib/bundle.mjs +4 -1
- package/src/lib/release-apply.mjs +81 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-release
|
|
3
|
+
description: "Preview and prepare one coherent version change across a consumer's profiled packages without duplicating release logic or creating commits, tags, pushes, publishes, or merges."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project Release
|
|
7
|
+
|
|
8
|
+
Prepare a consumer-owned multi-package release through the kit's shared
|
|
9
|
+
SemVer, preview, and transactional apply engine. This skill changes only the
|
|
10
|
+
profiled version files. It does not commit, tag, push, publish, or merge.
|
|
11
|
+
|
|
12
|
+
## Profile contract
|
|
13
|
+
|
|
14
|
+
Read `docs/agents/workflow-capabilities.json`. The consumer owns this file and
|
|
15
|
+
its unknown keys. Project Release requires this versioned section:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"schemaVersion": 1,
|
|
20
|
+
"projectRelease": {
|
|
21
|
+
"versionFiles": [
|
|
22
|
+
"package.json",
|
|
23
|
+
"packages/example/package.json"
|
|
24
|
+
],
|
|
25
|
+
"tagPrefix": "v"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every `versionFiles` path is repository-relative and must name a JSON file with
|
|
31
|
+
the same current SemVer. Never infer or silently add package files.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Run a byte-neutral preview for exactly one Patch, Minor, Major, or explicit
|
|
36
|
+
SemVer target:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
node scripts/project-release.mjs preview <patch|minor|major|version>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Report the structured result: exact current and target version, package
|
|
43
|
+
count, synchronized files, planned commit/tag actions, confirmation token,
|
|
44
|
+
and every blocker. Invalid or divergent versions, dirty profiled targets,
|
|
45
|
+
and an existing target tag block before the first write.
|
|
46
|
+
|
|
47
|
+
3. Ask the user to confirm the exact target and preview confirmation token.
|
|
48
|
+
Do not treat a bump recommendation or a prior preview as confirmation.
|
|
49
|
+
|
|
50
|
+
4. Apply only that still-current preview:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
node scripts/project-release.mjs apply <patch|minor|major|version> \
|
|
54
|
+
--confirm <confirmation-token>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Apply re-reads repository facts and every target byte. A stale token,
|
|
58
|
+
changed target, blocked preview, partial write, or repeated apply fails
|
|
59
|
+
without a double bump. Partial writes roll back to the original bytes.
|
|
60
|
+
|
|
61
|
+
5. Inspect the resulting version-file diff, then hand commit, tag, push,
|
|
62
|
+
publish, and merge to the repository's normal release/landing workflow.
|
|
63
|
+
Project Release does not commit, tag, push, publish, or merge.
|
|
64
|
+
|
|
65
|
+
## Safety contract
|
|
66
|
+
|
|
67
|
+
- Preview is repeatable and byte-neutral.
|
|
68
|
+
- Apply writes only `projectRelease.versionFiles`.
|
|
69
|
+
- Commit and tag entries in preview output are plans, never side effects.
|
|
70
|
+
- Existing tags are never overwritten.
|
|
71
|
+
- No runtime dependency is required beyond Node.js and Git.
|
|
@@ -21,6 +21,7 @@ Use this section as the entry-point map for agent-assisted work. The individual
|
|
|
21
21
|
- **Preview or restore consumer-owned memories:** use `memory-lifecycle` to classify every configured path without writing, then apply an explicitly approved collision-safe restore with a content-free receipt.
|
|
22
22
|
- **Finished slice:** use `wrapup` to prepare the branch, PR, and cleanup steps your repo expects.
|
|
23
23
|
- **Kit release:** use `kit-release` to derive the shipped delta, confirm Semver, regenerate the manifest, run all gates, and then hand landing to `wrapup`.
|
|
24
|
+
- **Consumer project release:** use `project-release` to preview and transactionally prepare one coherent version across the package files named by the consumer-owned capability profile.
|
|
24
25
|
- **Kit update:** use `kit-update` to preview and transactionally apply a parity-verified scoped release without overwriting local modifications.
|
|
25
26
|
- **A huge, foggy effort, too big for one session:** use `wayfinder` — it charts it as a shared map of investigation tickets, resolving one per session.
|
|
26
27
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-release
|
|
3
|
+
description: "Preview and prepare one coherent version change across a consumer's profiled packages without duplicating release logic or creating commits, tags, pushes, publishes, or merges."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Project Release
|
|
7
|
+
|
|
8
|
+
Prepare a consumer-owned multi-package release through the kit's shared
|
|
9
|
+
SemVer, preview, and transactional apply engine. This skill changes only the
|
|
10
|
+
profiled version files. It does not commit, tag, push, publish, or merge.
|
|
11
|
+
|
|
12
|
+
## Profile contract
|
|
13
|
+
|
|
14
|
+
Read `docs/agents/workflow-capabilities.json`. The consumer owns this file and
|
|
15
|
+
its unknown keys. Project Release requires this versioned section:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"schemaVersion": 1,
|
|
20
|
+
"projectRelease": {
|
|
21
|
+
"versionFiles": [
|
|
22
|
+
"package.json",
|
|
23
|
+
"packages/example/package.json"
|
|
24
|
+
],
|
|
25
|
+
"tagPrefix": "v"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every `versionFiles` path is repository-relative and must name a JSON file with
|
|
31
|
+
the same current SemVer. Never infer or silently add package files.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Run a byte-neutral preview for exactly one Patch, Minor, Major, or explicit
|
|
36
|
+
SemVer target:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
node scripts/project-release.mjs preview <patch|minor|major|version>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Report the structured result: exact current and target version, package
|
|
43
|
+
count, synchronized files, planned commit/tag actions, confirmation token,
|
|
44
|
+
and every blocker. Invalid or divergent versions, dirty profiled targets,
|
|
45
|
+
and an existing target tag block before the first write.
|
|
46
|
+
|
|
47
|
+
3. Ask the user to confirm the exact target and preview confirmation token.
|
|
48
|
+
Do not treat a bump recommendation or a prior preview as confirmation.
|
|
49
|
+
|
|
50
|
+
4. Apply only that still-current preview:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
node scripts/project-release.mjs apply <patch|minor|major|version> \
|
|
54
|
+
--confirm <confirmation-token>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Apply re-reads repository facts and every target byte. A stale token,
|
|
58
|
+
changed target, blocked preview, partial write, or repeated apply fails
|
|
59
|
+
without a double bump. Partial writes roll back to the original bytes.
|
|
60
|
+
|
|
61
|
+
5. Inspect the resulting version-file diff, then hand commit, tag, push,
|
|
62
|
+
publish, and merge to the repository's normal release/landing workflow.
|
|
63
|
+
Project Release does not commit, tag, push, publish, or merge.
|
|
64
|
+
|
|
65
|
+
## Safety contract
|
|
66
|
+
|
|
67
|
+
- Preview is repeatable and byte-neutral.
|
|
68
|
+
- Apply writes only `projectRelease.versionFiles`.
|
|
69
|
+
- Commit and tag entries in preview output are plans, never side effects.
|
|
70
|
+
- Existing tags are never overwritten.
|
|
71
|
+
- No runtime dependency is required beyond Node.js and Git.
|
|
@@ -21,6 +21,7 @@ Use this section as the entry-point map for agent-assisted work. The individual
|
|
|
21
21
|
- **Preview or restore consumer-owned memories:** use `memory-lifecycle` to classify every configured path without writing, then apply an explicitly approved collision-safe restore with a content-free receipt.
|
|
22
22
|
- **Finished slice:** use `wrapup` to prepare the branch, PR, and cleanup steps your repo expects.
|
|
23
23
|
- **Kit release:** use `kit-release` to derive the shipped delta, confirm Semver, regenerate the manifest, run all gates, and then hand landing to `wrapup`.
|
|
24
|
+
- **Consumer project release:** use `project-release` to preview and transactionally prepare one coherent version across the package files named by the consumer-owned capability profile.
|
|
24
25
|
- **Kit update:** use `kit-update` to preview and transactionally apply a parity-verified scoped release without overwriting local modifications.
|
|
25
26
|
- **A huge, foggy effort, too big for one session:** use `wayfinder` — it charts it as a shared map of investigation tickets, resolving one per session.
|
|
26
27
|
|
|
@@ -395,6 +395,16 @@
|
|
|
395
395
|
],
|
|
396
396
|
"provenance": "own"
|
|
397
397
|
},
|
|
398
|
+
"project-release": {
|
|
399
|
+
"class": "generic",
|
|
400
|
+
"publish": true,
|
|
401
|
+
"entryPoint": true,
|
|
402
|
+
"surfaces": [
|
|
403
|
+
"claude",
|
|
404
|
+
"codex"
|
|
405
|
+
],
|
|
406
|
+
"provenance": "own"
|
|
407
|
+
},
|
|
398
408
|
"kit-update": {
|
|
399
409
|
"class": "generic",
|
|
400
410
|
"publish": true,
|
package/PROVENANCE.md
CHANGED
|
@@ -33,7 +33,7 @@ carries Chase's `THIRD-PARTY-NOTICES.md`.
|
|
|
33
33
|
|
|
34
34
|
retro, wrapup, spec-self-critique, board-to-waves, verify-spike, decision-gate,
|
|
35
35
|
codex-adapter-sync, setup-pre-commit, code-review, orchestrate-wave,
|
|
36
|
-
census-update, memory-lifecycle — covered by the root LICENSE.
|
|
36
|
+
census-update, memory-lifecycle, project-release — covered by the root LICENSE.
|
|
37
37
|
`setup-pre-commit` was rewritten from Matt Pocock's husky-based skill to a
|
|
38
38
|
zero-dep native git `core.hooksPath` scaffold; nothing of the original
|
|
39
39
|
mechanic remains (courtesy attribution).
|
package/README.md
CHANGED
|
@@ -332,6 +332,16 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
332
332
|
|
|
333
333
|
## Release notes
|
|
334
334
|
|
|
335
|
+
### 0.21.0
|
|
336
|
+
|
|
337
|
+
- added: `.agents/skills/project-release/SKILL.md`
|
|
338
|
+
- added: `.claude/skills/project-release/SKILL.md`
|
|
339
|
+
- added: `scripts/project-release.mjs`
|
|
340
|
+
- added: `src/lib/release-apply.mjs`
|
|
341
|
+
- changed: `.agents/skills/setup-workflow/workflow-overview.md`
|
|
342
|
+
- changed: `.claude/skills/setup-workflow/workflow-overview.md`
|
|
343
|
+
- changed: `scripts/kit-release.mjs`
|
|
344
|
+
|
|
335
345
|
### 0.20.0
|
|
336
346
|
|
|
337
347
|
- added: `.claude/hooks/branch-context.py`
|
|
@@ -652,12 +662,13 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
652
662
|
|
|
653
663
|
## What's in the box
|
|
654
664
|
|
|
655
|
-
**
|
|
665
|
+
**42 skills** (Router: ask-matt — "which skill/flow fits?" · Plan: grill-me,
|
|
656
666
|
grill-with-docs, to-prd, to-issues, board-to-waves, triage, spec-self-critique,
|
|
657
667
|
verify-spike, decision-gate, scale-check, to-waves, wayfinder, research · Execute: tdd, prototype, implement, orchestrate-wave ·
|
|
658
668
|
Design/diagnose/refactor streams: diagnose, zoom-out,
|
|
659
669
|
improve-codebase-architecture, codebase-design, domain-modeling, security-audit · Land: wrapup,
|
|
660
|
-
resolving-merge-conflicts, code-review, local-ci, git-worktree-recover, kit-release
|
|
670
|
+
resolving-merge-conflicts, code-review, local-ci, git-worktree-recover, kit-release,
|
|
671
|
+
project-release · Learn: retro, audit-skills, write-a-skill · Setup:
|
|
661
672
|
setup-workflow, git-guardrails, setup-pre-commit · Codex cross-model: grill-me-codex,
|
|
662
673
|
grill-with-docs-codex, codex-review, codex-build),
|
|
663
674
|
installed for both surfaces — `.claude/skills`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.21.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -379,6 +379,15 @@
|
|
|
379
379
|
"mode": 420,
|
|
380
380
|
"origin": "kit"
|
|
381
381
|
},
|
|
382
|
+
{
|
|
383
|
+
"path": ".agents/skills/project-release/SKILL.md",
|
|
384
|
+
"kind": "skill",
|
|
385
|
+
"ownerSkill": "project-release",
|
|
386
|
+
"surface": "codex",
|
|
387
|
+
"sha256": "5aa0b0827e8691334c30608df8506c756455ecebcc460efa140889297cf02ed5",
|
|
388
|
+
"mode": 420,
|
|
389
|
+
"origin": "kit"
|
|
390
|
+
},
|
|
382
391
|
{
|
|
383
392
|
"path": ".agents/skills/prototype/LOGIC.md",
|
|
384
393
|
"kind": "skill",
|
|
@@ -609,7 +618,7 @@
|
|
|
609
618
|
"kind": "skill",
|
|
610
619
|
"ownerSkill": "setup-workflow",
|
|
611
620
|
"surface": "codex",
|
|
612
|
-
"sha256": "
|
|
621
|
+
"sha256": "bc4db6413135ad9056cc63691cb823a9b198ad47538f292b37fcbb915477a716",
|
|
613
622
|
"mode": 420,
|
|
614
623
|
"origin": "kit"
|
|
615
624
|
},
|
|
@@ -1405,6 +1414,15 @@
|
|
|
1405
1414
|
"mode": 420,
|
|
1406
1415
|
"origin": "kit"
|
|
1407
1416
|
},
|
|
1417
|
+
{
|
|
1418
|
+
"path": ".claude/skills/project-release/SKILL.md",
|
|
1419
|
+
"kind": "skill",
|
|
1420
|
+
"ownerSkill": "project-release",
|
|
1421
|
+
"surface": "claude",
|
|
1422
|
+
"sha256": "5aa0b0827e8691334c30608df8506c756455ecebcc460efa140889297cf02ed5",
|
|
1423
|
+
"mode": 420,
|
|
1424
|
+
"origin": "kit"
|
|
1425
|
+
},
|
|
1408
1426
|
{
|
|
1409
1427
|
"path": ".claude/skills/prototype/LOGIC.md",
|
|
1410
1428
|
"kind": "skill",
|
|
@@ -1653,7 +1671,7 @@
|
|
|
1653
1671
|
"kind": "skill",
|
|
1654
1672
|
"ownerSkill": "setup-workflow",
|
|
1655
1673
|
"surface": "claude",
|
|
1656
|
-
"sha256": "
|
|
1674
|
+
"sha256": "bc4db6413135ad9056cc63691cb823a9b198ad47538f292b37fcbb915477a716",
|
|
1657
1675
|
"mode": 420,
|
|
1658
1676
|
"origin": "kit"
|
|
1659
1677
|
},
|
|
@@ -2028,7 +2046,7 @@
|
|
|
2028
2046
|
{
|
|
2029
2047
|
"path": "scripts/kit-release.mjs",
|
|
2030
2048
|
"kind": "script",
|
|
2031
|
-
"sha256": "
|
|
2049
|
+
"sha256": "3c86eb5fc4b2071b423d87f023ba4c3f778ffe716a2603dcecdbe4e83ec1b8df",
|
|
2032
2050
|
"mode": 420,
|
|
2033
2051
|
"origin": "kit"
|
|
2034
2052
|
},
|
|
@@ -2102,6 +2120,13 @@
|
|
|
2102
2120
|
"mode": 420,
|
|
2103
2121
|
"origin": "kit"
|
|
2104
2122
|
},
|
|
2123
|
+
{
|
|
2124
|
+
"path": "scripts/project-release.mjs",
|
|
2125
|
+
"kind": "script",
|
|
2126
|
+
"sha256": "6c08cf16f97fdbc0172041c64109e859b1f073862e9f8edc2accbafd97b33843",
|
|
2127
|
+
"mode": 493,
|
|
2128
|
+
"origin": "kit"
|
|
2129
|
+
},
|
|
2105
2130
|
{
|
|
2106
2131
|
"path": "scripts/release-delta-guard.mjs",
|
|
2107
2132
|
"kind": "script",
|
|
@@ -2165,6 +2190,13 @@
|
|
|
2165
2190
|
"mode": 493,
|
|
2166
2191
|
"origin": "kit"
|
|
2167
2192
|
},
|
|
2193
|
+
{
|
|
2194
|
+
"path": "src/lib/release-apply.mjs",
|
|
2195
|
+
"kind": "script",
|
|
2196
|
+
"sha256": "d9b3bd07c88629c570d8a58f1d91d1cdac1457470cee973f99e186d9630efb03",
|
|
2197
|
+
"mode": 420,
|
|
2198
|
+
"origin": "kit"
|
|
2199
|
+
},
|
|
2168
2200
|
{
|
|
2169
2201
|
"path": "src/lib/release-preview.mjs",
|
|
2170
2202
|
"kind": "script",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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": {
|
package/scripts/kit-release.mjs
CHANGED
|
@@ -5,19 +5,15 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { applyProjectRelease } from '../src/lib/release-apply.mjs';
|
|
9
|
+
import { previewProjectRelease } from '../src/lib/release-preview.mjs';
|
|
10
|
+
import { nextVersion } from '../src/lib/semver.mjs';
|
|
8
11
|
import { buildKit } from './build-kit.mjs';
|
|
9
12
|
import { checkReleaseDelta } from './release-delta-guard.mjs';
|
|
10
13
|
|
|
11
14
|
const exec = promisify(execFile);
|
|
12
15
|
|
|
13
|
-
export
|
|
14
|
-
const parts = version.split('.').map(Number);
|
|
15
|
-
if (parts.length !== 3 || parts.some(Number.isNaN)) throw new Error(`invalid semver: ${version}`);
|
|
16
|
-
if (bump === 'major') return `${parts[0] + 1}.0.0`;
|
|
17
|
-
if (bump === 'minor') return `${parts[0]}.${parts[1] + 1}.0`;
|
|
18
|
-
if (bump === 'patch') return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
|
|
19
|
-
throw new Error(`invalid bump: ${bump}`);
|
|
20
|
-
}
|
|
16
|
+
export { nextVersion };
|
|
21
17
|
|
|
22
18
|
function note(version, delta) {
|
|
23
19
|
const lines = ['added', 'removed', 'changed'].flatMap((kind) =>
|
|
@@ -30,8 +26,17 @@ async function updateMetadata(repoRoot, targetVersion, delta) {
|
|
|
30
26
|
const pkg = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
31
27
|
const resumed = pkg.version === targetVersion;
|
|
32
28
|
if (!resumed) {
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const preview = await previewProjectRelease({
|
|
30
|
+
consumerRoot: repoRoot,
|
|
31
|
+
profile: { versionFiles: ['package.json'], tagPrefix: 'v' },
|
|
32
|
+
requestedVersion: targetVersion,
|
|
33
|
+
repositoryFacts: { dirtyPaths: [], existingTags: [] },
|
|
34
|
+
});
|
|
35
|
+
await applyProjectRelease({
|
|
36
|
+
consumerRoot: repoRoot,
|
|
37
|
+
preview,
|
|
38
|
+
confirmation: preview.confirmation,
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
const readmePath = join(repoRoot, 'README.md');
|
|
37
42
|
const readme = await readFile(readmePath, 'utf8');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { applyProjectRelease, assertSafeReleaseTargets } from '../src/lib/release-apply.mjs';
|
|
6
|
+
import { loadProjectReleaseProfile, previewProjectRelease } from '../src/lib/release-preview.mjs';
|
|
7
|
+
|
|
8
|
+
function gitOutput(consumerRoot, args) {
|
|
9
|
+
return execFileSync('git', args, { cwd: consumerRoot, encoding: 'utf8' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readRepositoryFacts(consumerRoot, run = gitOutput) {
|
|
13
|
+
const records = run(
|
|
14
|
+
consumerRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all'],
|
|
15
|
+
).split('\0').filter(Boolean);
|
|
16
|
+
const dirtyPaths = [];
|
|
17
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
18
|
+
const record = records[index];
|
|
19
|
+
dirtyPaths.push(record.slice(3));
|
|
20
|
+
if (record[0] === 'R' || record[0] === 'C'
|
|
21
|
+
|| record[1] === 'R' || record[1] === 'C') index += 1;
|
|
22
|
+
}
|
|
23
|
+
const existingTags = run(consumerRoot, ['tag', '--list'])
|
|
24
|
+
.split('\n').filter(Boolean);
|
|
25
|
+
return { dirtyPaths, existingTags };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function argument(args, name) {
|
|
29
|
+
const index = args.indexOf(name);
|
|
30
|
+
return index < 0 ? null : args[index + 1];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runProjectRelease(options) {
|
|
34
|
+
const {
|
|
35
|
+
consumerRoot, args,
|
|
36
|
+
repositoryFacts = readRepositoryFacts(consumerRoot),
|
|
37
|
+
output = console.log,
|
|
38
|
+
} = options;
|
|
39
|
+
const [command, requestedVersion] = args;
|
|
40
|
+
if (!['preview', 'apply'].includes(command) || !requestedVersion) {
|
|
41
|
+
throw new Error('usage: project-release <preview|apply> <patch|minor|major|version> [--confirm <token>]');
|
|
42
|
+
}
|
|
43
|
+
const profile = await loadProjectReleaseProfile(consumerRoot);
|
|
44
|
+
await assertSafeReleaseTargets(consumerRoot, profile.versionFiles);
|
|
45
|
+
const preview = await previewProjectRelease({
|
|
46
|
+
consumerRoot, profile, requestedVersion, repositoryFacts,
|
|
47
|
+
});
|
|
48
|
+
if (command === 'preview') {
|
|
49
|
+
output(JSON.stringify(preview));
|
|
50
|
+
return preview;
|
|
51
|
+
}
|
|
52
|
+
const result = await applyProjectRelease({
|
|
53
|
+
consumerRoot,
|
|
54
|
+
preview,
|
|
55
|
+
confirmation: argument(args, '--confirm'),
|
|
56
|
+
});
|
|
57
|
+
output(JSON.stringify(result));
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
await runProjectRelease({ consumerRoot: process.cwd(), args: process.argv.slice(2) });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) {
|
|
66
|
+
main().catch((error) => {
|
|
67
|
+
console.error(`project-release: ${error.message}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -15,8 +15,9 @@ Deny classes (each with a negative fixture below):
|
|
|
15
15
|
- residual issue refs (`#NNN`) / hard-rule refs (`HRn`)
|
|
16
16
|
- unresolvable provenance cross-refs (`ADR-####` / `Welle N` / `Slice N`),
|
|
17
17
|
with a documented fixture/example allowlist (PROVENANCE_FIXTURE_SUFFIXES)
|
|
18
|
-
- `../` cross-skill reaches (skills/scripts/docs —
|
|
19
|
-
|
|
18
|
+
- `../` cross-skill reaches (skills/scripts/docs — except a static top-level
|
|
19
|
+
script import into the shipped `src/lib` deep-module seam, and CLI `src`
|
|
20
|
+
imports which legitimately use `../lib/…`)
|
|
20
21
|
- bare owner/maintainer names OUTSIDE the generated credit files
|
|
21
22
|
- high-entropy secrets (after exempting the manifest's own sha256 file-hashes)
|
|
22
23
|
|
|
@@ -92,18 +93,27 @@ BARE_PRIVATE = [
|
|
|
92
93
|
("maintainer name", re.compile(r"\bNiko\b")),
|
|
93
94
|
]
|
|
94
95
|
PARENT_REF = re.compile(r"\.\./")
|
|
96
|
+
SHIPPED_SRC_IMPORT = re.compile(
|
|
97
|
+
r"^\s*import\b.*\bfrom\s+['\"]\.\./src/lib/[a-z0-9-]+\.mjs['\"];\s*$"
|
|
98
|
+
)
|
|
95
99
|
LONG_HEX = re.compile(r"\b[0-9a-f]{40,}\b")
|
|
96
100
|
GH_TOKEN = re.compile(r"\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{20,}")
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
def _parent_ref_scan_text(rel: str, text: str) -> str:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
lines = text.splitlines()
|
|
105
|
+
if rel == "scripts/loc_offender_core.py":
|
|
106
|
+
# This is the portable path-traversal guard itself, not a cross-skill reach.
|
|
107
|
+
lines = [
|
|
108
|
+
line for line in lines
|
|
109
|
+
if 'p.startswith("../")' not in line and '"/../" in p' not in line
|
|
110
|
+
]
|
|
111
|
+
if rel.startswith("scripts/") and rel.endswith(".mjs"):
|
|
112
|
+
# Top-level shipped scripts may consume the kit's shipped deep modules.
|
|
113
|
+
# The exact static-import shape stays inside the bundle root; every other
|
|
114
|
+
# parent reach remains visible to the deny rule below.
|
|
115
|
+
lines = [line for line in lines if not SHIPPED_SRC_IMPORT.match(line)]
|
|
116
|
+
return "\n".join(lines)
|
|
107
117
|
|
|
108
118
|
|
|
109
119
|
def _known_hashes(root: Path) -> set:
|
|
@@ -230,6 +240,15 @@ class AuditCatchesEachClass(unittest.TestCase):
|
|
|
230
240
|
def test_parent_ref(self):
|
|
231
241
|
self.assertTrue(any("parent" in v for v in self._body("[x](../../other/SKILL.md)")))
|
|
232
242
|
|
|
243
|
+
def test_static_script_import_into_shipped_src_lib_is_allowed(self):
|
|
244
|
+
script = self.dir / "scripts/release.mjs"
|
|
245
|
+
script.parent.mkdir()
|
|
246
|
+
script.write_text(
|
|
247
|
+
"import { run } from '../src/lib/release-core.mjs';\nrun();\n",
|
|
248
|
+
encoding="utf-8",
|
|
249
|
+
)
|
|
250
|
+
self.assertEqual(audit_dir(self.dir), [])
|
|
251
|
+
|
|
233
252
|
def test_bare_owner_in_body(self):
|
|
234
253
|
self.assertTrue(any("iKon85" in v for v in self._body("authored by iKon85")))
|
|
235
254
|
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -48,9 +48,12 @@ export const HELPER_FILES = [
|
|
|
48
48
|
{ path: 'scripts/release-parity.mjs', kind: 'script', mode: 0o644 },
|
|
49
49
|
{ path: 'scripts/release-state.mjs', kind: 'script', mode: 0o644 },
|
|
50
50
|
// Consumer-owned project release profiles use these read-only shared
|
|
51
|
-
// primitives before any apply/commit/tag action is allowed.
|
|
51
|
+
// primitives before any apply/commit/tag action is allowed. The thin CLI
|
|
52
|
+
// owns only repository-fact collection and delegates to the same engine.
|
|
52
53
|
{ path: 'src/lib/semver.mjs', kind: 'script', mode: 0o644 },
|
|
53
54
|
{ path: 'src/lib/release-preview.mjs', kind: 'script', mode: 0o644 },
|
|
55
|
+
{ path: 'src/lib/release-apply.mjs', kind: 'script', mode: 0o644 },
|
|
56
|
+
{ path: 'scripts/project-release.mjs', kind: 'script', mode: 0o755 },
|
|
54
57
|
// GitHub-consumer automation: invokes the existing update command, then owns
|
|
55
58
|
// only the stable tested branch/pull-request upsert.
|
|
56
59
|
{ path: 'scripts/kit-update-pr.mjs', kind: 'script', mode: 0o755 },
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { writeAtomic } from './atomicWrite.mjs';
|
|
5
|
+
|
|
6
|
+
const sha256 = (body) => createHash('sha256').update(body).digest('hex');
|
|
7
|
+
|
|
8
|
+
async function readSnapshot(consumerRoot, snapshot) {
|
|
9
|
+
return Promise.all(snapshot.map(async ({ path }) => {
|
|
10
|
+
const body = await readFile(join(consumerRoot, path), 'utf8');
|
|
11
|
+
return { path, body, version: JSON.parse(body).version, sha256: sha256(body) };
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const escapes = (root, target) => {
|
|
16
|
+
const path = relative(root, target);
|
|
17
|
+
return path === '..' || path.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)
|
|
18
|
+
|| isAbsolute(path);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function assertSafeReleaseTargets(consumerRoot, paths) {
|
|
22
|
+
const canonicalRoot = await realpath(consumerRoot);
|
|
23
|
+
for (const path of paths) {
|
|
24
|
+
const target = resolve(consumerRoot, path);
|
|
25
|
+
if (isAbsolute(path) || escapes(resolve(consumerRoot), target)) {
|
|
26
|
+
throw new Error(`release target is outside consumer root: ${path}`);
|
|
27
|
+
}
|
|
28
|
+
if ((await lstat(target)).isSymbolicLink()) {
|
|
29
|
+
throw new Error(`symlinked release target is not allowed: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
if (escapes(canonicalRoot, await realpath(target))) {
|
|
32
|
+
throw new Error(`release target resolves outside consumer root: ${path}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function applyProjectRelease(options) {
|
|
38
|
+
const { consumerRoot, preview, confirmation } = options;
|
|
39
|
+
const write = options.write ?? writeAtomic;
|
|
40
|
+
if (preview.status !== 'ready') throw new Error('release preview is blocked');
|
|
41
|
+
if (confirmation !== preview.confirmation) {
|
|
42
|
+
throw new Error('release confirmation does not match preview');
|
|
43
|
+
}
|
|
44
|
+
await assertSafeReleaseTargets(
|
|
45
|
+
consumerRoot, preview.snapshot.map(({ path }) => path),
|
|
46
|
+
);
|
|
47
|
+
const current = await readSnapshot(consumerRoot, preview.snapshot);
|
|
48
|
+
if (current.every(({ version }) => version === preview.summary.targetVersion)) {
|
|
49
|
+
throw new Error(`release already prepared at ${preview.summary.targetVersion}`);
|
|
50
|
+
}
|
|
51
|
+
for (const expected of preview.snapshot) {
|
|
52
|
+
const actual = current.find(({ path }) => path === expected.path);
|
|
53
|
+
if (actual.sha256 !== expected.sha256) {
|
|
54
|
+
throw new Error(`release target changed after preview: ${expected.path}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const version = preview.summary.targetVersion;
|
|
58
|
+
const candidates = current.map((file) => {
|
|
59
|
+
const body = JSON.parse(file.body);
|
|
60
|
+
body.version = version;
|
|
61
|
+
return { ...file, next: `${JSON.stringify(body, null, 2)}\n` };
|
|
62
|
+
});
|
|
63
|
+
const written = [];
|
|
64
|
+
try {
|
|
65
|
+
for (const file of candidates) {
|
|
66
|
+
await write(join(consumerRoot, file.path), file.next);
|
|
67
|
+
written.push(file);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
for (const file of written.reverse()) {
|
|
71
|
+
await writeAtomic(join(consumerRoot, file.path), file.body);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
status: 'prepared',
|
|
77
|
+
version,
|
|
78
|
+
updated: candidates.map(({ path }) => path),
|
|
79
|
+
plannedTag: preview.actions.find(({ type }) => type === 'tag').name,
|
|
80
|
+
};
|
|
81
|
+
}
|