release-skill 0.2.9 → 0.4.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 +41 -0
- package/INSTALL.md +34 -110
- package/INSTALL.zh-CN.md +17 -83
- package/README.md +49 -41
- package/README.zh-CN.md +43 -34
- 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 +4131 -3180
- package/adapters/claude/schemas/release-plan.schema.json +7 -0
- package/adapters/claude/schemas/release-run.schema.json +43 -0
- package/adapters/claude/skills/release-help/SKILL.md +10 -2
- package/adapters/claude/skills/release-prepare/SKILL.md +6 -15
- package/adapters/claude/skills/release-publish/SKILL.md +5 -6
- package/adapters/claude/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/claude/skills/release-verify/SKILL.md +4 -6
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/codex/schemas/release-plan.schema.json +7 -0
- package/adapters/codex/schemas/release-run.schema.json +43 -0
- package/adapters/codex/skills/release-help/SKILL.md +10 -2
- package/adapters/codex/skills/release-prepare/SKILL.md +6 -15
- package/adapters/codex/skills/release-publish/SKILL.md +5 -6
- package/adapters/codex/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/codex/skills/release-verify/SKILL.md +4 -6
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/kimi/schemas/release-plan.schema.json +7 -0
- package/adapters/kimi/schemas/release-run.schema.json +43 -0
- package/adapters/kimi/skills/release-help/SKILL.md +10 -2
- package/adapters/kimi/skills/release-prepare/SKILL.md +6 -15
- package/adapters/kimi/skills/release-publish/SKILL.md +5 -6
- package/adapters/kimi/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -6
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/workbuddy/schemas/release-plan.schema.json +7 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +43 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +10 -2
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +6 -15
- package/adapters/workbuddy/skills/release-publish/SKILL.md +5 -6
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -6
- package/bin/release-skill-cli.mjs +212 -43
- package/bin/release-skill.bundle.mjs +4131 -3180
- package/package.json +1 -1
- package/references/01-state-machine.md +1 -1
- package/references/06-adapter-contract.md +6 -5
- package/schemas/release-plan.schema.json +7 -0
- package/schemas/release-run.schema.json +43 -0
- package/skills/release-help/SKILL.md +10 -2
- package/skills/release-prepare/SKILL.md +6 -15
- package/skills/release-publish/SKILL.md +5 -6
- package/skills/release-reconcile/SKILL.md +2 -2
- package/skills/release-verify/SKILL.md +4 -6
- package/skills-src/release-help/SKILL.md +10 -2
- package/skills-src/release-prepare/SKILL.md +6 -15
- package/skills-src/release-publish/SKILL.md +5 -6
- package/skills-src/release-reconcile/SKILL.md +2 -2
- package/skills-src/release-verify/SKILL.md +4 -6
- package/src/adapters/plugin-marketplace.mjs +1 -1
- package/src/adapters/push-snapshot.mjs +3 -1
- package/src/commands/approve.mjs +8 -6
- package/src/commands/attest.mjs +195 -0
- package/src/commands/hooks.mjs +42 -0
- package/src/commands/prepare.mjs +13 -50
- package/src/commands/publish.mjs +0 -8
- package/src/commands/reconcile.mjs +3 -37
- package/src/commands/ship.mjs +334 -0
- package/src/commands/verify.mjs +195 -15
- package/src/core/git-transport.mjs +93 -0
- package/src/core/release-metadata.mjs +105 -0
- package/src/core/skill-resource-closure.mjs +7 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { SAFE_ID_RE, writeEvidenceAtomic } from '../adapters/contract.mjs';
|
|
5
|
+
import { ReleaseError, GATE_FAILED, MISSING_PARAMETERS } from '../core/errors.mjs';
|
|
6
|
+
|
|
7
|
+
const PLATFORM = Object.freeze({
|
|
8
|
+
kimi: {
|
|
9
|
+
directory: 'kimi-attestations',
|
|
10
|
+
requirementFile: 'release-skill-kimi-manual-install.json',
|
|
11
|
+
},
|
|
12
|
+
codebuddy: {
|
|
13
|
+
directory: 'codebuddy-attestations',
|
|
14
|
+
requirementFile: 'release-skill-codebuddy-manual-install.json',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function validateInstalledConsumerClosure({
|
|
19
|
+
root,
|
|
20
|
+
platform,
|
|
21
|
+
requirement,
|
|
22
|
+
installPath,
|
|
23
|
+
}) {
|
|
24
|
+
const planPath = resolve(root, '.release-skill', 'plans', `${requirement.planDigest}.json`);
|
|
25
|
+
const [
|
|
26
|
+
{ validatePlan, computePlanDigest, assertImmutablePlanAuthority },
|
|
27
|
+
{ createPluginMarketplaceAdapter, verifyInstalledMarketplacePayload },
|
|
28
|
+
] = await Promise.all([
|
|
29
|
+
import('../core/plan.mjs'),
|
|
30
|
+
import('../adapters/plugin-marketplace.mjs'),
|
|
31
|
+
]);
|
|
32
|
+
let plan;
|
|
33
|
+
try {
|
|
34
|
+
plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new ReleaseError(
|
|
37
|
+
GATE_FAILED,
|
|
38
|
+
`cannot read the frozen plan required to verify the installed ${platform} payload: ${error.message}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
validatePlan(plan);
|
|
42
|
+
assertImmutablePlanAuthority(planPath, plan);
|
|
43
|
+
if (computePlanDigest(plan) !== requirement.planDigest) {
|
|
44
|
+
throw new ReleaseError(GATE_FAILED, 'manual requirement planDigest does not match the frozen plan');
|
|
45
|
+
}
|
|
46
|
+
const actionType = `${platform}-marketplace-install`;
|
|
47
|
+
const action = (plan.externalActions ?? []).find((candidate) => (
|
|
48
|
+
candidate.type === actionType
|
|
49
|
+
&& candidate.parameters?.plugin === requirement.plugin
|
|
50
|
+
&& candidate.parameters?.version === requirement.version
|
|
51
|
+
));
|
|
52
|
+
if (!action) {
|
|
53
|
+
throw new ReleaseError(GATE_FAILED, `frozen plan has no matching ${platform} install action`);
|
|
54
|
+
}
|
|
55
|
+
const adapter = createPluginMarketplaceAdapter();
|
|
56
|
+
const context = {
|
|
57
|
+
externalWritesAuthorized: false,
|
|
58
|
+
isolatedConsumerWritesAuthorized: false,
|
|
59
|
+
plan,
|
|
60
|
+
baseline: plan.baseline,
|
|
61
|
+
root,
|
|
62
|
+
};
|
|
63
|
+
const actionInput = { actionType, ...action.parameters };
|
|
64
|
+
const preflight = await adapter.preflight(actionInput, context);
|
|
65
|
+
if (preflight.status !== 'PREFLIGHT_PASSED') {
|
|
66
|
+
throw new ReleaseError(
|
|
67
|
+
GATE_FAILED,
|
|
68
|
+
`${platform} installed-payload preflight failed: ${preflight.error}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const binding = await verifyInstalledMarketplacePayload(
|
|
72
|
+
actionInput,
|
|
73
|
+
context,
|
|
74
|
+
installPath,
|
|
75
|
+
platform,
|
|
76
|
+
);
|
|
77
|
+
return {
|
|
78
|
+
payloadDigest: action.parameters.manifestDigest,
|
|
79
|
+
manifestDigest: binding.manifestDigest,
|
|
80
|
+
extraInstalledPaths: binding.extraInstalledPaths ?? [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Record the human fact needed by an interactive-only consumer. Identity
|
|
86
|
+
* fields come exclusively from the generated requirement; the operator only
|
|
87
|
+
* supplies the result, actor and optional observed install facts.
|
|
88
|
+
*/
|
|
89
|
+
export async function recordManualAttestation(options = {}, injected = {}) {
|
|
90
|
+
const {
|
|
91
|
+
root = process.cwd(),
|
|
92
|
+
platform,
|
|
93
|
+
plugin,
|
|
94
|
+
actor,
|
|
95
|
+
result,
|
|
96
|
+
installPath,
|
|
97
|
+
installChannel,
|
|
98
|
+
note,
|
|
99
|
+
clock = () => new Date().toISOString(),
|
|
100
|
+
} = options;
|
|
101
|
+
const descriptor = PLATFORM[platform];
|
|
102
|
+
if (!descriptor || !SAFE_ID_RE.test(plugin ?? '')) {
|
|
103
|
+
throw new ReleaseError(
|
|
104
|
+
MISSING_PARAMETERS,
|
|
105
|
+
'attest requires --platform <kimi|codebuddy> and a safe --plugin <id>',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (typeof actor !== 'string' || actor.trim() === '') {
|
|
109
|
+
throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --actor <person>');
|
|
110
|
+
}
|
|
111
|
+
if (result !== 'passed' && result !== 'failed') {
|
|
112
|
+
throw new ReleaseError(MISSING_PARAMETERS, 'attest requires --result <passed|failed>');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const authorityDir = resolve(root, '.release-skill', descriptor.directory, plugin);
|
|
116
|
+
const requirementPath = join(authorityDir, descriptor.requirementFile);
|
|
117
|
+
let requirement;
|
|
118
|
+
try {
|
|
119
|
+
requirement = JSON.parse(await readFile(requirementPath, 'utf8'));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new ReleaseError(
|
|
122
|
+
GATE_FAILED,
|
|
123
|
+
`cannot read generated ${platform} requirement: ${error.message}`,
|
|
124
|
+
{ platform, plugin, requirementPath },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
requirement.platform !== platform
|
|
129
|
+
|| requirement.plugin !== plugin
|
|
130
|
+
|| typeof requirement.version !== 'string'
|
|
131
|
+
|| !/^[a-f0-9]{64}$/.test(requirement.planDigest ?? '')
|
|
132
|
+
|| typeof requirement.attestationFile !== 'string'
|
|
133
|
+
) {
|
|
134
|
+
throw new ReleaseError(
|
|
135
|
+
GATE_FAILED,
|
|
136
|
+
`generated ${platform} requirement has invalid identity fields`,
|
|
137
|
+
{ platform, plugin, requirementPath },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (result === 'passed' && (typeof installPath !== 'string' || installPath === '')) {
|
|
141
|
+
throw new ReleaseError(
|
|
142
|
+
MISSING_PARAMETERS,
|
|
143
|
+
`${platform} passed attestation requires --install-path for installed payload verification`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const needsInstallChannel = Object.hasOwn(requirement.attestationTemplate ?? {}, 'installChannel');
|
|
147
|
+
if (
|
|
148
|
+
result === 'passed'
|
|
149
|
+
&& needsInstallChannel
|
|
150
|
+
&& installChannel !== 'desktop'
|
|
151
|
+
&& installChannel !== 'cli'
|
|
152
|
+
) {
|
|
153
|
+
throw new ReleaseError(
|
|
154
|
+
MISSING_PARAMETERS,
|
|
155
|
+
`${platform} passed attestation requires --install-channel <desktop|cli>`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const installBinding = result === 'passed'
|
|
160
|
+
? await (injected.validateInstalledConsumerClosure ?? validateInstalledConsumerClosure)({
|
|
161
|
+
root,
|
|
162
|
+
platform,
|
|
163
|
+
requirement,
|
|
164
|
+
installPath,
|
|
165
|
+
})
|
|
166
|
+
: null;
|
|
167
|
+
const receipt = {
|
|
168
|
+
platform,
|
|
169
|
+
version: requirement.version,
|
|
170
|
+
planDigest: requirement.planDigest,
|
|
171
|
+
result,
|
|
172
|
+
actor: actor.trim(),
|
|
173
|
+
confirmedAt: clock(),
|
|
174
|
+
...(installPath ? { installPath } : {}),
|
|
175
|
+
...(installChannel ? { installChannel } : {}),
|
|
176
|
+
...(note ? { note } : {}),
|
|
177
|
+
...(installBinding ? {
|
|
178
|
+
payloadDigest: installBinding.payloadDigest,
|
|
179
|
+
installedClosureVerified: true,
|
|
180
|
+
extraInstalledPaths: installBinding.extraInstalledPaths,
|
|
181
|
+
} : {}),
|
|
182
|
+
};
|
|
183
|
+
const attestationPath = join(authorityDir, requirement.attestationFile);
|
|
184
|
+
await writeEvidenceAtomic(attestationPath, receipt);
|
|
185
|
+
return {
|
|
186
|
+
command: 'attest',
|
|
187
|
+
status: 'RECORDED',
|
|
188
|
+
platform,
|
|
189
|
+
plugin,
|
|
190
|
+
version: requirement.version,
|
|
191
|
+
planDigest: requirement.planDigest,
|
|
192
|
+
requirementPath,
|
|
193
|
+
attestationPath,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { loadProjectConfig } from '../core/config.mjs';
|
|
5
|
+
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
6
|
+
import { runDeclaredHooks } from './prepare.mjs';
|
|
7
|
+
// ReleaseError and GATE_FAILED removed: no authorization gate remains.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run the declared development gates and populate the exact same content-bound
|
|
11
|
+
* hook receipts consumed by prepare. A later ship/prepare reuses only
|
|
12
|
+
* cacheable hooks whose declared input closure is byte-identical.
|
|
13
|
+
*/
|
|
14
|
+
export async function validateDeclaredHooks(options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
root = process.cwd(),
|
|
17
|
+
hooksAuthorized: _hooksAuthorized,
|
|
18
|
+
hookCache = true,
|
|
19
|
+
runDir = resolve(root, '.release-skill', 'runs', `hooks-${Date.now()}`),
|
|
20
|
+
} = options;
|
|
21
|
+
// The command invocation itself authorizes execution of configured hooks.
|
|
22
|
+
// Old --acknowledge-hook-side-effects is accepted as a no-effect compatibility input.
|
|
23
|
+
const { config, configDigest } = await loadProjectConfig({ root });
|
|
24
|
+
await mkdir(runDir, { recursive: true });
|
|
25
|
+
const evidence = createEvidenceWriter({
|
|
26
|
+
runDir,
|
|
27
|
+
command: 'hooks-validate',
|
|
28
|
+
clock: () => new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
const startedAt = new Date().toISOString();
|
|
31
|
+
await runDeclaredHooks(config, root, evidence, undefined, { hookCache });
|
|
32
|
+
return {
|
|
33
|
+
command: 'hooks validate',
|
|
34
|
+
status: 'PASSED',
|
|
35
|
+
configDigest,
|
|
36
|
+
hooks: Object.keys(config.hooks ?? {}).sort(),
|
|
37
|
+
cacheEnabled: hookCache,
|
|
38
|
+
evidenceDir: runDir,
|
|
39
|
+
startedAt,
|
|
40
|
+
finishedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -1809,41 +1809,16 @@ export async function prepareRelease(options) {
|
|
|
1809
1809
|
cwd: hook.cwd ?? '.',
|
|
1810
1810
|
}));
|
|
1811
1811
|
|
|
1812
|
+
// Hook and gate authorization: the command invocation itself authorizes
|
|
1813
|
+
// execution of configured commands. Old acknowledgement parameters
|
|
1814
|
+
// (--acknowledge-hook-side-effects, --acknowledge-gate-side-effects) are
|
|
1815
|
+
// accepted as no-effect compatibility inputs but are not required.
|
|
1812
1816
|
if (declaredHooks.length > 0) {
|
|
1813
|
-
await evidence.append({
|
|
1814
|
-
phase: 'hook-authorization',
|
|
1815
|
-
status: 'started',
|
|
1816
|
-
hookCount: declaredHooks.length,
|
|
1817
|
-
hooks: declaredHooks.map((h) => `${h.name}: ${h.executable} ${h.args.join(' ')}`),
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
|
-
if (hooksAuthorized !== true) {
|
|
1821
|
-
const hookList = declaredHooks
|
|
1822
|
-
.map((h) => ` - ${h.name}: executable="${h.executable}", args=[${h.args.join(', ')}], cwd="${h.cwd}"`)
|
|
1823
|
-
.join('\n');
|
|
1824
|
-
|
|
1825
|
-
await evidence.append({
|
|
1826
|
-
phase: 'hook-authorization',
|
|
1827
|
-
status: 'denied',
|
|
1828
|
-
reason: 'hooks not explicitly authorized',
|
|
1829
|
-
});
|
|
1830
|
-
|
|
1831
|
-
throw new ReleaseError(
|
|
1832
|
-
GATE_FAILED,
|
|
1833
|
-
`project declares ${declaredHooks.length} hook(s) that will be executed as arbitrary local processes.\n` +
|
|
1834
|
-
`These hooks are NOT sandboxed — they may write to the filesystem outside the project, ` +
|
|
1835
|
-
`access local credentials, or make network calls.\n` +
|
|
1836
|
-
`The following hooks will run:\n${hookList}\n\n` +
|
|
1837
|
-
`To proceed, pass --acknowledge-hook-side-effects (CLI) or hooksAuthorized=true (API). ` +
|
|
1838
|
-
`Authorization means you accept hook side-effect risks; it does NOT make hooks safe.`,
|
|
1839
|
-
{ hookNames: declaredHooks.map((h) => h.name), hookCount: declaredHooks.length },
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
1817
|
await evidence.append({
|
|
1844
1818
|
phase: 'hook-authorization',
|
|
1845
1819
|
status: 'authorized',
|
|
1846
1820
|
hookCount: declaredHooks.length,
|
|
1821
|
+
hooks: declaredHooks.map((h) => `${h.name}: ${h.executable} ${h.args.join(' ')}`),
|
|
1847
1822
|
});
|
|
1848
1823
|
}
|
|
1849
1824
|
|
|
@@ -1851,7 +1826,7 @@ export async function prepareRelease(options) {
|
|
|
1851
1826
|
if (declaredVerificationGates.length > 0) {
|
|
1852
1827
|
await evidence.append({
|
|
1853
1828
|
phase: 'verification-gate-authorization',
|
|
1854
|
-
status: '
|
|
1829
|
+
status: 'authorized',
|
|
1855
1830
|
gateCount: declaredVerificationGates.length,
|
|
1856
1831
|
gates: declaredVerificationGates.map((gate) => ({
|
|
1857
1832
|
id: gate.id,
|
|
@@ -1863,25 +1838,6 @@ export async function prepareRelease(options) {
|
|
|
1863
1838
|
cwd: gate.cwd,
|
|
1864
1839
|
})),
|
|
1865
1840
|
});
|
|
1866
|
-
if (verificationGatesAuthorized !== true) {
|
|
1867
|
-
await evidence.append({
|
|
1868
|
-
phase: 'verification-gate-authorization',
|
|
1869
|
-
status: 'denied',
|
|
1870
|
-
gateCount: declaredVerificationGates.length,
|
|
1871
|
-
});
|
|
1872
|
-
throw new ReleaseError(
|
|
1873
|
-
GATE_FAILED,
|
|
1874
|
-
`project declares ${declaredVerificationGates.length} verification gate(s). ` +
|
|
1875
|
-
'They run local project commands without a network sandbox. ' +
|
|
1876
|
-
'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
|
|
1877
|
-
{ gateIds: declaredVerificationGates.map((gate) => gate.id) },
|
|
1878
|
-
);
|
|
1879
|
-
}
|
|
1880
|
-
await evidence.append({
|
|
1881
|
-
phase: 'verification-gate-authorization',
|
|
1882
|
-
status: 'authorized',
|
|
1883
|
-
gateCount: declaredVerificationGates.length,
|
|
1884
|
-
});
|
|
1885
1841
|
}
|
|
1886
1842
|
|
|
1887
1843
|
// --- Step 3: Run declared hooks ---
|
|
@@ -2848,6 +2804,12 @@ export async function prepareRelease(options) {
|
|
|
2848
2804
|
// Compute overall snapshot digest
|
|
2849
2805
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
2850
2806
|
|
|
2807
|
+
// Detect human consumer platforms (Kimi/CodeBuddy) in the plan.
|
|
2808
|
+
// When present, new plans mark them as non-blocking manual follow-up tasks.
|
|
2809
|
+
const hasHumanConsumerActions = externalActions.some(
|
|
2810
|
+
(a) => a.type === 'kimi-marketplace-install' || a.type === 'codebuddy-marketplace-install',
|
|
2811
|
+
);
|
|
2812
|
+
|
|
2851
2813
|
const plan = {
|
|
2852
2814
|
planVersion: 2,
|
|
2853
2815
|
status: 'PREPARED',
|
|
@@ -2870,6 +2832,7 @@ export async function prepareRelease(options) {
|
|
|
2870
2832
|
},
|
|
2871
2833
|
verificationGates: config.verificationGates ?? [],
|
|
2872
2834
|
snapshotDigest: overallSnapshotDigest,
|
|
2835
|
+
...(hasHumanConsumerActions ? { humanConsumersStrategy: 'manualFollowUps' } : {}),
|
|
2873
2836
|
...(production ? {
|
|
2874
2837
|
production: {
|
|
2875
2838
|
mode: 'github-npm-v1',
|
package/src/commands/publish.mjs
CHANGED
|
@@ -465,7 +465,6 @@ export async function publishRelease(options) {
|
|
|
465
465
|
clock: clockOpt,
|
|
466
466
|
captureBaselineFn,
|
|
467
467
|
productionMode = false,
|
|
468
|
-
productionConfirmation,
|
|
469
468
|
observePreviousPublicBaselineFn,
|
|
470
469
|
observeRetrySleep,
|
|
471
470
|
} = options ?? {};
|
|
@@ -544,13 +543,6 @@ export async function publishRelease(options) {
|
|
|
544
543
|
if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
|
|
545
544
|
throw new ReleaseError(GATE_FAILED, 'production plan requires a dedicated assetRoot');
|
|
546
545
|
}
|
|
547
|
-
if (!productionConfirmation || productionConfirmation !== actualDigest) {
|
|
548
|
-
throw new ReleaseError(
|
|
549
|
-
GATE_FAILED,
|
|
550
|
-
'production confirmation must exactly match the current plan digest',
|
|
551
|
-
{ planDigest: actualDigest },
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
546
|
}
|
|
555
547
|
|
|
556
548
|
// =======================================================================
|
|
@@ -91,7 +91,6 @@ function defaultClock() {
|
|
|
91
91
|
* @param {string} options.planPath - Absolute path to the frozen release plan.
|
|
92
92
|
* @param {string} options.sourceRunPath - Absolute path to the source run (publish or prior reconcile).
|
|
93
93
|
* @param {string} [options.approvalPath] - Path to the approval record (required if any action needs retry).
|
|
94
|
-
* @param {string} [options.productionConfirmation] - Exact plan digest required before retrying production writes.
|
|
95
94
|
* @param {Object} options.adapterRegistry - Adapter registry for action execution.
|
|
96
95
|
* @param {string} [options.runDir] - Evidence directory. Defaults to `<planDir>/runs/reconcile-<ts>`.
|
|
97
96
|
* @param {string} [options.root] - Project root for baseline capture.
|
|
@@ -114,7 +113,6 @@ export async function reconcileRelease(options) {
|
|
|
114
113
|
root = process.cwd(),
|
|
115
114
|
clock: clockOpt,
|
|
116
115
|
captureBaselineFn,
|
|
117
|
-
productionConfirmation,
|
|
118
116
|
observePreviousPublicBaselineFn,
|
|
119
117
|
observeRetrySleep,
|
|
120
118
|
} = options ?? {};
|
|
@@ -332,29 +330,6 @@ export async function reconcileRelease(options) {
|
|
|
332
330
|
);
|
|
333
331
|
}
|
|
334
332
|
|
|
335
|
-
// A production retry authority must be confirmed before any remote
|
|
336
|
-
// observation can influence whether a permanent write will be retried.
|
|
337
|
-
// Isolated marketplace consumer checks remain outside this requirement.
|
|
338
|
-
const planActionsById = new Map(
|
|
339
|
-
(plan.externalActions ?? []).map((action) => [action.id, action]),
|
|
340
|
-
);
|
|
341
|
-
const hasNonMarketplaceRetryCandidate = sourceRun.checkpoints.some((checkpoint) => {
|
|
342
|
-
if (checkpoint.status === 'succeeded') return false;
|
|
343
|
-
const action = planActionsById.get(checkpoint.actionId);
|
|
344
|
-
return action && !isMarketplaceAction(action.type);
|
|
345
|
-
});
|
|
346
|
-
if (
|
|
347
|
-
plan.production?.mode === 'github-npm-v1' &&
|
|
348
|
-
hasNonMarketplaceRetryCandidate &&
|
|
349
|
-
productionConfirmation !== actualDigest
|
|
350
|
-
) {
|
|
351
|
-
throw new ReleaseError(
|
|
352
|
-
GATE_FAILED,
|
|
353
|
-
'production reconcile confirmation must exactly match the current plan digest before retry',
|
|
354
|
-
{ planDigest: actualDigest },
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
333
|
await evidence.append({
|
|
359
334
|
phase: 'safety-gate',
|
|
360
335
|
gate: 'source-run-load',
|
|
@@ -918,7 +893,7 @@ export async function reconcileRelease(options) {
|
|
|
918
893
|
// --- Phase 2: Validate approval and global preflight before retrying ---
|
|
919
894
|
//
|
|
920
895
|
// Marketplace recovery is deferred to phase 4 and does NOT require
|
|
921
|
-
// approval
|
|
896
|
+
// approval; marketplace actions are isolated
|
|
922
897
|
// consumer checks, not permanent remote writes. Only non-marketplace
|
|
923
898
|
// retries require approval.
|
|
924
899
|
// =======================================================================
|
|
@@ -926,17 +901,8 @@ export async function reconcileRelease(options) {
|
|
|
926
901
|
const nonMarketplaceRetries = actionsToRetry.filter((a) => !isMarketplaceAction(a.type));
|
|
927
902
|
|
|
928
903
|
if (nonMarketplaceRetries.length > 0) {
|
|
929
|
-
// Non-marketplace retries require approval
|
|
930
|
-
|
|
931
|
-
plan.production?.mode === 'github-npm-v1' &&
|
|
932
|
-
productionConfirmation !== actualDigest
|
|
933
|
-
) {
|
|
934
|
-
throw new ReleaseError(
|
|
935
|
-
GATE_FAILED,
|
|
936
|
-
'production reconcile confirmation must exactly match the current plan digest before retry',
|
|
937
|
-
{ planDigest: actualDigest, actionsToRetry: nonMarketplaceRetries.map((action) => action.id) },
|
|
938
|
-
);
|
|
939
|
-
}
|
|
904
|
+
// Non-marketplace retries require a current immutable approval. The
|
|
905
|
+
// approval already binds the exact plan digest and action set.
|
|
940
906
|
if (!approval) {
|
|
941
907
|
throw new ReleaseError(
|
|
942
908
|
GATE_FAILED,
|