release-skill 0.6.2 → 0.7.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 +48 -0
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +47 -2
- package/INSTALL.zh-CN.md +29 -2
- package/README.md +137 -12
- package/README.zh-CN.md +110 -9
- 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/license-texts/Apache-2.0.txt +201 -0
- package/adapters/claude/bin/license-texts/MIT.txt +21 -0
- package/adapters/claude/bin/registry.json +64 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/.render-manifest.json +10 -6
- package/adapters/claude/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/claude/schemas/release-plan.schema.json +200 -4
- package/adapters/claude/schemas/release-project.schema.json +86 -5
- package/adapters/claude/schemas/release-run.schema.json +11 -6
- 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/license-texts/Apache-2.0.txt +201 -0
- package/adapters/codex/bin/license-texts/MIT.txt +21 -0
- package/adapters/codex/bin/registry.json +64 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/.render-manifest.json +10 -6
- package/adapters/codex/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/codex/schemas/release-plan.schema.json +200 -4
- package/adapters/codex/schemas/release-project.schema.json +86 -5
- package/adapters/codex/schemas/release-run.schema.json +11 -6
- 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/license-texts/Apache-2.0.txt +201 -0
- package/adapters/kimi/bin/license-texts/MIT.txt +21 -0
- package/adapters/kimi/bin/registry.json +64 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/.render-manifest.json +10 -6
- package/adapters/kimi/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/kimi/schemas/release-plan.schema.json +200 -4
- package/adapters/kimi/schemas/release-project.schema.json +86 -5
- package/adapters/kimi/schemas/release-run.schema.json +11 -6
- 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/license-texts/Apache-2.0.txt +201 -0
- package/adapters/workbuddy/bin/license-texts/MIT.txt +21 -0
- package/adapters/workbuddy/bin/registry.json +64 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +50570 -42208
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/.render-manifest.json +10 -6
- package/adapters/workbuddy/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +200 -4
- package/adapters/workbuddy/schemas/release-project.schema.json +86 -5
- package/adapters/workbuddy/schemas/release-run.schema.json +11 -6
- package/bin/error-codes.json +1 -1
- package/bin/kernel-protocol.json +1 -1
- package/bin/license-texts/Apache-2.0.txt +201 -0
- package/bin/license-texts/MIT.txt +21 -0
- package/bin/registry.json +64 -1
- package/bin/release-skill-cli.mjs +187 -9
- package/bin/release-skill.bundle.mjs +50570 -42208
- package/bin/rules.json +1 -1
- package/package.json +6 -4
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +5 -5
- package/references/01-state-machine.md +22 -2
- package/references/02-project-config.md +1 -1
- package/references/06-adapter-contract.md +6 -3
- package/schemas/.render-manifest.json +10 -6
- package/schemas/postpublish-approval-record.schema.json +47 -0
- package/schemas/release-plan.schema.json +200 -4
- package/schemas/release-project.schema.json +86 -5
- package/schemas/release-run.schema.json +11 -6
- package/src/adapters/distribute-git.mjs +17 -11
- package/src/commands/approve.mjs +163 -1
- package/src/commands/distribute.mjs +649 -89
- package/src/commands/postverify.mjs +808 -0
- package/src/commands/prepare.mjs +456 -45
- package/src/commands/setup.mjs +715 -0
- package/src/commands/ship.mjs +152 -5
- package/src/commands/verify.mjs +92 -15
- package/src/core/approval.mjs +93 -68
- package/src/core/bounded-output.mjs +46 -0
- package/src/core/derived-artifact-gates.mjs +258 -0
- package/src/core/docs-refresh-preset.mjs +171 -0
- package/src/core/errors.mjs +4 -0
- package/src/core/evidence.mjs +8 -1
- package/src/core/git-url-policy.mjs +214 -0
- package/src/core/hooks.mjs +28 -0
- package/src/core/marketplace-registry-entry.mjs +175 -0
- package/src/core/notify-handoff.mjs +76 -0
- package/src/core/postpublish-approval.mjs +241 -0
- package/src/core/postpublish-bundle.mjs +342 -0
- package/src/core/postpublish-projection.mjs +210 -0
- package/src/core/postpublish.mjs +449 -25
- package/src/core/preset-executor.mjs +175 -0
- package/src/core/preset-gitwrite.mjs +484 -0
- package/src/core/presets.mjs +754 -0
- package/src/core/proposal-inbox.mjs +646 -0
- package/src/core/redact.mjs +16 -3
- package/src/core/run.mjs +91 -6
- package/src/producers/build-adapters.mjs +28 -14
package/src/commands/prepare.mjs
CHANGED
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
* @module commands/prepare
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { resolve, relative, isAbsolute, normalize, dirname, basename } from 'node:path';
|
|
22
|
-
import { readFile, mkdir, readdir, realpath } from 'node:fs/promises';
|
|
21
|
+
import { resolve, relative, isAbsolute, normalize, dirname, basename, posix as pathPosix } from 'node:path';
|
|
22
|
+
import { readFile, mkdir, readdir, realpath, lstat } from 'node:fs/promises';
|
|
23
23
|
import { execFile as execFileCb } from 'node:child_process';
|
|
24
24
|
import { promisify } from 'node:util';
|
|
25
25
|
|
|
26
26
|
const execFile = promisify(execFileCb);
|
|
27
27
|
|
|
28
|
+
import { classifyPathInput } from 'skill-family-harness-node';
|
|
28
29
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
29
30
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
30
31
|
import { runHook } from '../core/hooks.mjs';
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
} from '../snapshot/frozen.mjs';
|
|
57
58
|
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT, BUNDLE_STALE } from '../core/errors.mjs';
|
|
58
59
|
import { assertBundleFreshness } from '../core/bundle-freshness.mjs';
|
|
60
|
+
import { assertAdapterFreshness, assertSelfBootstrapFacts } from '../core/derived-artifact-gates.mjs';
|
|
59
61
|
import { PKG_ROOT } from '../core/pkg-root.mjs';
|
|
60
62
|
import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
|
|
61
63
|
import {
|
|
@@ -71,7 +73,13 @@ import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
|
71
73
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
72
74
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
73
75
|
import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
|
|
74
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
validatePostPublishDeclaration,
|
|
78
|
+
normalizePostPublishDeclaration,
|
|
79
|
+
orderNormalizedHooks,
|
|
80
|
+
PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
81
|
+
} from '../core/postpublish.mjs';
|
|
82
|
+
import { freezeExecutionBundle, bundleRootForAuthorityDir } from '../core/postpublish-bundle.mjs';
|
|
75
83
|
|
|
76
84
|
// ---------------------------------------------------------------------------
|
|
77
85
|
// 安装契约常量
|
|
@@ -228,39 +236,15 @@ export async function resolveAllUnitVersions(units, root, explicitVersion, evide
|
|
|
228
236
|
// Hooks execution
|
|
229
237
|
// ---------------------------------------------------------------------------
|
|
230
238
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
* @param {string} [text] - Captured stdout/stderr text.
|
|
241
|
-
* @returns {string} The bounded tail ('' for empty/absent input).
|
|
242
|
-
*/
|
|
243
|
-
export function boundedOutputTail(text) {
|
|
244
|
-
if (typeof text !== 'string' || text.length === 0) return '';
|
|
245
|
-
let lines = text.split('\n');
|
|
246
|
-
// A trailing newline produces an empty final element; drop it so the line
|
|
247
|
-
// budget counts real output lines.
|
|
248
|
-
if (lines.length > 1 && lines[lines.length - 1] === '') {
|
|
249
|
-
lines = lines.slice(0, -1);
|
|
250
|
-
}
|
|
251
|
-
let tail = lines.slice(-HOOK_OUTPUT_TAIL_MAX_LINES);
|
|
252
|
-
let joined = tail.join('\n');
|
|
253
|
-
while (tail.length > 1 && Buffer.byteLength(joined, 'utf8') > HOOK_OUTPUT_TAIL_MAX_BYTES) {
|
|
254
|
-
tail = tail.slice(1);
|
|
255
|
-
joined = tail.join('\n');
|
|
256
|
-
}
|
|
257
|
-
if (Buffer.byteLength(joined, 'utf8') > HOOK_OUTPUT_TAIL_MAX_BYTES) {
|
|
258
|
-
// A single line exceeds the byte cap: keep the trailing bytes.
|
|
259
|
-
const buf = Buffer.from(joined, 'utf8');
|
|
260
|
-
joined = buf.subarray(buf.length - HOOK_OUTPUT_TAIL_MAX_BYTES).toString('utf8');
|
|
261
|
-
}
|
|
262
|
-
return joined;
|
|
263
|
-
}
|
|
239
|
+
// v0.6.3 R1 tail unification: the "last 50 lines / 8 KiB" tail authority now
|
|
240
|
+
// lives in core/bounded-output.mjs, shared by prepare and distribute. The
|
|
241
|
+
// re-export preserves prepare's historical public surface.
|
|
242
|
+
export {
|
|
243
|
+
boundedOutputTail,
|
|
244
|
+
HOOK_OUTPUT_TAIL_MAX_LINES,
|
|
245
|
+
HOOK_OUTPUT_TAIL_MAX_BYTES,
|
|
246
|
+
} from '../core/bounded-output.mjs';
|
|
247
|
+
import { boundedOutputTail } from '../core/bounded-output.mjs';
|
|
264
248
|
|
|
265
249
|
/**
|
|
266
250
|
* Run all declared project hooks in order: docs, build, test, typecheck.
|
|
@@ -908,6 +892,94 @@ function normalizedProductionConfig(unit) {
|
|
|
908
892
|
};
|
|
909
893
|
}
|
|
910
894
|
|
|
895
|
+
/**
|
|
896
|
+
* O5 (2026-08-18 release-cycle investigation §3.2): observe how the local
|
|
897
|
+
* workspace HEAD relates to `origin/<defaultBranch>` for an ONLINE production
|
|
898
|
+
* prepare. This is a WARNING-LEVEL, non-blocking pre-publish signal: in the
|
|
899
|
+
* 0.6.1 cycle a workspace 15 commits ahead of origin only surfaced when the
|
|
900
|
+
* publish source-authority gate rejected it. Pushing is a legitimate
|
|
901
|
+
* pre-publish action, so the observation must inform, never block the freeze.
|
|
902
|
+
*
|
|
903
|
+
* Read-only git plumbing only (argv arrays, no shell): rev-parse, ls-remote,
|
|
904
|
+
* cat-file, rev-list. Every failure degrades to a descriptive status object —
|
|
905
|
+
* this function NEVER throws, so it can never break a freeze.
|
|
906
|
+
*
|
|
907
|
+
* @param {object} options
|
|
908
|
+
* @param {string} options.root - Workspace (git repository) root.
|
|
909
|
+
* @param {string} options.defaultBranch - Default branch name on origin.
|
|
910
|
+
* @returns {Promise<{
|
|
911
|
+
* status: 'in-sync' | 'ahead' | 'behind' | 'diverged' | 'no-origin' | 'remote-ref-missing' | 'unknown',
|
|
912
|
+
* localHead?: string,
|
|
913
|
+
* remoteHead?: string,
|
|
914
|
+
* aheadCount?: number,
|
|
915
|
+
* behindCount?: number,
|
|
916
|
+
* error?: string,
|
|
917
|
+
* }>}
|
|
918
|
+
*/
|
|
919
|
+
export async function observeOriginAhead({ root, defaultBranch }) {
|
|
920
|
+
const runGit = (gitArgs) => execFile('git', ['-C', root, ...gitArgs], {
|
|
921
|
+
shell: false,
|
|
922
|
+
encoding: 'utf8',
|
|
923
|
+
timeout: 30000,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
let localHead;
|
|
927
|
+
try {
|
|
928
|
+
({ stdout: localHead } = await runGit(['rev-parse', 'HEAD']));
|
|
929
|
+
localHead = localHead.trim();
|
|
930
|
+
} catch (err) {
|
|
931
|
+
return { status: 'unknown', error: `rev-parse HEAD failed: ${err.message}` };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Distinguish "no origin remote" from a transient ls-remote failure.
|
|
935
|
+
try {
|
|
936
|
+
await runGit(['remote', 'get-url', 'origin']);
|
|
937
|
+
} catch {
|
|
938
|
+
return { status: 'no-origin', localHead };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let remoteHead;
|
|
942
|
+
try {
|
|
943
|
+
const { stdout } = await runGit(['ls-remote', 'origin', `refs/heads/${defaultBranch}`]);
|
|
944
|
+
const firstLine = stdout.trim().split('\n').filter((line) => line.length > 0)[0];
|
|
945
|
+
if (!firstLine) {
|
|
946
|
+
return { status: 'remote-ref-missing', localHead, defaultBranch };
|
|
947
|
+
}
|
|
948
|
+
remoteHead = firstLine.split('\t')[0];
|
|
949
|
+
} catch (err) {
|
|
950
|
+
return { status: 'unknown', localHead, error: `ls-remote origin failed: ${err.message}` };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (remoteHead === localHead) {
|
|
954
|
+
return { status: 'in-sync', localHead, remoteHead };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Ancestry is only computable when the remote head object exists locally
|
|
958
|
+
// (no implicit fetch — this observer is read-only on the network beyond the
|
|
959
|
+
// single ls-remote above). When it does not, report diverged with no counts.
|
|
960
|
+
try {
|
|
961
|
+
await runGit(['cat-file', '-e', `${remoteHead}^{commit}`]);
|
|
962
|
+
} catch {
|
|
963
|
+
return { status: 'diverged', localHead, remoteHead };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
const aheadRaw = await runGit(['rev-list', '--count', `${remoteHead}..HEAD`]);
|
|
968
|
+
const behindRaw = await runGit(['rev-list', '--count', `HEAD..${remoteHead}`]);
|
|
969
|
+
const aheadCount = Number.parseInt(aheadRaw.stdout.trim(), 10) || 0;
|
|
970
|
+
const behindCount = Number.parseInt(behindRaw.stdout.trim(), 10) || 0;
|
|
971
|
+
if (aheadCount > 0 && behindCount === 0) {
|
|
972
|
+
return { status: 'ahead', localHead, remoteHead, aheadCount };
|
|
973
|
+
}
|
|
974
|
+
if (behindCount > 0 && aheadCount === 0) {
|
|
975
|
+
return { status: 'behind', localHead, remoteHead, behindCount };
|
|
976
|
+
}
|
|
977
|
+
return { status: 'diverged', localHead, remoteHead, aheadCount, behindCount };
|
|
978
|
+
} catch (err) {
|
|
979
|
+
return { status: 'diverged', localHead, remoteHead, error: err.message };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
911
983
|
/**
|
|
912
984
|
* Derive the deterministic freeze timestamp for planVersion 2 plans (design:
|
|
913
985
|
* t1-2-digest-decoupling.md §4.2): the baseline headCommit's committer date,
|
|
@@ -1052,6 +1124,128 @@ async function buildProductionAssets(
|
|
|
1052
1124
|
return assets;
|
|
1053
1125
|
}
|
|
1054
1126
|
|
|
1127
|
+
// ---------------------------------------------------------------------------
|
|
1128
|
+
// F-01 / T1: private execution inputs freeze gate
|
|
1129
|
+
// ---------------------------------------------------------------------------
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Normalize one relative path for closure-style set comparison (the same
|
|
1133
|
+
* lexical normalization Foundation applies to closure resource paths).
|
|
1134
|
+
*
|
|
1135
|
+
* @param {string} value - Relative path candidate.
|
|
1136
|
+
* @returns {string} POSIX-normalized form.
|
|
1137
|
+
*/
|
|
1138
|
+
function normalizeRelativeClosurePath(value) {
|
|
1139
|
+
return pathPosix.normalize(String(value).replaceAll('\\', '/'));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Enumerate the paths present in the frozen tag tree: the production asset
|
|
1144
|
+
* commit is the commit the distribution tag will point at, and its tree
|
|
1145
|
+
* lives in the detached asset repository built by buildProductionAssets.
|
|
1146
|
+
* Read-only local git; no network.
|
|
1147
|
+
*
|
|
1148
|
+
* @param {string} root - Release workspace root.
|
|
1149
|
+
* @param {object} asset - productionAssets entry (gitObjectDir + commit).
|
|
1150
|
+
* @returns {Promise<Set<string>>} Normalized paths contained in the tag.
|
|
1151
|
+
*/
|
|
1152
|
+
async function enumerateFrozenTagPaths(root, asset) {
|
|
1153
|
+
const gitDir = resolve(root, asset.gitObjectDir);
|
|
1154
|
+
let stdout;
|
|
1155
|
+
try {
|
|
1156
|
+
({ stdout } = await execFile('git', [
|
|
1157
|
+
'--git-dir', gitDir,
|
|
1158
|
+
'ls-tree', '-r', '-z', '--name-only', asset.commit,
|
|
1159
|
+
]));
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
throw new ReleaseError(
|
|
1162
|
+
GATE_FAILED,
|
|
1163
|
+
`cannot enumerate the frozen tag tree for the executionFiles gate: ${err?.message ?? err}`,
|
|
1164
|
+
{ gitObjectDir: asset.gitObjectDir, commit: asset.commit },
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return new Set(`${stdout}`.split('\0').filter(Boolean).map(normalizeRelativeClosurePath));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Collect the relative-path candidates from every postPublish command array
|
|
1172
|
+
* (materialize, steps, custom command hooks). Flag-like elements are not
|
|
1173
|
+
* path candidates; lexical safety is decided by Foundation classification.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {object} postPublish - The unit postPublish declaration.
|
|
1176
|
+
* @returns {Array<{where: string, element: string}>}
|
|
1177
|
+
*/
|
|
1178
|
+
function collectPostPublishCommandCandidates(postPublish) {
|
|
1179
|
+
const candidates = [];
|
|
1180
|
+
const visit = (where, command) => {
|
|
1181
|
+
if (!Array.isArray(command)) return;
|
|
1182
|
+
for (const element of command) {
|
|
1183
|
+
if (typeof element !== 'string' || element.length === 0 || element.startsWith('-')) continue;
|
|
1184
|
+
candidates.push({ where, element });
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
visit('materialize', postPublish.materialize?.command);
|
|
1188
|
+
for (const step of postPublish.steps ?? []) visit(`steps[${step.name}]`, step.command);
|
|
1189
|
+
for (const hook of postPublish.hooks ?? []) {
|
|
1190
|
+
if (Array.isArray(hook.command)) visit(`hooks[${hook.id}]`, hook.command);
|
|
1191
|
+
}
|
|
1192
|
+
return candidates;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* F-01 / T1 declaration gate (fail-closed, before any plan write):
|
|
1197
|
+
*
|
|
1198
|
+
* - a command-array element that is a safe relative path, EXISTS as a
|
|
1199
|
+
* regular file in the live workspace, is ABSENT from the frozen tag tree,
|
|
1200
|
+
* and is NOT declared in executionFiles is an undeclared private input —
|
|
1201
|
+
* report it immediately instead of letting distribute guess or copy;
|
|
1202
|
+
* - an executionFiles entry that already exists in the frozen tag is
|
|
1203
|
+
* rejected: tag files stay bound to tagCommit and the execution bundle
|
|
1204
|
+
* must never shadow them.
|
|
1205
|
+
*
|
|
1206
|
+
* When no frozen tag exists yet (non-production prepare) the tag-dependent
|
|
1207
|
+
* checks are skipped — distribute fails closed later without a tagCommit.
|
|
1208
|
+
*
|
|
1209
|
+
* @param {object} postPublish - Unit postPublish declaration.
|
|
1210
|
+
* @param {object} params
|
|
1211
|
+
* @param {string} params.workspaceRoot - Release workspace root (realpath).
|
|
1212
|
+
* @param {Set<string>|null} params.frozenTagPaths - Frozen tag tree paths.
|
|
1213
|
+
* @param {string[]} params.executionFiles - Declared closed-world manifest.
|
|
1214
|
+
*/
|
|
1215
|
+
async function assertPrivateExecutionDeclarations(postPublish, { workspaceRoot, frozenTagPaths, executionFiles }) {
|
|
1216
|
+
const declared = new Set(executionFiles.map(normalizeRelativeClosurePath));
|
|
1217
|
+
for (const { where, element } of collectPostPublishCommandCandidates(postPublish)) {
|
|
1218
|
+
const classification = classifyPathInput(element);
|
|
1219
|
+
if (!classification.ok) continue; // absolute/UNC/backslash inputs are not workspace-relative files
|
|
1220
|
+
let stats = null;
|
|
1221
|
+
try {
|
|
1222
|
+
stats = await lstat(resolve(workspaceRoot, element));
|
|
1223
|
+
} catch {
|
|
1224
|
+
continue; // not present in the live workspace: nothing to declare
|
|
1225
|
+
}
|
|
1226
|
+
if (!stats.isFile()) continue;
|
|
1227
|
+
const normalized = normalizeRelativeClosurePath(element);
|
|
1228
|
+
if (frozenTagPaths && frozenTagPaths.has(normalized)) continue; // bound by tagCommit
|
|
1229
|
+
if (declared.has(normalized)) continue;
|
|
1230
|
+
throw new ReleaseError(
|
|
1231
|
+
GATE_FAILED,
|
|
1232
|
+
`postPublish ${where} command references the workspace-private file "${element}" that exists in the workspace but is absent from the frozen tag; declare it in postPublish.executionFiles (closed world — helper files included)`,
|
|
1233
|
+
{ where, path: element },
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (frozenTagPaths) {
|
|
1237
|
+
for (const entry of executionFiles) {
|
|
1238
|
+
if (frozenTagPaths.has(normalizeRelativeClosurePath(entry))) {
|
|
1239
|
+
throw new ReleaseError(
|
|
1240
|
+
GATE_FAILED,
|
|
1241
|
+
`postPublish.executionFiles entry "${entry}" already exists in the frozen tag; tag files stay bound to tagCommit and the execution bundle must never shadow them`,
|
|
1242
|
+
{ path: entry },
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1055
1249
|
// ---------------------------------------------------------------------------
|
|
1056
1250
|
// External independent marketplace freeze (production + online only)
|
|
1057
1251
|
// ---------------------------------------------------------------------------
|
|
@@ -1211,7 +1405,8 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1211
1405
|
if (offline) {
|
|
1212
1406
|
throw new ReleaseError(
|
|
1213
1407
|
GATE_FAILED,
|
|
1214
|
-
`unit "${unit.id}" ${dist.type} external marketplace form requires online production prepare to freeze the marketplace commit sha
|
|
1408
|
+
`unit "${unit.id}" ${dist.type} external marketplace form requires online production prepare to freeze the marketplace commit sha. ` +
|
|
1409
|
+
`Remediation: release-skill prepare --production --online`,
|
|
1215
1410
|
{ unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
|
|
1216
1411
|
);
|
|
1217
1412
|
}
|
|
@@ -1765,8 +1960,17 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1765
1960
|
* @param {boolean} [options.hookCache=true] - When false (CLI --no-hook-cache),
|
|
1766
1961
|
* every declared hook runs in full and the incremental hook cache is neither
|
|
1767
1962
|
* read nor written.
|
|
1963
|
+
* @param {Function} [options.adapterFreshnessFn] - Adapter derived-artifact
|
|
1964
|
+
* pre-gate (default assertAdapterFreshness). No environment variable
|
|
1965
|
+
* exempts the default gate (F-03): it runs in the CLI, plain function
|
|
1966
|
+
* calls, and the node:test harness alike. Tests that need a lightweight
|
|
1967
|
+
* fixture inject a double explicitly in-process; the CLI exposes no
|
|
1968
|
+
* parameter for it.
|
|
1969
|
+
* @param {Function} [options.selfBootstrapFactsFn] - Self-bootstrap fact-pin
|
|
1970
|
+
* pre-gate (default assertSelfBootstrapFacts); same injection contract as
|
|
1971
|
+
* adapterFreshnessFn.
|
|
1768
1972
|
*
|
|
1769
|
-
* @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string, warnings: ReadonlyArray<object> }>}
|
|
1973
|
+
* @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string, warnings: ReadonlyArray<object>, nextSteps: ReadonlyArray<{ code: string, message: string }> }>}
|
|
1770
1974
|
*
|
|
1771
1975
|
* @throws {ReleaseError} on any gate failure. No PREPARED plan is written.
|
|
1772
1976
|
*/
|
|
@@ -1925,6 +2129,9 @@ export async function prepareRelease(options) {
|
|
|
1925
2129
|
|
|
1926
2130
|
const { config, configPath, configDigest } = await loadProjectConfig({ root: realRoot });
|
|
1927
2131
|
const adoptionWarnings = collectExpectedPublicSurfaceAdoptionWarnings(config);
|
|
2132
|
+
// Mutable operator-facing warning list: seeded from the adoption warnings,
|
|
2133
|
+
// appended by later gates (O5 origin-ahead). Returned as `warnings`.
|
|
2134
|
+
const runWarnings = [...adoptionWarnings];
|
|
1928
2135
|
|
|
1929
2136
|
await evidence.append({
|
|
1930
2137
|
phase: 'config',
|
|
@@ -2026,6 +2233,75 @@ export async function prepareRelease(options) {
|
|
|
2026
2233
|
reasonTag: 'RELEASE_DOCS_STALE',
|
|
2027
2234
|
});
|
|
2028
2235
|
|
|
2236
|
+
// --- Step 1b-fast: version-sensitive derived-artifact fast pre-gates (O1) ---
|
|
2237
|
+
// 2026-08-18 investigation §3.2: adapter drift and stale self-bootstrap
|
|
2238
|
+
// fact pins used to surface only deep inside the ~80s full test hook.
|
|
2239
|
+
// Promote the two exact canonical checks (build-adapters --check and the
|
|
2240
|
+
// release-docs-self-bootstrap single-file test) to prepare's earliest
|
|
2241
|
+
// stage so the same drift fails closed in seconds, before any hook. Like
|
|
2242
|
+
// the bundle freshness gate this is artifact-integrity class: workflow
|
|
2243
|
+
// trimming never exempts it. Installed layouts record not-applicable.
|
|
2244
|
+
// F-03 (2026-08-21 architecture review): the gates run identically in
|
|
2245
|
+
// the CLI, plain function calls, and the node:test harness — no
|
|
2246
|
+
// environment variable may exempt them (a test seam is never a
|
|
2247
|
+
// production switch). Tests that need lightweight fixtures inject the
|
|
2248
|
+
// adapterFreshnessFn/selfBootstrapFactsFn seams explicitly in-process.
|
|
2249
|
+
await evidence.append({ phase: 'adapter-freshness', status: 'started' });
|
|
2250
|
+
const adapterFreshnessFn = options.adapterFreshnessFn ?? assertAdapterFreshness;
|
|
2251
|
+
let adapterFreshness;
|
|
2252
|
+
try {
|
|
2253
|
+
adapterFreshness = await adapterFreshnessFn(PKG_ROOT);
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
await evidence.append({
|
|
2256
|
+
phase: 'adapter-freshness',
|
|
2257
|
+
status: 'blocking',
|
|
2258
|
+
reason: err.details?.reason ?? null,
|
|
2259
|
+
error: { code: err.code, message: err.message },
|
|
2260
|
+
});
|
|
2261
|
+
throw err;
|
|
2262
|
+
}
|
|
2263
|
+
if (adapterFreshness?.applicable === false) {
|
|
2264
|
+
await evidence.append({
|
|
2265
|
+
phase: 'adapter-freshness',
|
|
2266
|
+
status: 'not-applicable',
|
|
2267
|
+
reason: adapterFreshness.reason,
|
|
2268
|
+
});
|
|
2269
|
+
} else {
|
|
2270
|
+
await evidence.append({
|
|
2271
|
+
phase: 'adapter-freshness',
|
|
2272
|
+
status: 'completed',
|
|
2273
|
+
durationMs: adapterFreshness?.durationMs ?? null,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
await evidence.append({ phase: 'self-bootstrap-facts', status: 'started' });
|
|
2278
|
+
const selfBootstrapFactsFn = options.selfBootstrapFactsFn ?? assertSelfBootstrapFacts;
|
|
2279
|
+
let selfBootstrapFacts;
|
|
2280
|
+
try {
|
|
2281
|
+
selfBootstrapFacts = await selfBootstrapFactsFn(PKG_ROOT);
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
await evidence.append({
|
|
2284
|
+
phase: 'self-bootstrap-facts',
|
|
2285
|
+
status: 'blocking',
|
|
2286
|
+
reason: err.details?.reason ?? null,
|
|
2287
|
+
error: { code: err.code, message: err.message },
|
|
2288
|
+
});
|
|
2289
|
+
throw err;
|
|
2290
|
+
}
|
|
2291
|
+
if (selfBootstrapFacts?.applicable === false) {
|
|
2292
|
+
await evidence.append({
|
|
2293
|
+
phase: 'self-bootstrap-facts',
|
|
2294
|
+
status: 'not-applicable',
|
|
2295
|
+
reason: selfBootstrapFacts.reason,
|
|
2296
|
+
});
|
|
2297
|
+
} else {
|
|
2298
|
+
await evidence.append({
|
|
2299
|
+
phase: 'self-bootstrap-facts',
|
|
2300
|
+
status: 'completed',
|
|
2301
|
+
durationMs: selfBootstrapFacts?.durationMs ?? null,
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2029
2305
|
// --- Step 1c: postPublish distribution declaration gate (R1/R2) ---
|
|
2030
2306
|
// The per-unit postPublish block drives the post-publish distribute
|
|
2031
2307
|
// command. Validate it here, before any hook, baseline, snapshot, remote
|
|
@@ -2033,8 +2309,13 @@ export async function prepareRelease(options) {
|
|
|
2033
2309
|
// side effects. This is the runtime re-check on top of the config JSON
|
|
2034
2310
|
// schema: plans frozen by older schema versions must not be able to
|
|
2035
2311
|
// smuggle shell strings, option-like executables, or secret-ish env
|
|
2036
|
-
// keys through.
|
|
2037
|
-
//
|
|
2312
|
+
// keys through. R2: preset references resolve against the built-in
|
|
2313
|
+
// preset registry (per-preset config validation + requiresApproval
|
|
2314
|
+
// grading), and targets normalize onto preset hooks — the normalized
|
|
2315
|
+
// table is a deterministic projection of the digest-bound declaration,
|
|
2316
|
+
// so any list change changes the plan digest and voids approvals. A
|
|
2317
|
+
// plan binds exactly one declaration; multiple units declaring
|
|
2318
|
+
// postPublish is a hard gate failure.
|
|
2038
2319
|
const postPublishDeclarations = configUnits
|
|
2039
2320
|
.map((unit, index) => ({ unit, index }))
|
|
2040
2321
|
.filter(({ unit }) => unit.postPublish !== undefined);
|
|
@@ -2049,12 +2330,20 @@ export async function prepareRelease(options) {
|
|
|
2049
2330
|
if (postPublishDeclarations.length === 1) {
|
|
2050
2331
|
const { unit, index } = postPublishDeclarations[0];
|
|
2051
2332
|
validatePostPublishDeclaration(unit.postPublish, { unitId: unit.id });
|
|
2333
|
+
// Normalized hook table (design §2.2): validate the dependency
|
|
2334
|
+
// topology at freeze time too, so a cyclic/dangling declaration can
|
|
2335
|
+
// never be frozen for distribute to trip over.
|
|
2336
|
+
const normalizedDeclaration = normalizePostPublishDeclaration(unit.postPublish);
|
|
2337
|
+
const orderedNormalizedHooks = orderNormalizedHooks(normalizedDeclaration.hooks);
|
|
2052
2338
|
postPublishDeclaration = { unit, index };
|
|
2053
2339
|
await evidence.append({
|
|
2054
2340
|
phase: 'postpublish-declaration',
|
|
2055
2341
|
status: 'validated',
|
|
2056
2342
|
unitId: unit.id,
|
|
2057
|
-
targetCount: unit.postPublish.targets.length,
|
|
2343
|
+
targetCount: (unit.postPublish.targets ?? []).length,
|
|
2344
|
+
hookCount: (unit.postPublish.hooks ?? []).length,
|
|
2345
|
+
normalizedHookCount: orderedNormalizedHooks.length,
|
|
2346
|
+
preGates: normalizedDeclaration.preGates.map((gate) => gate.gate),
|
|
2058
2347
|
});
|
|
2059
2348
|
}
|
|
2060
2349
|
|
|
@@ -2297,6 +2586,73 @@ export async function prepareRelease(options) {
|
|
|
2297
2586
|
inputDigest: sourceInputClosure.digest,
|
|
2298
2587
|
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2299
2588
|
});
|
|
2589
|
+
|
|
2590
|
+
// --- Step 3c-ahead: local vs origin ahead observation (O5) ---
|
|
2591
|
+
// Online production prepare only. WARNING level, never blocking: pushing
|
|
2592
|
+
// is a legitimate pre-publish action, but the operator must know before
|
|
2593
|
+
// approval — the publish source-authority gate compares the frozen
|
|
2594
|
+
// source-input closure against the remote default branch and rejects
|
|
2595
|
+
// unpushed local commits (0.6.1 cycle: 15 commits ahead, discovered only
|
|
2596
|
+
// at publish). Offline prepare keeps the legacy zero-observation form.
|
|
2597
|
+
if (!offline) {
|
|
2598
|
+
if (!configDefaultBranch) {
|
|
2599
|
+
await evidence.append({
|
|
2600
|
+
phase: 'origin-ahead',
|
|
2601
|
+
status: 'skipped',
|
|
2602
|
+
reason: 'no project.defaultBranch configured',
|
|
2603
|
+
});
|
|
2604
|
+
} else {
|
|
2605
|
+
await evidence.append({ phase: 'origin-ahead', status: 'started' });
|
|
2606
|
+
const observeOriginAheadFn = options.observeOriginAheadFn ?? observeOriginAhead;
|
|
2607
|
+
let originObservation;
|
|
2608
|
+
try {
|
|
2609
|
+
originObservation = await observeOriginAheadFn({
|
|
2610
|
+
root: realRoot,
|
|
2611
|
+
defaultBranch: configDefaultBranch,
|
|
2612
|
+
});
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
originObservation = { status: 'unknown', error: err?.message ?? String(err) };
|
|
2615
|
+
}
|
|
2616
|
+
if (originObservation?.status === 'ahead') {
|
|
2617
|
+
await evidence.append({
|
|
2618
|
+
phase: 'origin-ahead',
|
|
2619
|
+
status: 'warning',
|
|
2620
|
+
defaultBranch: configDefaultBranch,
|
|
2621
|
+
localHead: originObservation.localHead ?? null,
|
|
2622
|
+
remoteHead: originObservation.remoteHead ?? null,
|
|
2623
|
+
aheadCount: originObservation.aheadCount ?? null,
|
|
2624
|
+
guidance: 'push the workspace before publish (git push); the publish source-authority gate compares against the remote default branch',
|
|
2625
|
+
});
|
|
2626
|
+
runWarnings.push({
|
|
2627
|
+
code: 'ORIGIN_AHEAD',
|
|
2628
|
+
defaultBranch: configDefaultBranch,
|
|
2629
|
+
aheadCount: originObservation.aheadCount ?? null,
|
|
2630
|
+
message:
|
|
2631
|
+
`local HEAD is ${originObservation.aheadCount ?? 'an unknown number of'} commit(s) ahead of origin/${configDefaultBranch}; ` +
|
|
2632
|
+
'the publish source-authority gate compares against the remote default branch — push the workspace (git push) before publish',
|
|
2633
|
+
});
|
|
2634
|
+
} else if (['in-sync', 'behind', 'diverged'].includes(originObservation?.status)) {
|
|
2635
|
+
await evidence.append({
|
|
2636
|
+
phase: 'origin-ahead',
|
|
2637
|
+
status: 'completed',
|
|
2638
|
+
observation: originObservation.status,
|
|
2639
|
+
defaultBranch: configDefaultBranch,
|
|
2640
|
+
localHead: originObservation.localHead ?? null,
|
|
2641
|
+
remoteHead: originObservation.remoteHead ?? null,
|
|
2642
|
+
aheadCount: originObservation.aheadCount ?? null,
|
|
2643
|
+
behindCount: originObservation.behindCount ?? null,
|
|
2644
|
+
});
|
|
2645
|
+
} else {
|
|
2646
|
+
await evidence.append({
|
|
2647
|
+
phase: 'origin-ahead',
|
|
2648
|
+
status: 'unobserved',
|
|
2649
|
+
reason: originObservation?.status ?? 'unknown',
|
|
2650
|
+
defaultBranch: configDefaultBranch,
|
|
2651
|
+
error: originObservation?.error ?? null,
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2300
2656
|
} else if (production) {
|
|
2301
2657
|
await evidence.append({
|
|
2302
2658
|
phase: 'source-authority',
|
|
@@ -2391,7 +2747,8 @@ export async function prepareRelease(options) {
|
|
|
2391
2747
|
if (offline) {
|
|
2392
2748
|
throw new ReleaseError(
|
|
2393
2749
|
GATE_FAILED,
|
|
2394
|
-
`unit "${unit.id}" branch strategy "${branchStrategy}" requires online production prepare
|
|
2750
|
+
`unit "${unit.id}" branch strategy "${branchStrategy}" requires online production prepare. ` +
|
|
2751
|
+
`Remediation: release-skill prepare --production --online`,
|
|
2395
2752
|
{ unitId: unit.id, branchStrategy },
|
|
2396
2753
|
);
|
|
2397
2754
|
}
|
|
@@ -2495,7 +2852,8 @@ export async function prepareRelease(options) {
|
|
|
2495
2852
|
throw new ReleaseError(
|
|
2496
2853
|
GATE_FAILED,
|
|
2497
2854
|
`unit "${unit.id}" has bound previousPublicBaseline but production prepare uses --offline. ` +
|
|
2498
|
-
`Must use --online to observe the previous public baseline before freezing a production plan
|
|
2855
|
+
`Must use --online to observe the previous public baseline before freezing a production plan. ` +
|
|
2856
|
+
`Remediation: release-skill prepare --production --online`,
|
|
2499
2857
|
{ unitId: unit.id, repo: ppbConfig.repo, ref: ppbConfig.ref },
|
|
2500
2858
|
);
|
|
2501
2859
|
}
|
|
@@ -3375,16 +3733,51 @@ export async function prepareRelease(options) {
|
|
|
3375
3733
|
// tagCommit, never from workspace state). planVersion 2 record-layer
|
|
3376
3734
|
// stripping does not strip this block, so every declaration detail is
|
|
3377
3735
|
// bound into the plan digest.
|
|
3736
|
+
//
|
|
3737
|
+
// F-01 / T1 private execution bundle: parent-workspace files that the
|
|
3738
|
+
// post-publish commands need but the frozen tag does not contain are
|
|
3739
|
+
// frozen here — Foundation closure (verbatim computeResourceClosure
|
|
3740
|
+
// return) + the release-unit publicFiles projection — and their bytes
|
|
3741
|
+
// are published digest-addressed under this plan's .release-skill. The
|
|
3742
|
+
// plan is the bundle's only source of truth (no parallel manifest, no
|
|
3743
|
+
// second bundle digest): the raw executionFiles list folds into the
|
|
3744
|
+
// closure and is NOT duplicated into the frozen block.
|
|
3378
3745
|
let frozenPostPublish = null;
|
|
3379
3746
|
if (postPublishDeclaration) {
|
|
3380
3747
|
const { unit, index } = postPublishDeclaration;
|
|
3381
3748
|
const { tag } = resolveProductionBranch(unit, resolvedVersions[index]);
|
|
3749
|
+
const declaredExecutionFiles = unit.postPublish.executionFiles ?? [];
|
|
3750
|
+
const frozenTagPaths = productionAssets
|
|
3751
|
+
? await enumerateFrozenTagPaths(realRoot, productionAssets[index])
|
|
3752
|
+
: null;
|
|
3753
|
+
await assertPrivateExecutionDeclarations(unit.postPublish, {
|
|
3754
|
+
workspaceRoot: realRoot,
|
|
3755
|
+
frozenTagPaths,
|
|
3756
|
+
executionFiles: declaredExecutionFiles,
|
|
3757
|
+
});
|
|
3758
|
+
const executionBundle = await freezeExecutionBundle({
|
|
3759
|
+
workspaceRoot: realRoot,
|
|
3760
|
+
releaseSkillDir: releaseDir,
|
|
3761
|
+
executionFiles: declaredExecutionFiles,
|
|
3762
|
+
publicFiles: unit.publicFiles ?? [],
|
|
3763
|
+
});
|
|
3764
|
+
await evidence.append({
|
|
3765
|
+
phase: 'postpublish-execution-bundle',
|
|
3766
|
+
status: 'frozen',
|
|
3767
|
+
unitId: unit.id,
|
|
3768
|
+
closureDigest: executionBundle.closure.digest,
|
|
3769
|
+
resourceCount: executionBundle.closure.resources.length,
|
|
3770
|
+
publicFileCount: executionBundle.publicFiles.length,
|
|
3771
|
+
bundleRoot: relative(realRoot, bundleRootForAuthorityDir(releaseDir)),
|
|
3772
|
+
});
|
|
3773
|
+
const { executionFiles: _executionFiles, ...declarationWithoutManifest } = structuredClone(unit.postPublish);
|
|
3382
3774
|
frozenPostPublish = {
|
|
3383
|
-
...
|
|
3775
|
+
...declarationWithoutManifest,
|
|
3384
3776
|
tag,
|
|
3385
3777
|
...(productionAssets ? { tagCommit: productionAssets[index].commit } : {}),
|
|
3386
3778
|
unitId: unit.id,
|
|
3387
3779
|
payloadSource: PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
3780
|
+
executionBundle,
|
|
3388
3781
|
};
|
|
3389
3782
|
}
|
|
3390
3783
|
|
|
@@ -3494,11 +3887,29 @@ export async function prepareRelease(options) {
|
|
|
3494
3887
|
completedAt: (clock ? clock() : new Date().toISOString()),
|
|
3495
3888
|
});
|
|
3496
3889
|
|
|
3890
|
+
// --- O4 (2026-08-18 investigation §3.2): success-time guidance ---
|
|
3891
|
+
// A NON-PRODUCTION plan cannot be used by publish; historically operators
|
|
3892
|
+
// discovered that only after a full prepare+approve round-trip. Surface the
|
|
3893
|
+
// remediation at success time so the loop never starts. Production plans
|
|
3894
|
+
// carry no such warning. The prepare command's own defaults are unchanged —
|
|
3895
|
+
// this only enriches the result, never flips offline/production.
|
|
3896
|
+
const nextSteps = [];
|
|
3897
|
+
if (!production) {
|
|
3898
|
+
nextSteps.push({
|
|
3899
|
+
code: 'NON_PRODUCTION_PLAN_NOT_PUBLISHABLE',
|
|
3900
|
+
message:
|
|
3901
|
+
'This plan is NON-PRODUCTION and cannot be used by publish. ' +
|
|
3902
|
+
'To release, re-run: release-skill prepare --production --online — ' +
|
|
3903
|
+
'or use the guided happy end: release-skill ship --target-version <version>.',
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3497
3907
|
return {
|
|
3498
3908
|
planPath: writtenPath,
|
|
3499
3909
|
planDigest,
|
|
3500
3910
|
evidenceDir,
|
|
3501
|
-
warnings:
|
|
3911
|
+
warnings: runWarnings,
|
|
3912
|
+
nextSteps,
|
|
3502
3913
|
};
|
|
3503
3914
|
} catch (err) {
|
|
3504
3915
|
// Record failure evidence
|