release-skill 0.3.0 → 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 +23 -3
- package/INSTALL.md +34 -110
- package/INSTALL.zh-CN.md +17 -83
- package/README.md +42 -65
- package/README.zh-CN.md +37 -57
- 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 +138 -231
- 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 +5 -5
- 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 +138 -231
- 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 +5 -5
- 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 +138 -231
- 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 +5 -5
- 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 +138 -231
- 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 +5 -5
- 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 +45 -48
- package/bin/release-skill.bundle.mjs +138 -231
- 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 +5 -5
- 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 +5 -5
- 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/commands/approve.mjs +8 -6
- package/src/commands/hooks.mjs +4 -8
- 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 +55 -77
- package/src/commands/verify.mjs +56 -19
- package/src/core/skill-resource-closure.mjs +7 -1
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,
|
package/src/commands/ship.mjs
CHANGED
|
@@ -90,8 +90,7 @@ function publicState(state) {
|
|
|
90
90
|
status: state.status,
|
|
91
91
|
statePath: state.statePath,
|
|
92
92
|
targetVersion: state.targetVersion,
|
|
93
|
-
...(state.
|
|
94
|
-
hookAuthorizationDigest: state.hookAuthorizationDigest,
|
|
93
|
+
...(state.hooks && state.hooks.length > 0 ? {
|
|
95
94
|
hooks: state.hooks,
|
|
96
95
|
} : {}),
|
|
97
96
|
...(state.planPath ? {
|
|
@@ -100,34 +99,52 @@ function publicState(state) {
|
|
|
100
99
|
evidenceDir: state.evidenceDir,
|
|
101
100
|
warnings: state.warnings ?? [],
|
|
102
101
|
} : {}),
|
|
102
|
+
...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
|
|
103
103
|
...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
|
|
104
104
|
...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
|
|
105
105
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
106
|
+
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
106
107
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
107
|
-
verificationGateAuthorizationIncludedInPlanApproval:
|
|
108
|
-
|
|
109
|
-
postVerifyMetadataUpdateIncludedInPlanApproval:
|
|
110
|
-
state.status !== 'NEEDS_HOOK_AUTHORIZATION',
|
|
108
|
+
verificationGateAuthorizationIncludedInPlanApproval: true,
|
|
109
|
+
postVerifyMetadataUpdateIncludedInPlanApproval: true,
|
|
111
110
|
};
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Build a human-readable approval summary from the frozen plan.
|
|
115
|
+
* Lists each unit's version and every external action's id/type/unitId.
|
|
116
|
+
* Returns an empty summary when the plan file is not yet available (e.g., in tests).
|
|
117
|
+
*
|
|
118
|
+
* @param {string} planPath - Path to the frozen release plan.
|
|
119
|
+
* @returns {Promise<object>} The approval summary.
|
|
120
|
+
*/
|
|
121
|
+
async function buildApprovalSummary(planPath) {
|
|
122
|
+
try {
|
|
123
|
+
const plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
124
|
+
const units = (plan.units ?? []).map((unit) => ({
|
|
125
|
+
id: unit.id,
|
|
126
|
+
targetVersion: unit.targetVersion ?? unit.version,
|
|
127
|
+
}));
|
|
128
|
+
const actions = (plan.externalActions ?? []).map((action) => ({
|
|
129
|
+
id: action.id,
|
|
130
|
+
type: action.type,
|
|
131
|
+
unitId: action.unitId,
|
|
132
|
+
}));
|
|
133
|
+
return { units, actions };
|
|
134
|
+
} catch {
|
|
135
|
+
return { units: [], actions: [] };
|
|
136
|
+
}
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
/**
|
|
128
140
|
* Advance one durable production release. Re-running is safe: the state file
|
|
129
141
|
* carries the immutable plan, approval and source-run paths so the command
|
|
130
142
|
* resumes instead of reconstructing authority from terminal/chat output.
|
|
143
|
+
*
|
|
144
|
+
* New flow (v0.4+): ship directly runs configured hooks and verification gates
|
|
145
|
+
* without a separate hook authorization step. The only human gate is plan
|
|
146
|
+
* approval. Kimi/CodeBuddy installations are non-blocking manual follow-up
|
|
147
|
+
* tasks when the plan declares humanConsumersStrategy: 'manualFollowUps'.
|
|
131
148
|
*/
|
|
132
149
|
export async function advanceShip(options = {}, injected = {}) {
|
|
133
150
|
const root = resolve(options.root ?? process.cwd());
|
|
@@ -144,69 +161,25 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
144
161
|
|
|
145
162
|
if (!state) {
|
|
146
163
|
const loaded = await deps.loadProjectConfig({ root });
|
|
147
|
-
const
|
|
148
|
-
const hooks = authority.hooks;
|
|
149
|
-
const hookAuthorizationDigest = authority.digest;
|
|
164
|
+
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
150
165
|
state = {
|
|
151
|
-
schemaVersion:
|
|
166
|
+
schemaVersion: 2,
|
|
152
167
|
root,
|
|
153
168
|
statePath,
|
|
154
169
|
targetVersion: options.targetVersion ?? null,
|
|
155
170
|
configDigest: loaded.configDigest,
|
|
156
171
|
hooks,
|
|
157
|
-
|
|
158
|
-
status: hooks.length > 0 ? 'NEEDS_HOOK_AUTHORIZATION' : 'NEW',
|
|
172
|
+
status: 'NEW',
|
|
159
173
|
createdAt: new Date().toISOString(),
|
|
160
174
|
updatedAt: new Date().toISOString(),
|
|
161
175
|
};
|
|
162
176
|
await writeJsonAtomic(statePath, state);
|
|
163
|
-
if (hooks.length > 0 && options.hookAuthorizationDigest !== hookAuthorizationDigest) {
|
|
164
|
-
return publicState(state);
|
|
165
|
-
}
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
// Backward compatibility: auto-recover from legacy NEEDS_HOOK_AUTHORIZATION
|
|
180
|
+
// state. Old state files may have this status from the previous two-gate
|
|
181
|
+
// flow; the new flow runs hooks directly so we advance to NEW immediately.
|
|
168
182
|
if (state.status === 'NEEDS_HOOK_AUTHORIZATION') {
|
|
169
|
-
const loaded = await deps.loadProjectConfig({ root });
|
|
170
|
-
const current = hookAuthority(loaded, state.targetVersion);
|
|
171
|
-
if (
|
|
172
|
-
loaded.configDigest !== state.configDigest
|
|
173
|
-
|| current.digest !== state.hookAuthorizationDigest
|
|
174
|
-
) {
|
|
175
|
-
state = {
|
|
176
|
-
...state,
|
|
177
|
-
configDigest: loaded.configDigest,
|
|
178
|
-
hooks: current.hooks,
|
|
179
|
-
hookAuthorizationDigest: current.digest,
|
|
180
|
-
updatedAt: new Date().toISOString(),
|
|
181
|
-
};
|
|
182
|
-
await writeJsonAtomic(statePath, state);
|
|
183
|
-
if (verified.status === 'VERIFIED' && deps.updatePreviousPublicBaselines) {
|
|
184
|
-
try {
|
|
185
|
-
state.metadataUpdate = await deps.updatePreviousPublicBaselines({
|
|
186
|
-
root,
|
|
187
|
-
planPath: state.planPath,
|
|
188
|
-
});
|
|
189
|
-
} catch (error) {
|
|
190
|
-
state.metadataUpdate = {
|
|
191
|
-
status: 'FAILED',
|
|
192
|
-
error: error.message,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
state.updatedAt = new Date().toISOString();
|
|
196
|
-
await writeJsonAtomic(statePath, state);
|
|
197
|
-
}
|
|
198
|
-
return publicState(state);
|
|
199
|
-
}
|
|
200
|
-
if (options.hookAuthorizationDigest !== state.hookAuthorizationDigest) {
|
|
201
|
-
if (options.hookAuthorizationDigest) {
|
|
202
|
-
throw new ReleaseError(
|
|
203
|
-
PLAN_DIGEST_MISMATCH,
|
|
204
|
-
'hook authorization digest does not match the current config, hooks and target version',
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
return publicState(state);
|
|
208
|
-
}
|
|
209
|
-
state.hookAuthorizedBy = options.actor ?? null;
|
|
210
183
|
state.status = 'NEW';
|
|
211
184
|
state.updatedAt = new Date().toISOString();
|
|
212
185
|
await writeJsonAtomic(statePath, state);
|
|
@@ -215,17 +188,15 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
215
188
|
if (state.status === 'NEW') {
|
|
216
189
|
const loaded = await deps.loadProjectConfig({ root });
|
|
217
190
|
if (loaded.configDigest !== state.configDigest) {
|
|
218
|
-
const
|
|
191
|
+
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
219
192
|
state = {
|
|
220
193
|
...state,
|
|
221
194
|
configDigest: loaded.configDigest,
|
|
222
|
-
hooks
|
|
223
|
-
|
|
224
|
-
status: current.hooks.length > 0 ? 'NEEDS_HOOK_AUTHORIZATION' : 'NEW',
|
|
195
|
+
hooks,
|
|
196
|
+
status: 'NEW',
|
|
225
197
|
updatedAt: new Date().toISOString(),
|
|
226
198
|
};
|
|
227
199
|
await writeJsonAtomic(statePath, state);
|
|
228
|
-
if (state.status === 'NEEDS_HOOK_AUTHORIZATION') return publicState(state);
|
|
229
200
|
}
|
|
230
201
|
const prepared = await deps.prepareRelease({
|
|
231
202
|
root,
|
|
@@ -233,7 +204,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
233
204
|
offline: false,
|
|
234
205
|
production: true,
|
|
235
206
|
hooksAuthorized: true,
|
|
236
|
-
verificationGatesAuthorized:
|
|
207
|
+
verificationGatesAuthorized: true,
|
|
237
208
|
hookCache: true,
|
|
238
209
|
});
|
|
239
210
|
let transportPreflight = null;
|
|
@@ -242,6 +213,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
242
213
|
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
243
214
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
244
215
|
}
|
|
216
|
+
const approvalSummary = await buildApprovalSummary(prepared.planPath);
|
|
245
217
|
state = {
|
|
246
218
|
...state,
|
|
247
219
|
status: 'NEEDS_PLAN_APPROVAL',
|
|
@@ -249,6 +221,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
249
221
|
planDigest: prepared.planDigest,
|
|
250
222
|
evidenceDir: prepared.evidenceDir,
|
|
251
223
|
warnings: prepared.warnings,
|
|
224
|
+
approvalSummary,
|
|
252
225
|
...(transportPreflight ? {
|
|
253
226
|
gitTransport: transportPreflight.transport,
|
|
254
227
|
gitTransportPreflight: transportPreflight.repositories,
|
|
@@ -259,8 +232,14 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
259
232
|
}
|
|
260
233
|
|
|
261
234
|
if (state.status === 'NEEDS_PLAN_APPROVAL') {
|
|
262
|
-
|
|
263
|
-
|
|
235
|
+
// Boolean --approve: auto-use the stored planDigest (no user digest input needed).
|
|
236
|
+
// Legacy --approve-plan <digest>: explicit digest must match.
|
|
237
|
+
const approveRequested = options.approve === true
|
|
238
|
+
|| (typeof options.planApprovalDigest === 'string' && options.planApprovalDigest.length > 0);
|
|
239
|
+
if (!approveRequested) return publicState(state);
|
|
240
|
+
if (options.approve === true) {
|
|
241
|
+
// Boolean approve: no digest input; the stored planDigest is authoritative.
|
|
242
|
+
} else if (options.planApprovalDigest !== state.planDigest) {
|
|
264
243
|
throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'ship plan approval digest does not match the frozen plan');
|
|
265
244
|
}
|
|
266
245
|
if (!options.actor) {
|
|
@@ -294,7 +273,6 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
294
273
|
adapterRegistry: options.adapterRegistry,
|
|
295
274
|
root,
|
|
296
275
|
productionMode: true,
|
|
297
|
-
productionConfirmation: state.planDigest,
|
|
298
276
|
});
|
|
299
277
|
state = {
|
|
300
278
|
...state,
|
|
@@ -312,7 +290,6 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
312
290
|
approvalPath: state.approvalPath,
|
|
313
291
|
adapterRegistry: options.adapterRegistry,
|
|
314
292
|
root,
|
|
315
|
-
productionConfirmation: state.planDigest,
|
|
316
293
|
});
|
|
317
294
|
state = {
|
|
318
295
|
...state,
|
|
@@ -337,6 +314,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
337
314
|
status: verified.status,
|
|
338
315
|
verifyRunPath: verified.runPath,
|
|
339
316
|
requirements: undefined,
|
|
317
|
+
manualFollowUps: verified.manualFollowUps ?? undefined,
|
|
340
318
|
updatedAt: new Date().toISOString(),
|
|
341
319
|
};
|
|
342
320
|
await writeJsonAtomic(statePath, state);
|
package/src/commands/verify.mjs
CHANGED
|
@@ -755,7 +755,7 @@ export async function verifyRelease(options) {
|
|
|
755
755
|
runDir: runDirOpt,
|
|
756
756
|
clock: clockOpt,
|
|
757
757
|
npmExecutor,
|
|
758
|
-
verificationGatesAuthorized,
|
|
758
|
+
verificationGatesAuthorized: _verificationGatesAuthorized,
|
|
759
759
|
gateEnv,
|
|
760
760
|
previousVerifyRun,
|
|
761
761
|
} = options ?? {};
|
|
@@ -826,16 +826,9 @@ export async function verifyRelease(options) {
|
|
|
826
826
|
.filter((distribution) => distribution.type === 'npm' && distribution.smokeBin)
|
|
827
827
|
.map((distribution) => ({ unitId: unit.id, smokeBin: distribution.smokeBin }))
|
|
828
828
|
));
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
`plan declares ${consumerGates.length} consumer verification gate(s) and ` +
|
|
833
|
-
`${configuredSmokeBins.length} npm CLI smoke process(es). ` +
|
|
834
|
-
'They execute installed project code without an OS or network sandbox. ' +
|
|
835
|
-
'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
|
|
836
|
-
{ gateIds: consumerGates.map((gate) => gate.id), configuredSmokeBins },
|
|
837
|
-
);
|
|
838
|
-
}
|
|
829
|
+
// The command invocation itself authorizes execution of configured
|
|
830
|
+
// verification gates and smoke processes. Old --acknowledge-gate-side-effects
|
|
831
|
+
// is accepted as a no-effect compatibility input.
|
|
839
832
|
|
|
840
833
|
await evidence.append({ phase: 'verify', step: 'plan-load', status: 'passed' });
|
|
841
834
|
|
|
@@ -1112,14 +1105,21 @@ export async function verifyRelease(options) {
|
|
|
1112
1105
|
}
|
|
1113
1106
|
}
|
|
1114
1107
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1108
|
+
// When the plan declares humanConsumersStrategy: 'manualFollowUps',
|
|
1109
|
+
// Kimi/CodeBuddy installations are non-blocking post-release manual tasks.
|
|
1110
|
+
// Skip attestation collection and blocking for these platforms.
|
|
1111
|
+
const isHumanConsumerFollowUp = plan.humanConsumersStrategy === 'manualFollowUps';
|
|
1112
|
+
|
|
1113
|
+
const missingManualAttestations = isHumanConsumerFollowUp
|
|
1114
|
+
? []
|
|
1115
|
+
: await collectMissingManualAttestations({
|
|
1116
|
+
actions,
|
|
1117
|
+
adapterRegistry,
|
|
1118
|
+
plan,
|
|
1119
|
+
root,
|
|
1120
|
+
runDir,
|
|
1121
|
+
clockFn,
|
|
1122
|
+
});
|
|
1123
1123
|
if (missingManualAttestations.length > 0) {
|
|
1124
1124
|
await evidence.append({
|
|
1125
1125
|
phase: 'verify',
|
|
@@ -1134,6 +1134,9 @@ export async function verifyRelease(options) {
|
|
|
1134
1134
|
);
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
|
+
// Collect non-blocking manual follow-up tasks for human consumer platforms
|
|
1138
|
+
const manualFollowUps = [];
|
|
1139
|
+
|
|
1137
1140
|
// Every action is identity-bound and adapters receive read-only or
|
|
1138
1141
|
// per-action isolated consumer paths. Run independent checks concurrently;
|
|
1139
1142
|
// the evidence writer serializes append operations and result arrays are
|
|
@@ -1165,6 +1168,38 @@ export async function verifyRelease(options) {
|
|
|
1165
1168
|
}
|
|
1166
1169
|
|
|
1167
1170
|
if (isMarketplaceAction(action.type)) {
|
|
1171
|
+
// When humanConsumersStrategy is 'manualFollowUps', Kimi/CodeBuddy
|
|
1172
|
+
// installations are non-blocking manual follow-up tasks. Skip adapter
|
|
1173
|
+
// processing and collect them as manualFollowUps.
|
|
1174
|
+
const isHumanConsumerPlatform = action.type === 'kimi-marketplace-install'
|
|
1175
|
+
|| action.type === 'codebuddy-marketplace-install';
|
|
1176
|
+
if (isHumanConsumerFollowUp && isHumanConsumerPlatform) {
|
|
1177
|
+
const platform = action.type === 'kimi-marketplace-install' ? 'kimi' : 'codebuddy';
|
|
1178
|
+
manualFollowUps.push({
|
|
1179
|
+
actionId: action.id,
|
|
1180
|
+
platform,
|
|
1181
|
+
plugin: action.parameters?.plugin,
|
|
1182
|
+
version: action.parameters?.version,
|
|
1183
|
+
unitId: action.unitId,
|
|
1184
|
+
verifiedBySystem: false,
|
|
1185
|
+
reason: 'human consumer platform — installation is a manual post-release task; not verified by system',
|
|
1186
|
+
});
|
|
1187
|
+
adapterChecks.push({
|
|
1188
|
+
actionId: action.id,
|
|
1189
|
+
actionType: action.type,
|
|
1190
|
+
status: 'SKIPPED',
|
|
1191
|
+
reason: 'manual-follow-up',
|
|
1192
|
+
});
|
|
1193
|
+
await evidence.append({
|
|
1194
|
+
phase: 'verify-marketplace',
|
|
1195
|
+
actionId: action.id,
|
|
1196
|
+
actionType: action.type,
|
|
1197
|
+
status: 'SKIPPED',
|
|
1198
|
+
reason: 'manual-follow-up',
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1168
1203
|
// --- Marketplace: fresh consumer verification in verify's own runDir ---
|
|
1169
1204
|
// Context: isolatedConsumerWritesAuthorized allows writing to verify's
|
|
1170
1205
|
// runDir/consumers/ directory; externalWritesAuthorized stays false.
|
|
@@ -1637,6 +1672,7 @@ export async function verifyRelease(options) {
|
|
|
1637
1672
|
gateResults: consumerGateResults,
|
|
1638
1673
|
consumerVerificationReceipts,
|
|
1639
1674
|
skillResourceClosureReceipts,
|
|
1675
|
+
...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
|
|
1640
1676
|
startedAt: clockFn(),
|
|
1641
1677
|
finishedAt: clockFn(),
|
|
1642
1678
|
};
|
|
@@ -1660,6 +1696,7 @@ export async function verifyRelease(options) {
|
|
|
1660
1696
|
adapterChecks,
|
|
1661
1697
|
smokeTest,
|
|
1662
1698
|
gateResults: consumerGateResults,
|
|
1699
|
+
...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
|
|
1663
1700
|
};
|
|
1664
1701
|
} catch (err) {
|
|
1665
1702
|
await evidence.append({
|
|
@@ -275,10 +275,16 @@ export function evaluateConsumerSkillResourceClosureReceipts(plan, receipts) {
|
|
|
275
275
|
'kimi-plugin',
|
|
276
276
|
'codebuddy-plugin',
|
|
277
277
|
]);
|
|
278
|
+
// When plan declares humanConsumersStrategy: 'manualFollowUps', Kimi/CodeBuddy
|
|
279
|
+
// are non-blocking manual follow-up tasks and are not verified by the system.
|
|
280
|
+
// Exclude them from the expected receipt set.
|
|
281
|
+
const humanConsumerTypes = plan.humanConsumersStrategy === 'manualFollowUps'
|
|
282
|
+
? new Set(['kimi-plugin', 'codebuddy-plugin'])
|
|
283
|
+
: new Set();
|
|
278
284
|
const expectedKeys = [];
|
|
279
285
|
for (const unit of plan.units ?? []) {
|
|
280
286
|
for (const distribution of unit.distributions ?? []) {
|
|
281
|
-
if (supported.has(distribution.type)) {
|
|
287
|
+
if (supported.has(distribution.type) && !humanConsumerTypes.has(distribution.type)) {
|
|
282
288
|
expectedKeys.push(`${unit.id}:${distribution.type}`);
|
|
283
289
|
}
|
|
284
290
|
}
|