release-skill 0.5.0 → 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 +31 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +13 -21
- package/README.zh-CN.md +14 -22
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/kernel-protocol.json +1 -1
- package/adapters/claude/bin/registry.json +15 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/claude/bin/rules.json +1 -1
- 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/error-codes.json +1 -1
- package/adapters/codex/bin/kernel-protocol.json +1 -1
- package/adapters/codex/bin/registry.json +15 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/codex/bin/rules.json +1 -1
- 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/error-codes.json +1 -1
- package/adapters/kimi/bin/kernel-protocol.json +1 -1
- package/adapters/kimi/bin/registry.json +15 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/kimi/bin/rules.json +1 -1
- 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/error-codes.json +1 -1
- package/adapters/workbuddy/bin/kernel-protocol.json +1 -1
- package/adapters/workbuddy/bin/registry.json +15 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/workbuddy/bin/rules.json +1 -1
- 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/error-codes.json +1 -1
- package/bin/kernel-protocol.json +1 -1
- package/bin/registry.json +15 -1
- package/bin/release-skill-cli.mjs +235 -7
- package/bin/release-skill.bundle.mjs +38147 -23284
- package/bin/rules.json +1 -1
- package/package.json +5 -4
- 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/adapters/plugin-marketplace.mjs +48 -4
- package/src/adapters/push-snapshot.mjs +12 -4
- package/src/commands/assess.mjs +171 -0
- package/src/commands/distribute.mjs +1078 -0
- package/src/commands/hooks.mjs +6 -1
- package/src/commands/lineage.mjs +616 -0
- package/src/commands/prepare.mjs +413 -72
- 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/baseline.mjs +11 -1
- package/src/core/checkpoints.mjs +25 -0
- package/src/core/foundation-inflight.mjs +7 -0
- package/src/core/plan.mjs +12 -2
- 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
|
// 安装契约常量
|
|
@@ -243,6 +244,13 @@ export async function resolveAllUnitVersions(units, root, explicitVersion, evide
|
|
|
243
244
|
* @param {object} [options]
|
|
244
245
|
* @param {boolean} [options.hookCache=true] - When false (--no-hook-cache),
|
|
245
246
|
* every hook runs in full and the cache is neither read nor written.
|
|
247
|
+
* @param {Record<string, string>} [options.env] - Explicit environment map
|
|
248
|
+
* merged into the hook context (`hookFn(hook, { root, env })`). The hook
|
|
249
|
+
* runner's `buildFilteredEnv` reads `envAllowlist` keys exclusively from
|
|
250
|
+
* this map (never from process.env), so the caller decides what is
|
|
251
|
+
* injectable. Defaults to process.env at the prepare call site, which makes
|
|
252
|
+
* allowlisted keys exported by the invoking shell reach the hook
|
|
253
|
+
* subprocess.
|
|
246
254
|
* @returns {Promise<void>}
|
|
247
255
|
* @throws {ReleaseError} GATE_FAILED if any hook returns a non-zero exit code,
|
|
248
256
|
* throws, or declares a cacheInputs glob that matches no file.
|
|
@@ -294,7 +302,10 @@ export async function runDeclaredHooks(config, root, evidence, hookFn = runHook,
|
|
|
294
302
|
|
|
295
303
|
let result;
|
|
296
304
|
try {
|
|
297
|
-
result = await hookFn(hook, {
|
|
305
|
+
result = await hookFn(hook, {
|
|
306
|
+
root,
|
|
307
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
308
|
+
});
|
|
298
309
|
} catch (err) {
|
|
299
310
|
await evidence.append({
|
|
300
311
|
phase: 'hooks',
|
|
@@ -1683,6 +1694,43 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1683
1694
|
*
|
|
1684
1695
|
* @throws {ReleaseError} on any gate failure. No PREPARED plan is written.
|
|
1685
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
|
+
|
|
1686
1734
|
export async function prepareRelease(options) {
|
|
1687
1735
|
const {
|
|
1688
1736
|
root,
|
|
@@ -1694,9 +1742,31 @@ export async function prepareRelease(options) {
|
|
|
1694
1742
|
hooksAuthorized,
|
|
1695
1743
|
verificationGatesAuthorized,
|
|
1696
1744
|
production = false,
|
|
1745
|
+
workflow = 'full',
|
|
1697
1746
|
observePreviousPublicBaselineFn,
|
|
1698
1747
|
} = options ?? {};
|
|
1699
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
|
+
|
|
1700
1770
|
// --- Validate root ---
|
|
1701
1771
|
if (!root || typeof root !== 'string') {
|
|
1702
1772
|
throw new ReleaseError(CONFIG_INVALID, 'root must be a non-empty string');
|
|
@@ -1775,6 +1845,28 @@ export async function prepareRelease(options) {
|
|
|
1775
1845
|
});
|
|
1776
1846
|
}
|
|
1777
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
|
+
|
|
1778
1870
|
// --- Step 1b: Resolve authoritative versions and gate release-document
|
|
1779
1871
|
// freshness BEFORE hook authorization ---
|
|
1780
1872
|
// Authoritative versions resolve exactly once here and are reused by
|
|
@@ -1801,11 +1893,46 @@ export async function prepareRelease(options) {
|
|
|
1801
1893
|
reasonTag: 'RELEASE_DOCS_STALE',
|
|
1802
1894
|
});
|
|
1803
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
|
+
|
|
1804
1928
|
// --- Step 2: Hook authorization gate ---
|
|
1805
1929
|
// Hooks are user-configured arbitrary local processes without filesystem
|
|
1806
1930
|
// or network isolation. They may write outside the project, access local
|
|
1807
1931
|
// credentials, or make network calls. The user must explicitly accept
|
|
1808
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.
|
|
1809
1936
|
const declaredHooks = Object.entries(config.hooks ?? {})
|
|
1810
1937
|
.filter(([, hook]) => hook && hook.command)
|
|
1811
1938
|
.map(([name, hook]) => ({
|
|
@@ -1819,7 +1946,7 @@ export async function prepareRelease(options) {
|
|
|
1819
1946
|
// execution of configured commands. Old acknowledgement parameters
|
|
1820
1947
|
// (--acknowledge-hook-side-effects, --acknowledge-gate-side-effects) are
|
|
1821
1948
|
// accepted as no-effect compatibility inputs but are not required.
|
|
1822
|
-
if (declaredHooks.length > 0) {
|
|
1949
|
+
if (!skipDeclaredHooks && declaredHooks.length > 0) {
|
|
1823
1950
|
await evidence.append({
|
|
1824
1951
|
phase: 'hook-authorization',
|
|
1825
1952
|
status: 'authorized',
|
|
@@ -1847,11 +1974,24 @@ export async function prepareRelease(options) {
|
|
|
1847
1974
|
}
|
|
1848
1975
|
|
|
1849
1976
|
// --- Step 3: Run declared hooks ---
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
+
}
|
|
1855
1995
|
|
|
1856
1996
|
// --- Step 3b: Re-check release-document freshness AFTER hooks ---
|
|
1857
1997
|
// Declared hooks run as arbitrary local processes; they may rewrite a
|
|
@@ -1953,7 +2093,7 @@ export async function prepareRelease(options) {
|
|
|
1953
2093
|
|
|
1954
2094
|
let sourceAuthority = null;
|
|
1955
2095
|
let sourceInputClosure = null;
|
|
1956
|
-
if (production) {
|
|
2096
|
+
if (production && !skipSourceAuthorityClosure) {
|
|
1957
2097
|
if (!sourceRepository || typeof sourceRepository !== 'string') {
|
|
1958
2098
|
throw new ReleaseError(
|
|
1959
2099
|
CONFIG_MISSING,
|
|
@@ -2023,6 +2163,12 @@ export async function prepareRelease(options) {
|
|
|
2023
2163
|
inputDigest: sourceInputClosure.digest,
|
|
2024
2164
|
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2025
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
|
+
});
|
|
2026
2172
|
}
|
|
2027
2173
|
|
|
2028
2174
|
// --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
|
|
@@ -2077,6 +2223,29 @@ export async function prepareRelease(options) {
|
|
|
2077
2223
|
}
|
|
2078
2224
|
};
|
|
2079
2225
|
const observeDefaultBranch = options.observeDefaultBranchFn ?? defaultObserveDefaultBranchFn;
|
|
2226
|
+
// CHAIN_GAP detection (0.5.1 chain hardening): when a unit declares
|
|
2227
|
+
// previousPublicBaseline.mode=none (first-release semantics) but the
|
|
2228
|
+
// public repository already carries release tags for the unit's
|
|
2229
|
+
// tagTemplate, a subsequent release pretending to be the first would
|
|
2230
|
+
// freeze a plan whose push-snapshot creates a new orphan root commit.
|
|
2231
|
+
// Detection is online-only: `git ls-remote` on the tag pattern
|
|
2232
|
+
// (tagTemplate with {version} → *). Any match proves a prior release.
|
|
2233
|
+
const defaultPriorReleaseTagDetector = async (repo, tagPattern, { githubHost = 'github.com' } = {}) => {
|
|
2234
|
+
try {
|
|
2235
|
+
const { stdout } = await execFile(
|
|
2236
|
+
'git',
|
|
2237
|
+
['ls-remote', `https://${githubHost}/${repo}.git`, `refs/tags/${tagPattern}`],
|
|
2238
|
+
{ shell: false, encoding: 'utf8', timeout: 30000 },
|
|
2239
|
+
);
|
|
2240
|
+
return { found: stdout.trim().length > 0 };
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
// Network/auth failure: unknown, not "no prior release". The caller
|
|
2243
|
+
// records a warning and continues (consistent with the bound-baseline
|
|
2244
|
+
// observer's unknown status); it never silently proves first release.
|
|
2245
|
+
return { error: err.message };
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
const detectPriorReleaseTags = options.detectPriorReleaseTagsFn ?? defaultPriorReleaseTagDetector;
|
|
2080
2249
|
const unitBaselineResults = new Map();
|
|
2081
2250
|
for (let unitIndex = 0; unitIndex < configUnits.length; unitIndex += 1) {
|
|
2082
2251
|
const unit = configUnits[unitIndex];
|
|
@@ -2120,6 +2289,50 @@ export async function prepareRelease(options) {
|
|
|
2120
2289
|
});
|
|
2121
2290
|
|
|
2122
2291
|
if (ppbConfig.mode === "none") {
|
|
2292
|
+
// Chain-integrity gate (0.5.1, CHAIN_GAP): a production online
|
|
2293
|
+
// prepare must not freeze a first-release plan for a repository that
|
|
2294
|
+
// has already published. This is the orphan-root channel that caused
|
|
2295
|
+
// the synthetic 2000-01-01 root commits in the historical public
|
|
2296
|
+
// repositories: first releases used mode=none, push-snapshot degraded
|
|
2297
|
+
// to create-release-branch with no parent, and every later release
|
|
2298
|
+
// started a fresh lineage. Fail closed and demand a bound baseline
|
|
2299
|
+
// pointing at the previous release commit instead.
|
|
2300
|
+
if (production && !offline) {
|
|
2301
|
+
const tagTemplate = unit.version?.tagTemplate;
|
|
2302
|
+
const tagPattern = tagTemplate ? tagTemplate.replace('{version}', '*') : null;
|
|
2303
|
+
if (tagPattern) {
|
|
2304
|
+
const detection = await detectPriorReleaseTags(unit.publicRepo, tagPattern, {
|
|
2305
|
+
githubHost: productionGithubHost,
|
|
2306
|
+
});
|
|
2307
|
+
if (detection.found) {
|
|
2308
|
+
await evidence.append({
|
|
2309
|
+
phase: "previous-public-baseline",
|
|
2310
|
+
unitId: unit.id,
|
|
2311
|
+
status: "blocking",
|
|
2312
|
+
reason: "CHAIN_GAP",
|
|
2313
|
+
repo: unit.publicRepo,
|
|
2314
|
+
tagPattern,
|
|
2315
|
+
guidance: "非首次发布:必须把 previousPublicBaseline 绑定到上一发布提交(mode=bound),不能以 mode=none 制造新的孤儿根提交",
|
|
2316
|
+
});
|
|
2317
|
+
throw new ReleaseError(
|
|
2318
|
+
GATE_FAILED,
|
|
2319
|
+
`unit "${unit.id}" previousPublicBaseline.mode=none but the public repository already has release tags matching "${tagPattern}" (CHAIN_GAP): bind the previous public release commit as a bound baseline`,
|
|
2320
|
+
{ unitId: unit.id, reason: 'CHAIN_GAP', repo: unit.publicRepo, tagPattern },
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
if (detection.error) {
|
|
2324
|
+
await evidence.append({
|
|
2325
|
+
phase: "previous-public-baseline",
|
|
2326
|
+
unitId: unit.id,
|
|
2327
|
+
status: "warning",
|
|
2328
|
+
reason: "CHAIN_GAP_DETECTION_FAILED",
|
|
2329
|
+
repo: unit.publicRepo,
|
|
2330
|
+
tagPattern,
|
|
2331
|
+
error: detection.error,
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2123
2336
|
unitBaselineResults.set(unit.id, {
|
|
2124
2337
|
mode: "none",
|
|
2125
2338
|
status: "consistent",
|
|
@@ -2276,24 +2489,38 @@ export async function prepareRelease(options) {
|
|
|
2276
2489
|
// Snapshot gates always run on disposable writable copies. The public
|
|
2277
2490
|
// snapshot authority is re-digested after every gate and is never exposed
|
|
2278
2491
|
// as the gate working directory.
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
+
}
|
|
2291
2516
|
|
|
2292
2517
|
// Bind the remote source-authority proof to the exact bytes that entered
|
|
2293
2518
|
// the frozen snapshots, not merely to an earlier read of the workspace.
|
|
2294
2519
|
// Then re-read the complete closure and dirty state once more so version
|
|
2295
2520
|
// sources and non-snapshot closure entries cannot drift during prepare.
|
|
2296
|
-
|
|
2521
|
+
// Trimmed together with Step 3c when a docs/config/marketplace workflow
|
|
2522
|
+
// skips the source-authority closure.
|
|
2523
|
+
if (production && !skipSourceAuthorityClosure) {
|
|
2297
2524
|
const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
|
|
2298
2525
|
closure: sourceInputClosure,
|
|
2299
2526
|
unitResults,
|
|
@@ -2420,59 +2647,72 @@ export async function prepareRelease(options) {
|
|
|
2420
2647
|
// publish will re-verify, rather than the writable pre-freeze staging tree.
|
|
2421
2648
|
// Non-production plans scan the final staging tree at the same point.
|
|
2422
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.
|
|
2423
2654
|
const skillResourceClosureResults = [];
|
|
2424
|
-
|
|
2655
|
+
if (skipSkillResourceClosure) {
|
|
2425
2656
|
await evidence.append({
|
|
2426
2657
|
phase: 'skill-resource-closure',
|
|
2427
|
-
status: '
|
|
2428
|
-
|
|
2658
|
+
status: 'skipped',
|
|
2659
|
+
reason: `workflow "${workflow}" trims the skill resource closure gate`,
|
|
2660
|
+
unitCount: unitResults.length,
|
|
2429
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
|
+
});
|
|
2430
2669
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2670
|
+
const closureResult = await checkSkillResourceClosure({
|
|
2671
|
+
snapshotDir: manifest.outputDir,
|
|
2672
|
+
host: 'root',
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
|
|
2676
|
+
skillResourceClosureResults.push(receipt);
|
|
2435
2677
|
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
+
}
|
|
2438
2702
|
|
|
2439
|
-
if (closureResult.findings.length > 0) {
|
|
2440
2703
|
await evidence.append({
|
|
2441
2704
|
phase: 'skill-resource-closure',
|
|
2442
|
-
status: '
|
|
2705
|
+
status: 'completed',
|
|
2443
2706
|
unitId: unit.id,
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
})),
|
|
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,
|
|
2452
2714
|
});
|
|
2453
|
-
throw new ReleaseError(
|
|
2454
|
-
GATE_FAILED,
|
|
2455
|
-
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2456
|
-
{
|
|
2457
|
-
unitId: unit.id,
|
|
2458
|
-
findingCount: closureResult.findings.length,
|
|
2459
|
-
findings: closureResult.findings,
|
|
2460
|
-
},
|
|
2461
|
-
);
|
|
2462
2715
|
}
|
|
2463
|
-
|
|
2464
|
-
await evidence.append({
|
|
2465
|
-
phase: 'skill-resource-closure',
|
|
2466
|
-
status: 'completed',
|
|
2467
|
-
unitId: unit.id,
|
|
2468
|
-
checkerVersion: closureResult.checkerVersion,
|
|
2469
|
-
surfaceCount: receipt.surfaceCount,
|
|
2470
|
-
skillCount: receipt.skillCount,
|
|
2471
|
-
referenceCount: closureResult.referenceCount,
|
|
2472
|
-
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2473
|
-
findingCount: 0,
|
|
2474
|
-
receiptDigest: closureResult.receiptDigest,
|
|
2475
|
-
});
|
|
2476
2716
|
}
|
|
2477
2717
|
|
|
2478
2718
|
// Freeze external independent marketplace HEADs (production + online only):
|
|
@@ -2805,20 +3045,114 @@ export async function prepareRelease(options) {
|
|
|
2805
3045
|
// 构建冻结分发映射:unitId -> frozen distributions
|
|
2806
3046
|
const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
|
|
2807
3047
|
|
|
2808
|
-
|
|
3048
|
+
let externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
|
|
2809
3049
|
|
|
2810
3050
|
// Compute overall snapshot digest
|
|
2811
3051
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
2812
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
|
+
|
|
2813
3118
|
// Detect human consumer platforms (Kimi/CodeBuddy) in the plan.
|
|
2814
3119
|
// When present, new plans mark them as non-blocking manual follow-up tasks.
|
|
2815
3120
|
const hasHumanConsumerActions = externalActions.some(
|
|
2816
3121
|
(a) => a.type === 'kimi-marketplace-install' || a.type === 'codebuddy-marketplace-install',
|
|
2817
3122
|
);
|
|
2818
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
|
+
|
|
2819
3147
|
const plan = {
|
|
2820
3148
|
planVersion: 2,
|
|
2821
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 } : {}),
|
|
2822
3156
|
baseline: {
|
|
2823
3157
|
gitTreeHash: baseline.gitTreeHash,
|
|
2824
3158
|
headCommit: baseline.gitHead,
|
|
@@ -2828,14 +3162,18 @@ export async function prepareRelease(options) {
|
|
|
2828
3162
|
capturedAt: baseline.capturedAt,
|
|
2829
3163
|
},
|
|
2830
3164
|
configDigest,
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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
|
+
: {}),
|
|
2839
3177
|
verificationGates: config.verificationGates ?? [],
|
|
2840
3178
|
snapshotDigest: overallSnapshotDigest,
|
|
2841
3179
|
...(hasHumanConsumerActions ? { humanConsumersStrategy: 'manualFollowUps' } : {}),
|
|
@@ -2847,6 +3185,7 @@ export async function prepareRelease(options) {
|
|
|
2847
3185
|
} : {}),
|
|
2848
3186
|
units,
|
|
2849
3187
|
externalActions,
|
|
3188
|
+
...(frozenPostPublish ? { postPublish: frozenPostPublish } : {}),
|
|
2850
3189
|
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
2851
3190
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
2852
3191
|
};
|
|
@@ -2886,6 +3225,8 @@ export async function prepareRelease(options) {
|
|
|
2886
3225
|
unitCount: units.length,
|
|
2887
3226
|
actionCount: externalActions.length,
|
|
2888
3227
|
offline,
|
|
3228
|
+
workflowKind: workflow,
|
|
3229
|
+
...(workflowDecision ? { workflowDecision } : {}),
|
|
2889
3230
|
completedAt: (clock ? clock() : new Date().toISOString()),
|
|
2890
3231
|
});
|
|
2891
3232
|
|