release-skill 0.8.0 → 0.9.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 +65 -7
- package/INSTALL.md +14 -2
- package/INSTALL.zh-CN.md +12 -2
- package/README.md +51 -31
- package/README.zh-CN.md +33 -20
- 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/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/prebuild-manifest.json +8 -4
- package/adapters/claude/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/claude/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/claude/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +43 -5
- package/adapters/claude/schemas/release-run.schema.json +35 -1
- package/adapters/claude/skills/release-finish/SKILL.md +65 -0
- package/adapters/claude/skills/release-verify/SKILL.md +6 -1
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/claude/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/claude/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/prebuild-manifest.json +8 -4
- package/adapters/codex/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/codex/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/codex/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +43 -5
- package/adapters/codex/schemas/release-run.schema.json +35 -1
- package/adapters/codex/skills/release-finish/SKILL.md +72 -0
- package/adapters/codex/skills/release-verify/SKILL.md +6 -1
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/codex/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/codex/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/prebuild-manifest.json +8 -4
- package/adapters/kimi/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/kimi/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +43 -5
- package/adapters/kimi/schemas/release-run.schema.json +35 -1
- package/adapters/kimi/skills/release-finish/SKILL.md +72 -0
- package/adapters/kimi/skills/release-verify/SKILL.md +6 -1
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/kimi/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/kimi/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/prebuild-manifest.json +8 -4
- package/adapters/workbuddy/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/release-plan.schema.json +43 -5
- package/adapters/workbuddy/schemas/release-run.schema.json +35 -1
- package/adapters/workbuddy/skills/release-finish/SKILL.md +65 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -1
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/workbuddy/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/workbuddy/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/prebuild-manifest.json +8 -4
- package/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +126 -1
- package/bin/release-skill-local-finish.mjs +4 -0
- package/bin/release-skill.bundle.mjs +7152 -4231
- package/bin/rules.json +1 -1
- package/package.json +4 -4
- package/platform-manifest.json +30 -4
- package/schemas/release-plan.schema.json +43 -5
- package/schemas/release-run.schema.json +35 -1
- package/skills/release-finish/SKILL.md +65 -0
- package/skills/release-verify/SKILL.md +6 -1
- package/skills-src/release-finish/SKILL.md +65 -0
- package/skills-src/release-verify/SKILL.md +6 -1
- package/src/commands/approve.mjs +14 -2
- package/src/commands/distribute.mjs +562 -321
- package/src/commands/post-release-local.mjs +611 -0
- package/src/commands/postverify.mjs +262 -155
- package/src/commands/prepare.mjs +83 -57
- package/src/commands/publish.mjs +11 -10
- package/src/commands/reconcile.mjs +11 -10
- package/src/commands/setup.mjs +26 -13
- package/src/commands/ship.mjs +45 -12
- package/src/commands/verify.mjs +81 -18
- package/src/core/adoption-assessment.mjs +0 -3
- package/src/core/approval.mjs +11 -8
- package/src/core/foundation-plugin-verification.mjs +247 -0
- package/src/core/hooks.mjs +0 -16
- package/src/core/plan.mjs +19 -11
- package/src/core/postpublish-approval.mjs +12 -2
- package/src/core/postpublish-bundle.mjs +51 -13
- package/src/core/postpublish.mjs +146 -6
- package/src/core/preset-executor.mjs +53 -0
- package/src/core/proposal-inbox.mjs +91 -27
- package/src/core/recovery.mjs +27 -12
- package/src/core/skill-resource-closure.mjs +8 -4
- package/src/producers/build-adapters.mjs +10 -1
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/src/schemas/host-verification-result.schema.json +2 -2
- package/src/schemas/plugin-verification-request.schema.json +374 -0
- package/src/schemas/plugin-verification-result.schema.json +799 -0
- package/src/schemas/watchdog-termination-envelope.schema.json +16 -0
package/src/commands/prepare.mjs
CHANGED
|
@@ -28,7 +28,7 @@ const execFile = promisify(execFileCb);
|
|
|
28
28
|
import { classifyPathInput, writeFileAtomic } from 'skill-family-harness-node';
|
|
29
29
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
30
30
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
31
|
-
import {
|
|
31
|
+
import { runHook } from '../core/hooks.mjs';
|
|
32
32
|
import { computeHookCacheKey, readHookCache, writeHookCache } from '../core/hook-cache.mjs';
|
|
33
33
|
import {
|
|
34
34
|
assertExpectedPublicSurface,
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT, BUNDLE_STALE } from '../core/errors.mjs';
|
|
59
59
|
import { assertBundleFreshness } from '../core/bundle-freshness.mjs';
|
|
60
60
|
import { assertAdapterFreshness, assertSelfBootstrapFacts } from '../core/derived-artifact-gates.mjs';
|
|
61
|
+
import { isFoundationPluginVerificationEligible } from '../core/foundation-plugin-verification.mjs';
|
|
61
62
|
import { PKG_ROOT } from '../core/pkg-root.mjs';
|
|
62
63
|
import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
|
|
63
64
|
import {
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
validatePostPublishDeclaration,
|
|
81
82
|
normalizePostPublishDeclaration,
|
|
82
83
|
orderNormalizedHooks,
|
|
84
|
+
validatePostPublishHookIdUniqueness,
|
|
83
85
|
PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
84
86
|
} from '../core/postpublish.mjs';
|
|
85
87
|
import { freezeExecutionBundle, bundleRootForAuthorityDir } from '../core/postpublish-bundle.mjs';
|
|
@@ -3091,38 +3093,31 @@ export async function prepareRelease(options) {
|
|
|
3091
3093
|
|
|
3092
3094
|
// --- Step 1c: postPublish distribution declaration gate (R1/R2) ---
|
|
3093
3095
|
// The per-unit postPublish block drives the post-publish distribute
|
|
3094
|
-
// command. Validate
|
|
3095
|
-
// check, or plan write, so an unsafe declaration fails
|
|
3096
|
-
// side effects. This is the runtime re-check on top of
|
|
3097
|
-
// schema: plans frozen by older schema versions must not
|
|
3098
|
-
// smuggle shell strings, option-like executables, or
|
|
3099
|
-
// keys through. R2: preset references resolve against the
|
|
3100
|
-
// preset registry (per-preset config validation +
|
|
3101
|
-
// grading), and targets normalize onto preset hooks —
|
|
3102
|
-
// table is a deterministic projection of the digest-bound
|
|
3103
|
-
// so any list change changes the plan digest and voids
|
|
3104
|
-
//
|
|
3105
|
-
//
|
|
3096
|
+
// command. Validate every declaration here, before any hook, baseline,
|
|
3097
|
+
// snapshot, remote check, or plan write, so an unsafe declaration fails
|
|
3098
|
+
// closed with zero side effects. This is the runtime re-check on top of
|
|
3099
|
+
// the config JSON schema: plans frozen by older schema versions must not
|
|
3100
|
+
// be able to smuggle shell strings, option-like executables, or
|
|
3101
|
+
// secret-ish env keys through. R2: preset references resolve against the
|
|
3102
|
+
// built-in preset registry (per-preset config validation +
|
|
3103
|
+
// requiresApproval grading), and targets normalize onto preset hooks —
|
|
3104
|
+
// the normalized table is a deterministic projection of the digest-bound
|
|
3105
|
+
// declaration, so any list change changes the plan digest and voids
|
|
3106
|
+
// approvals. Multiple units may each declare postPublish (multi-release-
|
|
3107
|
+
// unit v3): every declaration is validated individually, in plan.units
|
|
3108
|
+
// order, and the whole project's EXPLICIT hooks[].id must be unique
|
|
3109
|
+
// across units (target and internal probe local ids may repeat; the
|
|
3110
|
+
// (planDigest, hookId) approval contract needs globally unique hook ids).
|
|
3106
3111
|
const postPublishDeclarations = configUnits
|
|
3107
3112
|
.map((unit, index) => ({ unit, index }))
|
|
3108
3113
|
.filter(({ unit }) => unit.postPublish !== undefined);
|
|
3109
|
-
|
|
3110
|
-
throw new ReleaseError(
|
|
3111
|
-
GATE_FAILED,
|
|
3112
|
-
`multiple units declare postPublish (${postPublishDeclarations.map(({ unit }) => unit.id).join(', ')}); a release plan binds exactly one postPublish declaration`,
|
|
3113
|
-
{ unitIds: postPublishDeclarations.map(({ unit }) => unit.id) },
|
|
3114
|
-
);
|
|
3115
|
-
}
|
|
3116
|
-
let postPublishDeclaration = null;
|
|
3117
|
-
if (postPublishDeclarations.length === 1) {
|
|
3118
|
-
const { unit, index } = postPublishDeclarations[0];
|
|
3114
|
+
for (const { unit } of postPublishDeclarations) {
|
|
3119
3115
|
validatePostPublishDeclaration(unit.postPublish, { unitId: unit.id });
|
|
3120
3116
|
// Normalized hook table (design §2.2): validate the dependency
|
|
3121
3117
|
// topology at freeze time too, so a cyclic/dangling declaration can
|
|
3122
3118
|
// never be frozen for distribute to trip over.
|
|
3123
3119
|
const normalizedDeclaration = normalizePostPublishDeclaration(unit.postPublish);
|
|
3124
3120
|
const orderedNormalizedHooks = orderNormalizedHooks(normalizedDeclaration.hooks);
|
|
3125
|
-
postPublishDeclaration = { unit, index };
|
|
3126
3121
|
await evidence.append({
|
|
3127
3122
|
phase: 'postpublish-declaration',
|
|
3128
3123
|
status: 'validated',
|
|
@@ -3133,6 +3128,19 @@ export async function prepareRelease(options) {
|
|
|
3133
3128
|
preGates: normalizedDeclaration.preGates.map((gate) => gate.gate),
|
|
3134
3129
|
});
|
|
3135
3130
|
}
|
|
3131
|
+
// Whole-project explicit hooks[].id uniqueness (design §9.2 rule 3;
|
|
3132
|
+
// rework R-02): the (planDigest, hookId) approval contract binds every
|
|
3133
|
+
// explicit hook id plan-wide, so a duplicate across units must fail
|
|
3134
|
+
// before any plan write and before any project hook runs. The single
|
|
3135
|
+
// array-level authority lives in core/postpublish.mjs
|
|
3136
|
+
// validatePostPublishHookIdUniqueness — this entry only normalizes its
|
|
3137
|
+
// input to the declaration array view (config blocks carry no unitId;
|
|
3138
|
+
// the owning unit id is bound here, exactly as the frozen plan does) and
|
|
3139
|
+
// calls it. Per-declaration duplicates are already rejected by
|
|
3140
|
+
// validatePostPublishDeclaration.
|
|
3141
|
+
validatePostPublishHookIdUniqueness(
|
|
3142
|
+
postPublishDeclarations.map(({ unit }) => ({ ...unit.postPublish, unitId: unit.id })),
|
|
3143
|
+
);
|
|
3136
3144
|
|
|
3137
3145
|
// --- Step 2: Hook authorization gate ---
|
|
3138
3146
|
// Hooks are user-configured arbitrary local processes without filesystem
|
|
@@ -3840,20 +3848,21 @@ export async function prepareRelease(options) {
|
|
|
3840
3848
|
});
|
|
3841
3849
|
}
|
|
3842
3850
|
|
|
3843
|
-
// New prepares emit planVersion
|
|
3844
|
-
//
|
|
3845
|
-
//
|
|
3846
|
-
//
|
|
3847
|
-
//
|
|
3848
|
-
// every unit's
|
|
3849
|
-
//
|
|
3850
|
-
//
|
|
3851
|
-
//
|
|
3852
|
-
//
|
|
3853
|
-
//
|
|
3854
|
-
//
|
|
3855
|
-
// the
|
|
3856
|
-
//
|
|
3851
|
+
// New prepares emit planVersion 3 (multi-release-unit postPublish v3;
|
|
3852
|
+
// v3 inherits the v2 record-layer freeze-timestamp semantics from
|
|
3853
|
+
// t1-2-digest-decoupling.md §4.2/§7). Production freeze timestamps are
|
|
3854
|
+
// derived deterministically from the baseline headCommit's committer
|
|
3855
|
+
// date, before the first frozen Git object exists. This single canonical
|
|
3856
|
+
// value becomes GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for every unit's
|
|
3857
|
+
// frozen commit and every unit's frozenSnapshot.commitTimestamp;
|
|
3858
|
+
// identical sources freeze byte-identical Git objects on every
|
|
3859
|
+
// re-prepare. The wall-clock sample is still validated here (fail closed
|
|
3860
|
+
// before any Git write) and becomes plan.createdAt -- record-layer real
|
|
3861
|
+
// clock behind the 24h approval window, no longer equal to the freeze
|
|
3862
|
+
// timestamp for v2/v3 plans. The v1 legacy path used this same sample as
|
|
3863
|
+
// the freeze timestamp itself (commitTimestamp == createdAt). publish,
|
|
3864
|
+
// retry, and reconcile consume the frozen value from the plan and never
|
|
3865
|
+
// re-read the wall clock or re-derive it.
|
|
3857
3866
|
const createdAtTimestamp = production
|
|
3858
3867
|
? normalizeGitTimestamp(clock ? clock() : new Date().toISOString(), 'plan createdAt timestamp')
|
|
3859
3868
|
: null;
|
|
@@ -4235,22 +4244,36 @@ export async function prepareRelease(options) {
|
|
|
4235
4244
|
});
|
|
4236
4245
|
}
|
|
4237
4246
|
|
|
4238
|
-
// Detect human consumer platforms (Kimi/CodeBuddy) in the plan.
|
|
4239
|
-
//
|
|
4240
|
-
|
|
4247
|
+
// Detect human consumer platforms (Kimi/CodeBuddy) in the plan. Foundation
|
|
4248
|
+
// 0.13 can observe their complete local payload only when production has
|
|
4249
|
+
// frozen a bundled-family source. Standalone marketplace sources remain on
|
|
4250
|
+
// the legacy manual-follow-up path because Foundation exposes no public
|
|
4251
|
+
// Kimi/WorkBuddy public-channel driver in this release.
|
|
4252
|
+
const humanConsumerActions = externalActions.filter(
|
|
4241
4253
|
(a) => a.type === 'kimi-marketplace-install' || a.type === 'codebuddy-marketplace-install',
|
|
4242
4254
|
);
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4255
|
+
const humanConsumersStrategy = humanConsumerActions.length === 0
|
|
4256
|
+
? null
|
|
4257
|
+
: humanConsumerActions.some((action) => (
|
|
4258
|
+
isFoundationPluginVerificationEligible({ action, units })
|
|
4259
|
+
))
|
|
4260
|
+
? 'foundationPayloadThenManualFollowUps'
|
|
4261
|
+
: 'manualFollowUps';
|
|
4262
|
+
|
|
4263
|
+
// --- Fold the validated postPublish declarations into the frozen plan ---
|
|
4264
|
+
// Multi-release-unit v3 contract: the frozen field is ALWAYS an array,
|
|
4265
|
+
// one entry per declaring unit in plan.units order (zero declarations
|
|
4266
|
+
// freeze the empty array, which never enters post-release execution).
|
|
4267
|
+
// Bindings per entry: tag (tagTemplate rendered at the resolved target
|
|
4268
|
+
// version — the same computation create-tag will use), tagCommit (the
|
|
4269
|
+
// frozen production asset commit the tag will point at; only production
|
|
4248
4270
|
// prepares can freeze it — distribute fails closed when it is absent),
|
|
4249
4271
|
// unitId (declaring unit), and payloadSource "tag-worktree" (R1 timing
|
|
4250
4272
|
// contract: payload may only come from the detached worktree at
|
|
4251
|
-
// tagCommit, never from workspace state).
|
|
4252
|
-
//
|
|
4253
|
-
//
|
|
4273
|
+
// tagCommit, never from workspace state). The array and every entry
|
|
4274
|
+
// participate in the plan digest through the existing digest mechanism
|
|
4275
|
+
// (no per-entry summaries, maps, or second manifest), so declaration
|
|
4276
|
+
// order and any bound field change change the plan digest.
|
|
4254
4277
|
//
|
|
4255
4278
|
// F-01 / T1 private execution bundle: parent-workspace files that the
|
|
4256
4279
|
// post-publish commands need but the frozen tag does not contain are
|
|
@@ -4260,9 +4283,8 @@ export async function prepareRelease(options) {
|
|
|
4260
4283
|
// plan is the bundle's only source of truth (no parallel manifest, no
|
|
4261
4284
|
// second bundle digest): the raw executionFiles list folds into the
|
|
4262
4285
|
// closure and is NOT duplicated into the frozen block.
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
const { unit, index } = postPublishDeclaration;
|
|
4286
|
+
const frozenPostPublish = [];
|
|
4287
|
+
for (const { unit, index } of postPublishDeclarations) {
|
|
4266
4288
|
const { tag } = resolveProductionBranch(unit, resolvedVersions[index]);
|
|
4267
4289
|
const declaredExecutionFiles = unit.postPublish.executionFiles ?? [];
|
|
4268
4290
|
const frozenTagPaths = productionAssets
|
|
@@ -4289,18 +4311,22 @@ export async function prepareRelease(options) {
|
|
|
4289
4311
|
bundleRoot: relative(realRoot, bundleRootForAuthorityDir(releaseDir)),
|
|
4290
4312
|
});
|
|
4291
4313
|
const { executionFiles: _executionFiles, ...declarationWithoutManifest } = structuredClone(unit.postPublish);
|
|
4292
|
-
frozenPostPublish
|
|
4314
|
+
frozenPostPublish.push({
|
|
4293
4315
|
...declarationWithoutManifest,
|
|
4294
4316
|
tag,
|
|
4295
4317
|
...(productionAssets ? { tagCommit: productionAssets[index].commit } : {}),
|
|
4296
4318
|
unitId: unit.id,
|
|
4297
4319
|
payloadSource: PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
4298
4320
|
executionBundle,
|
|
4299
|
-
};
|
|
4321
|
+
});
|
|
4300
4322
|
}
|
|
4301
4323
|
|
|
4302
4324
|
const plan = {
|
|
4303
|
-
planVersion:
|
|
4325
|
+
// New prepares emit planVersion 3 (multi-release-unit postPublish v3):
|
|
4326
|
+
// postPublish is the declaration ARRAY (possibly empty). v3 inherits
|
|
4327
|
+
// the v2 record-layer digest and approval semantics; only the
|
|
4328
|
+
// postPublish shape changes.
|
|
4329
|
+
planVersion: 3,
|
|
4304
4330
|
status: 'PREPARED',
|
|
4305
4331
|
// Workflow profile (H5): 'full' for the complete gate set;
|
|
4306
4332
|
// 'docs'/'config'/'marketplace' for trimmed code-class gates. Both
|
|
@@ -4331,7 +4357,7 @@ export async function prepareRelease(options) {
|
|
|
4331
4357
|
: {}),
|
|
4332
4358
|
verificationGates: config.verificationGates ?? [],
|
|
4333
4359
|
snapshotDigest: overallSnapshotDigest,
|
|
4334
|
-
...(
|
|
4360
|
+
...(humanConsumersStrategy ? { humanConsumersStrategy } : {}),
|
|
4335
4361
|
...(production ? {
|
|
4336
4362
|
production: {
|
|
4337
4363
|
mode: 'github-npm-v1',
|
|
@@ -4341,7 +4367,7 @@ export async function prepareRelease(options) {
|
|
|
4341
4367
|
units,
|
|
4342
4368
|
externalActions,
|
|
4343
4369
|
...(publicSourceAuthorityReceipt ? { publicSourceAuthorityReceipt } : {}),
|
|
4344
|
-
|
|
4370
|
+
postPublish: frozenPostPublish,
|
|
4345
4371
|
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
4346
4372
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
4347
4373
|
};
|
package/src/commands/publish.mjs
CHANGED
|
@@ -791,21 +791,22 @@ export async function publishRelease(options) {
|
|
|
791
791
|
|
|
792
792
|
const currentBaseline = await captureBaselineActual(root);
|
|
793
793
|
|
|
794
|
-
// planVersion fork (design: t1-2-digest-decoupling.md §4.3): for
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
794
|
+
// planVersion fork (design: t1-2-digest-decoupling.md §4.3): for
|
|
795
|
+
// planVersion 2/3 plans (v3 inherits the v2 semantics per 多发布单元
|
|
796
|
+
// postPublish v3 §4.2) the baseline is record-layer audit data. Drift is
|
|
797
|
+
// recorded as a warning and execution continues -- artifact integrity is
|
|
798
|
+
// sealed by the frozen-artifact re-verification above, never by workspace
|
|
799
|
+
// equality. v1 plans keep the BASELINE_CHANGED hard failure, byte for byte.
|
|
800
|
+
const recordLayerBaseline = plan.planVersion === 2 || plan.planVersion === 3;
|
|
800
801
|
|
|
801
802
|
if (currentBaseline.gitTreeHash !== plan.baseline.gitTreeHash) {
|
|
802
|
-
if (
|
|
803
|
+
if (recordLayerBaseline) {
|
|
803
804
|
await evidence.append({
|
|
804
805
|
phase: 'safety-gate',
|
|
805
806
|
gate: 'baseline-check',
|
|
806
807
|
status: 'warning',
|
|
807
808
|
severity: 'warning',
|
|
808
|
-
reason: 'planVersion 2: baseline drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
809
|
+
reason: 'planVersion 2/3: baseline drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
809
810
|
planTreeHash: plan.baseline.gitTreeHash,
|
|
810
811
|
currentTreeHash: currentBaseline.gitTreeHash,
|
|
811
812
|
});
|
|
@@ -831,13 +832,13 @@ export async function publishRelease(options) {
|
|
|
831
832
|
plan.baseline.workspaceDigest &&
|
|
832
833
|
currentBaseline.workspaceDigest !== plan.baseline.workspaceDigest
|
|
833
834
|
) {
|
|
834
|
-
if (
|
|
835
|
+
if (recordLayerBaseline) {
|
|
835
836
|
await evidence.append({
|
|
836
837
|
phase: 'safety-gate',
|
|
837
838
|
gate: 'baseline-check',
|
|
838
839
|
status: 'warning',
|
|
839
840
|
severity: 'warning',
|
|
840
|
-
reason: 'planVersion 2: workspace digest drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
841
|
+
reason: 'planVersion 2/3: workspace digest drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
841
842
|
planWorkspaceDigest: plan.baseline.workspaceDigest,
|
|
842
843
|
currentWorkspaceDigest: currentBaseline.workspaceDigest,
|
|
843
844
|
});
|
|
@@ -357,21 +357,22 @@ export async function reconcileRelease(options) {
|
|
|
357
357
|
|
|
358
358
|
const currentBaseline = await captureBaselineActual(root);
|
|
359
359
|
|
|
360
|
-
// planVersion fork (design: t1-2-digest-decoupling.md §4.3): for
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
|
|
360
|
+
// planVersion fork (design: t1-2-digest-decoupling.md §4.3): for
|
|
361
|
+
// planVersion 2/3 plans (v3 inherits the v2 semantics per 多发布单元
|
|
362
|
+
// postPublish v3 §4.2) the baseline is record-layer audit data. Drift is
|
|
363
|
+
// recorded as a warning and execution continues -- artifact integrity is
|
|
364
|
+
// sealed by the frozen-artifact re-verification, never by workspace
|
|
365
|
+
// equality. v1 plans keep the BASELINE_CHANGED hard failure, byte for byte.
|
|
366
|
+
const recordLayerBaseline = plan.planVersion === 2 || plan.planVersion === 3;
|
|
366
367
|
|
|
367
368
|
if (currentBaseline.gitTreeHash !== plan.baseline.gitTreeHash) {
|
|
368
|
-
if (
|
|
369
|
+
if (recordLayerBaseline) {
|
|
369
370
|
await evidence.append({
|
|
370
371
|
phase: 'safety-gate',
|
|
371
372
|
gate: 'baseline-check',
|
|
372
373
|
status: 'warning',
|
|
373
374
|
severity: 'warning',
|
|
374
|
-
reason: 'planVersion 2: baseline drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
375
|
+
reason: 'planVersion 2/3: baseline drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
375
376
|
planTreeHash: plan.baseline.gitTreeHash,
|
|
376
377
|
currentTreeHash: currentBaseline.gitTreeHash,
|
|
377
378
|
});
|
|
@@ -396,13 +397,13 @@ export async function reconcileRelease(options) {
|
|
|
396
397
|
plan.baseline.workspaceDigest &&
|
|
397
398
|
currentBaseline.workspaceDigest !== plan.baseline.workspaceDigest
|
|
398
399
|
) {
|
|
399
|
-
if (
|
|
400
|
+
if (recordLayerBaseline) {
|
|
400
401
|
await evidence.append({
|
|
401
402
|
phase: 'safety-gate',
|
|
402
403
|
gate: 'baseline-check',
|
|
403
404
|
status: 'warning',
|
|
404
405
|
severity: 'warning',
|
|
405
|
-
reason: 'planVersion 2: workspace digest drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
406
|
+
reason: 'planVersion 2/3: workspace digest drift is record-layer audit data; frozen-artifact re-verification remains the integrity authority',
|
|
406
407
|
planWorkspaceDigest: plan.baseline.workspaceDigest,
|
|
407
408
|
currentWorkspaceDigest: currentBaseline.workspaceDigest,
|
|
408
409
|
});
|
package/src/commands/setup.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from '../core/errors.mjs';
|
|
35
35
|
import { readTrustedPackageResource } from '../core/trusted-resource.mjs';
|
|
36
36
|
import { validatePresetHook } from '../core/presets.mjs';
|
|
37
|
-
import { validatePostPublishDeclaration } from '../core/postpublish.mjs';
|
|
37
|
+
import { validatePostPublishDeclaration, validatePostPublishHookIdUniqueness } from '../core/postpublish.mjs';
|
|
38
38
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
39
39
|
import { resolveProducerVersion } from '../core/evidence.mjs';
|
|
40
40
|
import { validateHook } from '../core/hooks.mjs';
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
deriveGateSuggestions,
|
|
47
47
|
deriveHookDurations,
|
|
48
48
|
deriveLongHookSuggestions,
|
|
49
|
-
findPostPublishUnitConflict,
|
|
50
49
|
scanGateDeclarationFindings,
|
|
51
50
|
FINDING_CATEGORY,
|
|
52
51
|
ASSESSMENT_STATUS,
|
|
@@ -3101,17 +3100,31 @@ export async function assessAdoption({ root } = {}) {
|
|
|
3101
3100
|
evidence: { configDigest },
|
|
3102
3101
|
}));
|
|
3103
3102
|
|
|
3104
|
-
//
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
}));
|
|
3103
|
+
// 多发布单元 postPublish v3(设计 §9.2/§9.7;返工 R-02):多个发布单元
|
|
3104
|
+
// 各自声明 postPublish 本身不再构成缺口——每个声明的字段级校验已在上面
|
|
3105
|
+
// 逐项完成。仍失败关闭的只有:声明内容无效(POSTPUBLISH_INVALID,逐项
|
|
3106
|
+
// 报告)与整份计划显式 hooks[].id 跨单元重复((planDigest, hookId) 批准
|
|
3107
|
+
// 合同要求全局唯一;target 与内部 probe 的本地 ID 可跨单元重复,检查点
|
|
3108
|
+
// 以 unitId 区分)。唯一校验真源是 core/postpublish.mjs 的
|
|
3109
|
+
// validatePostPublishHookIdUniqueness——本入口只做输入归一(收集各单元的
|
|
3110
|
+
// 声明块)并调用同一函数,再把领域事实转成评估 finding。
|
|
3111
|
+
const declaredPostPublishBlocks = declaredUnits
|
|
3112
|
+
.filter((unit) => unit.postPublish !== undefined)
|
|
3113
|
+
.map((unit) => ({ ...unit.postPublish, unitId: unit.id }));
|
|
3114
|
+
try {
|
|
3115
|
+
validatePostPublishHookIdUniqueness(declaredPostPublishBlocks);
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
if (error?.details?.hookId !== undefined && Array.isArray(error?.details?.unitIds)) {
|
|
3118
|
+
const [owner, unitId] = error.details.unitIds;
|
|
3119
|
+
findings.push(createFinding({
|
|
3120
|
+
category: FINDING_CATEGORY.MANDATORY_GAP, code: 'POSTPUBLISH_HOOK_ID_DUPLICATE',
|
|
3121
|
+
unitId, fieldPath: 'releaseUnits[].postPublish.hooks[].id',
|
|
3122
|
+
message: `postPublish 显式 hook id "${error.details.hookId}" 同时由发布单元 "${owner}" 与 "${unitId}" 声明;整份计划的显式 hooks[].id 必须唯一(target 与内部 probe 的本地 ID 可跨单元重复)。`,
|
|
3123
|
+
evidence: { hookId: error.details.hookId, unitIds: error.details.unitIds },
|
|
3124
|
+
action: '为重复的显式 hook id 改名(或合并声明)后重新评估。',
|
|
3125
|
+
severity: 'blocking',
|
|
3126
|
+
}));
|
|
3127
|
+
}
|
|
3115
3128
|
}
|
|
3116
3129
|
|
|
3117
3130
|
// 裁决 20: 评估复用实际 Hook 校验(core/hooks.mjs 的纯校验函数,不调用
|
package/src/commands/ship.mjs
CHANGED
|
@@ -3,7 +3,15 @@ import { basename, dirname, resolve } from 'node:path';
|
|
|
3
3
|
import { publishFileOrReplace } from 'skill-family-harness-node';
|
|
4
4
|
|
|
5
5
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
effectiveHookRequiresApproval,
|
|
8
|
+
normalizePostPublishView,
|
|
9
|
+
postPublishActionId,
|
|
10
|
+
} from '../core/postpublish.mjs';
|
|
11
|
+
import {
|
|
12
|
+
derivePostReleaseChecklist,
|
|
13
|
+
unavailablePostReleaseChecklist,
|
|
14
|
+
} from './post-release-local.mjs';
|
|
7
15
|
import {
|
|
8
16
|
ReleaseError,
|
|
9
17
|
GATE_FAILED,
|
|
@@ -101,11 +109,13 @@ function publicState(state) {
|
|
|
101
109
|
...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
|
|
102
110
|
...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
|
|
103
111
|
...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
|
|
112
|
+
...(state.verifyRunPath ? { verifyRunPath: state.verifyRunPath } : {}),
|
|
104
113
|
...(state.distributeRunPath ? { distributeRunPath: state.distributeRunPath } : {}),
|
|
105
114
|
...(state.postVerify ? { postVerify: state.postVerify } : {}),
|
|
106
115
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
107
116
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
108
117
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
118
|
+
...(state.postRelease ? { postRelease: state.postRelease } : {}),
|
|
109
119
|
verificationGateAuthorizationIncludedInPlanApproval: true,
|
|
110
120
|
postVerifyMetadataUpdateIncludedInPlanApproval: true,
|
|
111
121
|
};
|
|
@@ -146,11 +156,19 @@ async function buildApprovalSummary(planPath) {
|
|
|
146
156
|
* or missing record falls back to ALL declared hooks — fail-safe, because a
|
|
147
157
|
* gated hook in doubt keeps the gate shut.
|
|
148
158
|
*
|
|
159
|
+
* Checkpoint action ids are unit-scoped for planVersion 3
|
|
160
|
+
* (`unitId/localId`, postPublishActionId) and bare local ids for legacy
|
|
161
|
+
* plans, so the closed-set comparison must derive the id from the owning
|
|
162
|
+
* declaration instead of comparing the bare hook id — otherwise every v3
|
|
163
|
+
* hook looks unclosed and a closed gated hook keeps the gate shut.
|
|
164
|
+
*
|
|
149
165
|
* @param {object} state - Ship state (postVerify.runPath inspected).
|
|
150
|
-
* @param {object
|
|
166
|
+
* @param {Array<{hook: object, unitId: string}>} postVerifyHooks - Declared
|
|
167
|
+
* phase:postVerify hooks bound to their owning declaration's unitId.
|
|
168
|
+
* @param {number} planVersion - Frozen plan's planVersion (action-id rule).
|
|
151
169
|
* @returns {Promise<boolean>}
|
|
152
170
|
*/
|
|
153
|
-
async function allUnclosedPostVerifyHooksUngated(state, postVerifyHooks) {
|
|
171
|
+
async function allUnclosedPostVerifyHooksUngated(state, postVerifyHooks, planVersion) {
|
|
154
172
|
if (!Array.isArray(postVerifyHooks) || postVerifyHooks.length === 0) return false;
|
|
155
173
|
let candidates = postVerifyHooks;
|
|
156
174
|
if (state.postVerify?.runPath) {
|
|
@@ -163,13 +181,14 @@ async function allUnclosedPostVerifyHooksUngated(state, postVerifyHooks) {
|
|
|
163
181
|
&& (cp.status === 'succeeded' || cp.status === 'NO_CHANGE'))
|
|
164
182
|
.map((cp) => cp.actionId),
|
|
165
183
|
);
|
|
166
|
-
candidates = postVerifyHooks.filter((hook) =>
|
|
184
|
+
candidates = postVerifyHooks.filter(({ hook, unitId }) =>
|
|
185
|
+
!closedIds.has(postPublishActionId({ planVersion, unitId, localId: hook.id })));
|
|
167
186
|
} catch {
|
|
168
187
|
candidates = postVerifyHooks; // unreadable record: fail safe
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
if (candidates.length === 0) return false;
|
|
172
|
-
return candidates.every((hook) => effectiveHookRequiresApproval(hook) === false);
|
|
191
|
+
return candidates.every(({ hook }) => effectiveHookRequiresApproval(hook) === false);
|
|
173
192
|
}
|
|
174
193
|
|
|
175
194
|
/**
|
|
@@ -344,10 +363,12 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
344
363
|
if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
|
|
345
364
|
// Step 1: Check if postPublish requires distribution.
|
|
346
365
|
// Hooks-only declarations (no targets) still route through distribute.
|
|
366
|
+
// §4.3 unified normalization: v3 empty arrays carry no distribute work;
|
|
367
|
+
// legacy absent postPublish resolves to the same empty view.
|
|
347
368
|
const plan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
348
|
-
const hasDistributeWork = (plan.
|
|
349
|
-
|| (
|
|
350
|
-
const needsDistribution =
|
|
369
|
+
const hasDistributeWork = normalizePostPublishView(plan).some((declaration) =>
|
|
370
|
+
(declaration.targets?.length ?? 0) > 0 || (declaration.hooks?.length ?? 0) > 0);
|
|
371
|
+
const needsDistribution = hasDistributeWork;
|
|
351
372
|
if (needsDistribution && deps.distributeRelease) {
|
|
352
373
|
state.status = 'DISTRIBUTING';
|
|
353
374
|
await writeJsonAtomic(statePath, state);
|
|
@@ -404,9 +425,19 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
404
425
|
requirements: undefined,
|
|
405
426
|
manualFollowUps: verified.manualFollowUps ?? undefined,
|
|
406
427
|
baselineAdvance: verified.baselineAdvance ?? undefined,
|
|
428
|
+
postRelease: undefined,
|
|
407
429
|
updatedAt: new Date().toISOString(),
|
|
408
430
|
};
|
|
409
431
|
await writeJsonAtomic(statePath, state);
|
|
432
|
+
if (verified.status === 'VERIFIED') {
|
|
433
|
+
try {
|
|
434
|
+
state.postRelease = derivePostReleaseChecklist(plan);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
state.postRelease = unavailablePostReleaseChecklist(plan, error);
|
|
437
|
+
}
|
|
438
|
+
state.updatedAt = new Date().toISOString();
|
|
439
|
+
await writeJsonAtomic(statePath, state);
|
|
440
|
+
}
|
|
410
441
|
} catch (error) {
|
|
411
442
|
if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
|
|
412
443
|
state = {
|
|
@@ -422,7 +453,8 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
422
453
|
// independent run after the main run is VERIFIED, with the verify run as
|
|
423
454
|
// lineage source. A PARTIAL postVerify run or a postVerify gate failure
|
|
424
455
|
// NEVER demotes VERIFIED — the outcome is recorded on the ship state.
|
|
425
|
-
const postVerifyHooks = (plan
|
|
456
|
+
const postVerifyHooks = normalizePostPublishView(plan)
|
|
457
|
+
.flatMap((declaration) => declaration.hooks ?? [])
|
|
426
458
|
.filter((hook) => hook.phase === 'postVerify');
|
|
427
459
|
if (state.status === 'VERIFIED' && postVerifyHooks.length > 0 && deps.postVerifyRelease) {
|
|
428
460
|
postVerifyRanThisCall = true;
|
|
@@ -476,10 +508,11 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
476
508
|
&& (!state.postVerify || state.postVerify.status !== 'DISTRIBUTED')
|
|
477
509
|
) {
|
|
478
510
|
const reentryPlan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
479
|
-
const reentryPostVerifyHooks = (reentryPlan
|
|
480
|
-
.
|
|
511
|
+
const reentryPostVerifyHooks = normalizePostPublishView(reentryPlan)
|
|
512
|
+
.flatMap((declaration) => (declaration.hooks ?? []).map((hook) => ({ hook, unitId: declaration.unitId })))
|
|
513
|
+
.filter(({ hook }) => hook.phase === 'postVerify');
|
|
481
514
|
const approvallessRetryAllowed = reentryApprovalPaths.length === 0
|
|
482
|
-
&& await allUnclosedPostVerifyHooksUngated(state, reentryPostVerifyHooks);
|
|
515
|
+
&& await allUnclosedPostVerifyHooksUngated(state, reentryPostVerifyHooks, reentryPlan.planVersion);
|
|
483
516
|
if (reentryPostVerifyHooks.length > 0
|
|
484
517
|
&& (reentryApprovalPaths.length > 0 || approvallessRetryAllowed)) {
|
|
485
518
|
let postVerifyOutcome;
|