release-skill 0.5.1 → 0.6.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +14 -14
- package/README.zh-CN.md +15 -15
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +3226 -565
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +401 -2
- package/adapters/claude/schemas/release-project.schema.json +291 -0
- package/adapters/claude/schemas/release-run.schema.json +59 -6
- package/adapters/claude/skills/release-config/SKILL.md +140 -0
- package/adapters/claude/skills/release-docs/SKILL.md +106 -0
- package/adapters/claude/skills/release-help/SKILL.md +39 -0
- package/adapters/claude/skills/release-marketplace/SKILL.md +147 -0
- package/adapters/claude/skills/release-publish/SKILL.md +25 -6
- package/adapters/claude/skills/release-verify/SKILL.md +10 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +3226 -565
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +401 -2
- package/adapters/codex/schemas/release-project.schema.json +291 -0
- package/adapters/codex/schemas/release-run.schema.json +59 -6
- package/adapters/codex/skills/release-config/SKILL.md +147 -0
- package/adapters/codex/skills/release-docs/SKILL.md +113 -0
- package/adapters/codex/skills/release-help/SKILL.md +39 -0
- package/adapters/codex/skills/release-marketplace/SKILL.md +154 -0
- package/adapters/codex/skills/release-publish/SKILL.md +25 -6
- package/adapters/codex/skills/release-verify/SKILL.md +10 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +3226 -565
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +401 -2
- package/adapters/kimi/schemas/release-project.schema.json +291 -0
- package/adapters/kimi/schemas/release-run.schema.json +59 -6
- package/adapters/kimi/skills/release-config/SKILL.md +147 -0
- package/adapters/kimi/skills/release-docs/SKILL.md +113 -0
- package/adapters/kimi/skills/release-help/SKILL.md +39 -0
- package/adapters/kimi/skills/release-marketplace/SKILL.md +154 -0
- package/adapters/kimi/skills/release-publish/SKILL.md +25 -6
- package/adapters/kimi/skills/release-verify/SKILL.md +10 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +3226 -565
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +401 -2
- package/adapters/workbuddy/schemas/release-project.schema.json +291 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +59 -6
- package/adapters/workbuddy/skills/release-config/SKILL.md +140 -0
- package/adapters/workbuddy/skills/release-docs/SKILL.md +106 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +39 -0
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +147 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +25 -6
- package/adapters/workbuddy/skills/release-verify/SKILL.md +10 -0
- package/bin/release-skill-cli.mjs +235 -7
- package/bin/release-skill.bundle.mjs +3226 -565
- package/package.json +3 -2
- package/platform-manifest.json +359 -0
- package/references/.render-manifest.json +9 -9
- package/references/02-project-config.md +35 -0
- package/references/05-evidence-and-errors.md +44 -0
- package/references/06-adapter-contract.md +13 -0
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +401 -2
- package/schemas/release-project.schema.json +291 -0
- package/schemas/release-run.schema.json +59 -6
- package/skills/release-config/SKILL.md +140 -0
- package/skills/release-docs/SKILL.md +106 -0
- package/skills/release-help/SKILL.md +39 -0
- package/skills/release-marketplace/SKILL.md +147 -0
- package/skills/release-publish/SKILL.md +25 -6
- package/skills/release-verify/SKILL.md +10 -0
- package/skills-src/release-config/SKILL.md +140 -0
- package/skills-src/release-docs/SKILL.md +106 -0
- package/skills-src/release-help/SKILL.md +39 -0
- package/skills-src/release-marketplace/SKILL.md +147 -0
- package/skills-src/release-publish/SKILL.md +25 -6
- package/skills-src/release-verify/SKILL.md +10 -0
- package/src/adapters/contract.mjs +5 -0
- package/src/adapters/distribute-git.mjs +625 -0
- package/src/commands/distribute.mjs +1078 -0
- package/src/commands/lineage.mjs +616 -0
- package/src/commands/prepare.mjs +335 -76
- package/src/commands/route.mjs +686 -0
- package/src/commands/ship.mjs +45 -1
- package/src/commands/verify.mjs +176 -0
- package/src/core/baseline-advance.mjs +185 -0
- package/src/core/checkpoints.mjs +25 -0
- package/src/core/postpublish.mjs +315 -0
package/src/commands/prepare.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { resolve, relative, isAbsolute, normalize, dirname } from 'node:path';
|
|
22
|
-
import { readFile, mkdir, realpath } from 'node:fs/promises';
|
|
22
|
+
import { readFile, mkdir, readdir, realpath } from 'node:fs/promises';
|
|
23
23
|
import { execFile as execFileCb } from 'node:child_process';
|
|
24
24
|
import { promisify } from 'node:util';
|
|
25
25
|
|
|
@@ -67,6 +67,7 @@ import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
|
67
67
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
68
68
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
69
69
|
import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
|
|
70
|
+
import { validatePostPublishDeclaration, PAYLOAD_SOURCE_TAG_WORKTREE } from '../core/postpublish.mjs';
|
|
70
71
|
|
|
71
72
|
// ---------------------------------------------------------------------------
|
|
72
73
|
// 安装契约常量
|
|
@@ -1693,6 +1694,43 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1693
1694
|
*
|
|
1694
1695
|
* @throws {ReleaseError} on any gate failure. No PREPARED plan is written.
|
|
1695
1696
|
*/
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Read the newest frozen plan from `.release-skill/plans/` (digest-addressed
|
|
1700
|
+
* immutable plans written by writePlanImmutable). Used by the config workflow
|
|
1701
|
+
* decision to compare public bytes (per-unit snapshotDigest) against the
|
|
1702
|
+
* latest frozen state. Returns null when no readable plan exists.
|
|
1703
|
+
*
|
|
1704
|
+
* @param {string} root - project root directory
|
|
1705
|
+
* @returns {Promise<{ plan: object, fileName: string } | null>}
|
|
1706
|
+
*/
|
|
1707
|
+
export async function readLatestFrozenPlan(root) {
|
|
1708
|
+
const plansDir = resolve(root, '.release-skill', 'plans');
|
|
1709
|
+
let files;
|
|
1710
|
+
try {
|
|
1711
|
+
files = await readdir(plansDir);
|
|
1712
|
+
} catch {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
let best = null;
|
|
1716
|
+
for (const file of files.sort()) {
|
|
1717
|
+
if (!file.endsWith('.json')) continue;
|
|
1718
|
+
let plan;
|
|
1719
|
+
try {
|
|
1720
|
+
plan = JSON.parse(await readFile(resolve(plansDir, file), 'utf8'));
|
|
1721
|
+
} catch {
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
if (!plan || typeof plan !== 'object' || !Array.isArray(plan.units)) continue;
|
|
1725
|
+
const ts = plan.createdAt ? new Date(plan.createdAt).getTime() : NaN;
|
|
1726
|
+
const effectiveTs = Number.isFinite(ts) ? ts : 0;
|
|
1727
|
+
if (!best || effectiveTs > best.ts) {
|
|
1728
|
+
best = { plan, fileName: file, ts: effectiveTs };
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return best ? { plan: best.plan, fileName: best.fileName } : null;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1696
1734
|
export async function prepareRelease(options) {
|
|
1697
1735
|
const {
|
|
1698
1736
|
root,
|
|
@@ -1704,9 +1742,31 @@ export async function prepareRelease(options) {
|
|
|
1704
1742
|
hooksAuthorized,
|
|
1705
1743
|
verificationGatesAuthorized,
|
|
1706
1744
|
production = false,
|
|
1745
|
+
workflow = 'full',
|
|
1707
1746
|
observePreviousPublicBaselineFn,
|
|
1708
1747
|
} = options ?? {};
|
|
1709
1748
|
|
|
1749
|
+
// --- Workflow profile (H5) ---
|
|
1750
|
+
// 'full' runs the complete gate set. 'docs', 'config' and 'marketplace'
|
|
1751
|
+
// trim only code-class gates — declared hooks, snapshot-verify gates,
|
|
1752
|
+
// source-authority closure, and skill-resource-closure — and record the
|
|
1753
|
+
// trim as `workflowDecision` bound into the plan digest. All other gates
|
|
1754
|
+
// (docs freshness, public surface, baseline, snapshots, remote checks,
|
|
1755
|
+
// plan freeze, consumer-verify gates) run identically for every workflow.
|
|
1756
|
+
const WORKFLOW_KINDS = new Set(['full', 'docs', 'config', 'marketplace']);
|
|
1757
|
+
if (!WORKFLOW_KINDS.has(workflow)) {
|
|
1758
|
+
throw new ReleaseError(
|
|
1759
|
+
CONFIG_INVALID,
|
|
1760
|
+
`unknown workflow kind "${workflow}"; expected one of ${[...WORKFLOW_KINDS].sort().join(', ')}`,
|
|
1761
|
+
{ workflow },
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
const trimmedWorkflow = workflow !== 'full';
|
|
1765
|
+
const skipDeclaredHooks = trimmedWorkflow;
|
|
1766
|
+
const skipSnapshotVerifyGates = trimmedWorkflow;
|
|
1767
|
+
const skipSourceAuthorityClosure = trimmedWorkflow;
|
|
1768
|
+
const skipSkillResourceClosure = trimmedWorkflow;
|
|
1769
|
+
|
|
1710
1770
|
// --- Validate root ---
|
|
1711
1771
|
if (!root || typeof root !== 'string') {
|
|
1712
1772
|
throw new ReleaseError(CONFIG_INVALID, 'root must be a non-empty string');
|
|
@@ -1785,6 +1845,28 @@ export async function prepareRelease(options) {
|
|
|
1785
1845
|
});
|
|
1786
1846
|
}
|
|
1787
1847
|
|
|
1848
|
+
// --- Step 1a: Workflow configuration evidence ---
|
|
1849
|
+
// Records the deterministic trim set for docs/config/marketplace
|
|
1850
|
+
// workflows. The trim never weakens the retained gates; it only removes
|
|
1851
|
+
// code-class gates that a non-code change surface cannot exercise.
|
|
1852
|
+
await evidence.append({
|
|
1853
|
+
phase: 'workflow',
|
|
1854
|
+
status: 'configured',
|
|
1855
|
+
workflowKind: workflow,
|
|
1856
|
+
trimmedGates: trimmedWorkflow
|
|
1857
|
+
? ['declared-hooks', 'snapshot-verify-gates', 'source-authority-closure', 'skill-resource-closure']
|
|
1858
|
+
: [],
|
|
1859
|
+
retainedGates: [
|
|
1860
|
+
'docs-freshness',
|
|
1861
|
+
'public-surface',
|
|
1862
|
+
'baseline',
|
|
1863
|
+
'snapshots',
|
|
1864
|
+
'remote-check',
|
|
1865
|
+
'plan-freeze',
|
|
1866
|
+
'consumer-verify',
|
|
1867
|
+
],
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1788
1870
|
// --- Step 1b: Resolve authoritative versions and gate release-document
|
|
1789
1871
|
// freshness BEFORE hook authorization ---
|
|
1790
1872
|
// Authoritative versions resolve exactly once here and are reused by
|
|
@@ -1811,11 +1893,46 @@ export async function prepareRelease(options) {
|
|
|
1811
1893
|
reasonTag: 'RELEASE_DOCS_STALE',
|
|
1812
1894
|
});
|
|
1813
1895
|
|
|
1896
|
+
// --- Step 1c: postPublish distribution declaration gate (R1/R2) ---
|
|
1897
|
+
// The per-unit postPublish block drives the post-publish distribute
|
|
1898
|
+
// command. Validate it here, before any hook, baseline, snapshot, remote
|
|
1899
|
+
// check, or plan write, so an unsafe declaration fails closed with zero
|
|
1900
|
+
// side effects. This is the runtime re-check on top of the config JSON
|
|
1901
|
+
// schema: plans frozen by older schema versions must not be able to
|
|
1902
|
+
// smuggle shell strings, option-like executables, or secret-ish env
|
|
1903
|
+
// keys through. A plan binds exactly one declaration; multiple units
|
|
1904
|
+
// declaring postPublish is a hard gate failure.
|
|
1905
|
+
const postPublishDeclarations = configUnits
|
|
1906
|
+
.map((unit, index) => ({ unit, index }))
|
|
1907
|
+
.filter(({ unit }) => unit.postPublish !== undefined);
|
|
1908
|
+
if (postPublishDeclarations.length > 1) {
|
|
1909
|
+
throw new ReleaseError(
|
|
1910
|
+
GATE_FAILED,
|
|
1911
|
+
`multiple units declare postPublish (${postPublishDeclarations.map(({ unit }) => unit.id).join(', ')}); a release plan binds exactly one postPublish declaration`,
|
|
1912
|
+
{ unitIds: postPublishDeclarations.map(({ unit }) => unit.id) },
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
let postPublishDeclaration = null;
|
|
1916
|
+
if (postPublishDeclarations.length === 1) {
|
|
1917
|
+
const { unit, index } = postPublishDeclarations[0];
|
|
1918
|
+
validatePostPublishDeclaration(unit.postPublish, { unitId: unit.id });
|
|
1919
|
+
postPublishDeclaration = { unit, index };
|
|
1920
|
+
await evidence.append({
|
|
1921
|
+
phase: 'postpublish-declaration',
|
|
1922
|
+
status: 'validated',
|
|
1923
|
+
unitId: unit.id,
|
|
1924
|
+
targetCount: unit.postPublish.targets.length,
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1814
1928
|
// --- Step 2: Hook authorization gate ---
|
|
1815
1929
|
// Hooks are user-configured arbitrary local processes without filesystem
|
|
1816
1930
|
// or network isolation. They may write outside the project, access local
|
|
1817
1931
|
// credentials, or make network calls. The user must explicitly accept
|
|
1818
1932
|
// these risks before any hook is executed.
|
|
1933
|
+
// docs/config/marketplace workflows trim code-class hooks entirely
|
|
1934
|
+
// (H5): a non-code change surface cannot exercise them, so they are
|
|
1935
|
+
// recorded as skipped rather than authorized-and-run.
|
|
1819
1936
|
const declaredHooks = Object.entries(config.hooks ?? {})
|
|
1820
1937
|
.filter(([, hook]) => hook && hook.command)
|
|
1821
1938
|
.map(([name, hook]) => ({
|
|
@@ -1829,7 +1946,7 @@ export async function prepareRelease(options) {
|
|
|
1829
1946
|
// execution of configured commands. Old acknowledgement parameters
|
|
1830
1947
|
// (--acknowledge-hook-side-effects, --acknowledge-gate-side-effects) are
|
|
1831
1948
|
// accepted as no-effect compatibility inputs but are not required.
|
|
1832
|
-
if (declaredHooks.length > 0) {
|
|
1949
|
+
if (!skipDeclaredHooks && declaredHooks.length > 0) {
|
|
1833
1950
|
await evidence.append({
|
|
1834
1951
|
phase: 'hook-authorization',
|
|
1835
1952
|
status: 'authorized',
|
|
@@ -1857,16 +1974,24 @@ export async function prepareRelease(options) {
|
|
|
1857
1974
|
}
|
|
1858
1975
|
|
|
1859
1976
|
// --- Step 3: Run declared hooks ---
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1977
|
+
if (skipDeclaredHooks) {
|
|
1978
|
+
await evidence.append({
|
|
1979
|
+
phase: 'hooks',
|
|
1980
|
+
status: 'skipped',
|
|
1981
|
+
reason: `workflow "${workflow}" trims code-class hooks; declared hooks: ${declaredHooks.length}`,
|
|
1982
|
+
});
|
|
1983
|
+
} else {
|
|
1984
|
+
await evidence.append({ phase: 'hooks', status: 'started' });
|
|
1985
|
+
await runDeclaredHooks(config, realRoot, evidence, options.runHookFn ?? runHook, {
|
|
1986
|
+
hookCache: options.hookCache,
|
|
1987
|
+
// Explicit env delivery (0.5.1 hook-env-delivery fix): the hook runner
|
|
1988
|
+
// reads envAllowlist keys exclusively from context.env, so the invoking
|
|
1989
|
+
// shell's environment is injected here explicitly. Allowlist semantics
|
|
1990
|
+
// are unchanged — only allowlisted keys from this map reach the child.
|
|
1991
|
+
env: options.env ?? process.env,
|
|
1992
|
+
});
|
|
1993
|
+
await evidence.append({ phase: 'hooks', status: 'completed' });
|
|
1994
|
+
}
|
|
1870
1995
|
|
|
1871
1996
|
// --- Step 3b: Re-check release-document freshness AFTER hooks ---
|
|
1872
1997
|
// Declared hooks run as arbitrary local processes; they may rewrite a
|
|
@@ -1968,7 +2093,7 @@ export async function prepareRelease(options) {
|
|
|
1968
2093
|
|
|
1969
2094
|
let sourceAuthority = null;
|
|
1970
2095
|
let sourceInputClosure = null;
|
|
1971
|
-
if (production) {
|
|
2096
|
+
if (production && !skipSourceAuthorityClosure) {
|
|
1972
2097
|
if (!sourceRepository || typeof sourceRepository !== 'string') {
|
|
1973
2098
|
throw new ReleaseError(
|
|
1974
2099
|
CONFIG_MISSING,
|
|
@@ -2038,6 +2163,12 @@ export async function prepareRelease(options) {
|
|
|
2038
2163
|
inputDigest: sourceInputClosure.digest,
|
|
2039
2164
|
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2040
2165
|
});
|
|
2166
|
+
} else if (production) {
|
|
2167
|
+
await evidence.append({
|
|
2168
|
+
phase: 'source-authority',
|
|
2169
|
+
status: 'skipped',
|
|
2170
|
+
reason: `workflow "${workflow}" trims the source-authority content closure gate`,
|
|
2171
|
+
});
|
|
2041
2172
|
}
|
|
2042
2173
|
|
|
2043
2174
|
// --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
|
|
@@ -2358,24 +2489,38 @@ export async function prepareRelease(options) {
|
|
|
2358
2489
|
// Snapshot gates always run on disposable writable copies. The public
|
|
2359
2490
|
// snapshot authority is re-digested after every gate and is never exposed
|
|
2360
2491
|
// as the gate working directory.
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2492
|
+
// docs/config/marketplace workflows trim snapshot-verify gates (H5):
|
|
2493
|
+
// they are declared code-class gates that a non-code change surface
|
|
2494
|
+
// cannot exercise. Consumer-verify gates (phase 'consumer-verify') are
|
|
2495
|
+
// NOT part of this skip — they run during verify regardless of workflow.
|
|
2496
|
+
if (skipSnapshotVerifyGates) {
|
|
2497
|
+
await evidence.append({
|
|
2498
|
+
phase: 'snapshot-verify',
|
|
2499
|
+
status: 'skipped',
|
|
2500
|
+
reason: `workflow "${workflow}" trims snapshot-verify gates; declared gates: ${declaredVerificationGates.length}`,
|
|
2501
|
+
});
|
|
2502
|
+
} else {
|
|
2503
|
+
const snapshotGateResults = await runSnapshotVerificationGates({
|
|
2504
|
+
gates: declaredVerificationGates,
|
|
2505
|
+
unitResults,
|
|
2506
|
+
runDir,
|
|
2507
|
+
evidence,
|
|
2508
|
+
env: options.gateEnv ?? process.env,
|
|
2509
|
+
});
|
|
2510
|
+
await evidence.append({
|
|
2511
|
+
phase: 'snapshot-verify',
|
|
2512
|
+
status: 'completed',
|
|
2513
|
+
gateCount: snapshotGateResults.length,
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2373
2516
|
|
|
2374
2517
|
// Bind the remote source-authority proof to the exact bytes that entered
|
|
2375
2518
|
// the frozen snapshots, not merely to an earlier read of the workspace.
|
|
2376
2519
|
// Then re-read the complete closure and dirty state once more so version
|
|
2377
2520
|
// sources and non-snapshot closure entries cannot drift during prepare.
|
|
2378
|
-
|
|
2521
|
+
// Trimmed together with Step 3c when a docs/config/marketplace workflow
|
|
2522
|
+
// skips the source-authority closure.
|
|
2523
|
+
if (production && !skipSourceAuthorityClosure) {
|
|
2379
2524
|
const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
|
|
2380
2525
|
closure: sourceInputClosure,
|
|
2381
2526
|
unitResults,
|
|
@@ -2502,59 +2647,72 @@ export async function prepareRelease(options) {
|
|
|
2502
2647
|
// publish will re-verify, rather than the writable pre-freeze staging tree.
|
|
2503
2648
|
// Non-production plans scan the final staging tree at the same point.
|
|
2504
2649
|
// This gate is built in, read-only, and cannot be disabled by overlays.
|
|
2650
|
+
// docs/config/marketplace workflows trim it (H5): the plan omits
|
|
2651
|
+
// `skillResourceClosure` entirely, so publish's conditional re-check
|
|
2652
|
+
// (guarded by plan.skillResourceClosure) and verify's receipt comparison
|
|
2653
|
+
// both skip it consistently. The trim is recorded in workflowDecision.
|
|
2505
2654
|
const skillResourceClosureResults = [];
|
|
2506
|
-
|
|
2655
|
+
if (skipSkillResourceClosure) {
|
|
2507
2656
|
await evidence.append({
|
|
2508
2657
|
phase: 'skill-resource-closure',
|
|
2509
|
-
status: '
|
|
2510
|
-
|
|
2658
|
+
status: 'skipped',
|
|
2659
|
+
reason: `workflow "${workflow}" trims the skill resource closure gate`,
|
|
2660
|
+
unitCount: unitResults.length,
|
|
2511
2661
|
});
|
|
2662
|
+
} else {
|
|
2663
|
+
for (const { unit, manifest } of unitResults) {
|
|
2664
|
+
await evidence.append({
|
|
2665
|
+
phase: 'skill-resource-closure',
|
|
2666
|
+
status: 'started',
|
|
2667
|
+
unitId: unit.id,
|
|
2668
|
+
});
|
|
2512
2669
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2670
|
+
const closureResult = await checkSkillResourceClosure({
|
|
2671
|
+
snapshotDir: manifest.outputDir,
|
|
2672
|
+
host: 'root',
|
|
2673
|
+
});
|
|
2517
2674
|
|
|
2518
|
-
|
|
2519
|
-
|
|
2675
|
+
const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
|
|
2676
|
+
skillResourceClosureResults.push(receipt);
|
|
2677
|
+
|
|
2678
|
+
if (closureResult.findings.length > 0) {
|
|
2679
|
+
await evidence.append({
|
|
2680
|
+
phase: 'skill-resource-closure',
|
|
2681
|
+
status: 'blocking',
|
|
2682
|
+
unitId: unit.id,
|
|
2683
|
+
findingCount: closureResult.findings.length,
|
|
2684
|
+
findings: closureResult.findings.map((f) => ({
|
|
2685
|
+
skill: f.skill,
|
|
2686
|
+
line: f.line,
|
|
2687
|
+
reference: f.reference,
|
|
2688
|
+
classification: f.classification,
|
|
2689
|
+
code: f.code,
|
|
2690
|
+
})),
|
|
2691
|
+
});
|
|
2692
|
+
throw new ReleaseError(
|
|
2693
|
+
GATE_FAILED,
|
|
2694
|
+
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2695
|
+
{
|
|
2696
|
+
unitId: unit.id,
|
|
2697
|
+
findingCount: closureResult.findings.length,
|
|
2698
|
+
findings: closureResult.findings,
|
|
2699
|
+
},
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2520
2702
|
|
|
2521
|
-
if (closureResult.findings.length > 0) {
|
|
2522
2703
|
await evidence.append({
|
|
2523
2704
|
phase: 'skill-resource-closure',
|
|
2524
|
-
status: '
|
|
2705
|
+
status: 'completed',
|
|
2525
2706
|
unitId: unit.id,
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
})),
|
|
2707
|
+
checkerVersion: closureResult.checkerVersion,
|
|
2708
|
+
surfaceCount: receipt.surfaceCount,
|
|
2709
|
+
skillCount: receipt.skillCount,
|
|
2710
|
+
referenceCount: closureResult.referenceCount,
|
|
2711
|
+
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2712
|
+
findingCount: 0,
|
|
2713
|
+
receiptDigest: closureResult.receiptDigest,
|
|
2534
2714
|
});
|
|
2535
|
-
throw new ReleaseError(
|
|
2536
|
-
GATE_FAILED,
|
|
2537
|
-
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2538
|
-
{
|
|
2539
|
-
unitId: unit.id,
|
|
2540
|
-
findingCount: closureResult.findings.length,
|
|
2541
|
-
findings: closureResult.findings,
|
|
2542
|
-
},
|
|
2543
|
-
);
|
|
2544
2715
|
}
|
|
2545
|
-
|
|
2546
|
-
await evidence.append({
|
|
2547
|
-
phase: 'skill-resource-closure',
|
|
2548
|
-
status: 'completed',
|
|
2549
|
-
unitId: unit.id,
|
|
2550
|
-
checkerVersion: closureResult.checkerVersion,
|
|
2551
|
-
surfaceCount: receipt.surfaceCount,
|
|
2552
|
-
skillCount: receipt.skillCount,
|
|
2553
|
-
referenceCount: closureResult.referenceCount,
|
|
2554
|
-
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2555
|
-
findingCount: 0,
|
|
2556
|
-
receiptDigest: closureResult.receiptDigest,
|
|
2557
|
-
});
|
|
2558
2716
|
}
|
|
2559
2717
|
|
|
2560
2718
|
// Freeze external independent marketplace HEADs (production + online only):
|
|
@@ -2887,20 +3045,114 @@ export async function prepareRelease(options) {
|
|
|
2887
3045
|
// 构建冻结分发映射:unitId -> frozen distributions
|
|
2888
3046
|
const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
|
|
2889
3047
|
|
|
2890
|
-
|
|
3048
|
+
let externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
|
|
2891
3049
|
|
|
2892
3050
|
// Compute overall snapshot digest
|
|
2893
3051
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
2894
3052
|
|
|
3053
|
+
// --- Step 6b: Workflow decision (H5) ---
|
|
3054
|
+
// config workflow: compare per-unit snapshotDigests with the latest
|
|
3055
|
+
// frozen plan. Identical public bytes → no publish path (all publish-class
|
|
3056
|
+
// external actions are dropped from the plan, so approve/publish execute
|
|
3057
|
+
// nothing). No comparable plan or any byte difference → publish-needed
|
|
3058
|
+
// (fail-safe). The decision is bound into the plan digest as
|
|
3059
|
+
// `workflowDecision`, making the trim immutable and auditable.
|
|
3060
|
+
let workflowDecision = null;
|
|
3061
|
+
if (workflow === 'config') {
|
|
3062
|
+
const previousPlan = await readLatestFrozenPlan(realRoot);
|
|
3063
|
+
let publishPath;
|
|
3064
|
+
let decision;
|
|
3065
|
+
if (!previousPlan) {
|
|
3066
|
+
publishPath = 'publish-needed';
|
|
3067
|
+
decision = 'indeterminable';
|
|
3068
|
+
} else {
|
|
3069
|
+
const prevDigests = new Map(
|
|
3070
|
+
(previousPlan.plan.units ?? []).map((u) => [u.id, u.snapshotDigest]),
|
|
3071
|
+
);
|
|
3072
|
+
const unchanged =
|
|
3073
|
+
units.length > 0 && units.every((u) => prevDigests.get(u.id) === u.snapshotDigest);
|
|
3074
|
+
publishPath = unchanged ? 'no-publish-needed' : 'publish-needed';
|
|
3075
|
+
decision = unchanged ? 'public-bytes-unchanged' : 'public-bytes-changed';
|
|
3076
|
+
}
|
|
3077
|
+
workflowDecision = {
|
|
3078
|
+
workflowKind: workflow,
|
|
3079
|
+
decision,
|
|
3080
|
+
publishPath,
|
|
3081
|
+
trimmedGates: [
|
|
3082
|
+
'declared-hooks',
|
|
3083
|
+
'snapshot-verify-gates',
|
|
3084
|
+
'source-authority-closure',
|
|
3085
|
+
'skill-resource-closure',
|
|
3086
|
+
],
|
|
3087
|
+
...(previousPlan ? { comparedPlan: previousPlan.fileName } : {}),
|
|
3088
|
+
};
|
|
3089
|
+
if (publishPath === 'no-publish-needed') {
|
|
3090
|
+
externalActions = [];
|
|
3091
|
+
}
|
|
3092
|
+
await evidence.append({
|
|
3093
|
+
phase: 'workflow-decision',
|
|
3094
|
+
status: publishPath,
|
|
3095
|
+
decision,
|
|
3096
|
+
...(previousPlan ? { comparedPlan: previousPlan.fileName } : {}),
|
|
3097
|
+
actionCount: externalActions.length,
|
|
3098
|
+
});
|
|
3099
|
+
} else if (trimmedWorkflow) {
|
|
3100
|
+
workflowDecision = {
|
|
3101
|
+
workflowKind: workflow,
|
|
3102
|
+
decision: 'code-gates-trimmed',
|
|
3103
|
+
publishPath: 'publish-needed',
|
|
3104
|
+
trimmedGates: [
|
|
3105
|
+
'declared-hooks',
|
|
3106
|
+
'snapshot-verify-gates',
|
|
3107
|
+
'source-authority-closure',
|
|
3108
|
+
'skill-resource-closure',
|
|
3109
|
+
],
|
|
3110
|
+
};
|
|
3111
|
+
await evidence.append({
|
|
3112
|
+
phase: 'workflow-decision',
|
|
3113
|
+
status: 'publish-needed',
|
|
3114
|
+
decision: 'code-gates-trimmed',
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
|
|
2895
3118
|
// Detect human consumer platforms (Kimi/CodeBuddy) in the plan.
|
|
2896
3119
|
// When present, new plans mark them as non-blocking manual follow-up tasks.
|
|
2897
3120
|
const hasHumanConsumerActions = externalActions.some(
|
|
2898
3121
|
(a) => a.type === 'kimi-marketplace-install' || a.type === 'codebuddy-marketplace-install',
|
|
2899
3122
|
);
|
|
2900
3123
|
|
|
3124
|
+
// --- Fold the validated postPublish declaration into the frozen plan ---
|
|
3125
|
+
// Bindings: tag (tagTemplate rendered at the resolved target version —
|
|
3126
|
+
// the same computation create-tag will use), tagCommit (the frozen
|
|
3127
|
+
// production asset commit the tag will point at; only production
|
|
3128
|
+
// prepares can freeze it — distribute fails closed when it is absent),
|
|
3129
|
+
// unitId (declaring unit), and payloadSource "tag-worktree" (R1 timing
|
|
3130
|
+
// contract: payload may only come from the detached worktree at
|
|
3131
|
+
// tagCommit, never from workspace state). planVersion 2 record-layer
|
|
3132
|
+
// stripping does not strip this block, so every declaration detail is
|
|
3133
|
+
// bound into the plan digest.
|
|
3134
|
+
let frozenPostPublish = null;
|
|
3135
|
+
if (postPublishDeclaration) {
|
|
3136
|
+
const { unit, index } = postPublishDeclaration;
|
|
3137
|
+
const { tag } = resolveProductionBranch(unit, resolvedVersions[index]);
|
|
3138
|
+
frozenPostPublish = {
|
|
3139
|
+
...structuredClone(unit.postPublish),
|
|
3140
|
+
tag,
|
|
3141
|
+
...(productionAssets ? { tagCommit: productionAssets[index].commit } : {}),
|
|
3142
|
+
unitId: unit.id,
|
|
3143
|
+
payloadSource: PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
|
|
2901
3147
|
const plan = {
|
|
2902
3148
|
planVersion: 2,
|
|
2903
3149
|
status: 'PREPARED',
|
|
3150
|
+
// Workflow profile (H5): 'full' for the complete gate set;
|
|
3151
|
+
// 'docs'/'config'/'marketplace' for trimmed code-class gates. Both
|
|
3152
|
+
// fields are binding-layer (not stripped from the plan digest), so the
|
|
3153
|
+
// trim is frozen immutably with the plan.
|
|
3154
|
+
workflowKind: workflow,
|
|
3155
|
+
...(workflowDecision ? { workflowDecision } : {}),
|
|
2904
3156
|
baseline: {
|
|
2905
3157
|
gitTreeHash: baseline.gitTreeHash,
|
|
2906
3158
|
headCommit: baseline.gitHead,
|
|
@@ -2910,14 +3162,18 @@ export async function prepareRelease(options) {
|
|
|
2910
3162
|
capturedAt: baseline.capturedAt,
|
|
2911
3163
|
},
|
|
2912
3164
|
configDigest,
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3165
|
+
...(skillResourceClosureResults.length > 0
|
|
3166
|
+
? {
|
|
3167
|
+
skillResourceClosure: {
|
|
3168
|
+
checkerVersion: SKILL_RESOURCE_CHECKER_VERSION,
|
|
3169
|
+
unitReceipts: skillResourceClosureResults,
|
|
3170
|
+
totalSkillCount: skillResourceClosureResults.reduce((sum, item) => sum + item.skillCount, 0),
|
|
3171
|
+
totalReferenceCount: skillResourceClosureResults.reduce((sum, item) => sum + item.referenceCount, 0),
|
|
3172
|
+
totalSourceOnlyCount: skillResourceClosureResults.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
|
|
3173
|
+
totalFindingCount: 0,
|
|
3174
|
+
},
|
|
3175
|
+
}
|
|
3176
|
+
: {}),
|
|
2921
3177
|
verificationGates: config.verificationGates ?? [],
|
|
2922
3178
|
snapshotDigest: overallSnapshotDigest,
|
|
2923
3179
|
...(hasHumanConsumerActions ? { humanConsumersStrategy: 'manualFollowUps' } : {}),
|
|
@@ -2929,6 +3185,7 @@ export async function prepareRelease(options) {
|
|
|
2929
3185
|
} : {}),
|
|
2930
3186
|
units,
|
|
2931
3187
|
externalActions,
|
|
3188
|
+
...(frozenPostPublish ? { postPublish: frozenPostPublish } : {}),
|
|
2932
3189
|
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
2933
3190
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
2934
3191
|
};
|
|
@@ -2968,6 +3225,8 @@ export async function prepareRelease(options) {
|
|
|
2968
3225
|
unitCount: units.length,
|
|
2969
3226
|
actionCount: externalActions.length,
|
|
2970
3227
|
offline,
|
|
3228
|
+
workflowKind: workflow,
|
|
3229
|
+
...(workflowDecision ? { workflowDecision } : {}),
|
|
2971
3230
|
completedAt: (clock ? clock() : new Date().toISOString()),
|
|
2972
3231
|
});
|
|
2973
3232
|
|