release-skill 0.1.10 → 0.2.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/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +17 -32
- package/README.zh-CN.md +17 -25
- 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 +2721 -1843
- package/adapters/claude/schemas/.render-manifest.json +8 -8
- package/adapters/claude/schemas/approval-record.schema.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +6 -2
- package/adapters/claude/schemas/release-project.schema.json +14 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/codex/schemas/.render-manifest.json +8 -8
- package/adapters/codex/schemas/approval-record.schema.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +6 -2
- package/adapters/codex/schemas/release-project.schema.json +14 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/kimi/schemas/.render-manifest.json +8 -8
- package/adapters/kimi/schemas/approval-record.schema.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +6 -2
- package/adapters/kimi/schemas/release-project.schema.json +14 -0
- package/bin/release-skill-cli.mjs +3 -0
- package/bin/release-skill.bundle.mjs +2721 -1843
- package/package.json +8 -2
- package/references/.render-manifest.json +8 -8
- package/references/01-state-machine.md +5 -5
- package/references/02-project-config.md +1 -1
- package/references/05-evidence-and-errors.md +1 -1
- package/references/06-adapter-contract.md +41 -1
- package/schemas/.render-manifest.json +8 -8
- package/schemas/approval-record.schema.json +1 -1
- package/schemas/release-plan.schema.json +6 -2
- package/schemas/release-project.schema.json +14 -0
- package/scripts/sync-public-files.mjs +462 -0
- package/src/adapters/contract.mjs +60 -0
- package/src/adapters/plugin-marketplace.mjs +289 -736
- package/src/commands/prepare.mjs +195 -182
- package/src/commands/publish.mjs +438 -122
- package/src/commands/reconcile.mjs +369 -191
- package/src/commands/verify.mjs +13 -2
- package/src/core/approval.mjs +72 -45
- package/src/core/baseline.mjs +5 -0
- package/src/core/checkpoints.mjs +143 -0
- package/src/core/evidence.mjs +30 -3
- package/src/core/hook-cache.mjs +254 -0
- package/src/core/hooks.mjs +37 -1
- package/src/core/observe-retry.mjs +223 -0
- package/src/core/plan.mjs +162 -253
- package/src/platforms/kimi.mjs +514 -0
- package/src/platforms/registry.mjs +393 -0
- package/src/producers/build-adapters.mjs +14 -22
package/src/commands/prepare.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const execFile = promisify(execFileCb);
|
|
|
28
28
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
29
29
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
30
30
|
import { runHook } from '../core/hooks.mjs';
|
|
31
|
+
import { computeHookCacheKey, readHookCache, writeHookCache } from '../core/hook-cache.mjs';
|
|
31
32
|
import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
32
33
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
33
34
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
@@ -48,6 +49,7 @@ import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
|
48
49
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
49
50
|
import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
|
|
50
51
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
52
|
+
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
51
53
|
|
|
52
54
|
// ---------------------------------------------------------------------------
|
|
53
55
|
// Version resolution
|
|
@@ -200,17 +202,31 @@ export async function resolveAllUnitVersions(units, root, explicitVersion, evide
|
|
|
200
202
|
/**
|
|
201
203
|
* Run all declared project hooks in order: docs, build, test, typecheck.
|
|
202
204
|
*
|
|
205
|
+
* Incremental cache (T3.2): a hook that opts in with `cacheable: true` and a
|
|
206
|
+
* non-empty `cacheInputs` is fingerprinted by its configuration plus the
|
|
207
|
+
* content of every matched input file. On an unchanged fingerprint whose last
|
|
208
|
+
* run succeeded, execution is skipped and the cached outcome replayed. The
|
|
209
|
+
* cache only ever skips execution — it runs after the hook authorization gate
|
|
210
|
+
* and never bypasses any GATE; hook order and failure semantics are unchanged.
|
|
211
|
+
* Failures (non-zero exit or HOOK_TIMEOUT) are never cached. A `cacheInputs`
|
|
212
|
+
* glob that matches nothing fails closed before the hook runs.
|
|
213
|
+
*
|
|
203
214
|
* @param {object} config - The loaded project config.
|
|
204
215
|
* @param {string} root - Absolute project root.
|
|
205
216
|
* @param {object} evidence - The evidence writer.
|
|
206
217
|
* @param {Function} [hookFn] - Hook runner (default runHook); tests inject a
|
|
207
218
|
* spy that records call order while delegating to the real implementation.
|
|
219
|
+
* @param {object} [options]
|
|
220
|
+
* @param {boolean} [options.hookCache=true] - When false (--no-hook-cache),
|
|
221
|
+
* every hook runs in full and the cache is neither read nor written.
|
|
208
222
|
* @returns {Promise<void>}
|
|
209
|
-
* @throws {ReleaseError} GATE_FAILED if any hook returns a non-zero exit code
|
|
223
|
+
* @throws {ReleaseError} GATE_FAILED if any hook returns a non-zero exit code,
|
|
224
|
+
* throws, or declares a cacheInputs glob that matches no file.
|
|
210
225
|
*/
|
|
211
|
-
async function runDeclaredHooks(config, root, evidence, hookFn = runHook) {
|
|
226
|
+
export async function runDeclaredHooks(config, root, evidence, hookFn = runHook, options = {}) {
|
|
212
227
|
const hookOrder = ['docs', 'build', 'test', 'typecheck'];
|
|
213
228
|
const hooks = config.hooks ?? {};
|
|
229
|
+
const cacheEnabled = options.hookCache !== false;
|
|
214
230
|
|
|
215
231
|
for (const name of hookOrder) {
|
|
216
232
|
const hook = hooks[name];
|
|
@@ -222,6 +238,36 @@ async function runDeclaredHooks(config, root, evidence, hookFn = runHook) {
|
|
|
222
238
|
hookName: name,
|
|
223
239
|
});
|
|
224
240
|
|
|
241
|
+
// --- Incremental cache lookup (opt-in only; default zero change) ---
|
|
242
|
+
let cacheKey;
|
|
243
|
+
if (cacheEnabled && hook.cacheable === true) {
|
|
244
|
+
try {
|
|
245
|
+
({ cacheKey } = await computeHookCacheKey(hook, root));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
await evidence.append({
|
|
248
|
+
phase: 'hooks',
|
|
249
|
+
status: 'failed',
|
|
250
|
+
hookName: name,
|
|
251
|
+
error: { code: err.code, message: err.message },
|
|
252
|
+
});
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const cached = await readHookCache(root, name, cacheKey);
|
|
257
|
+
if (cached) {
|
|
258
|
+
// Cache hit: skip execution. The authorization gate already passed and
|
|
259
|
+
// no GATE is bypassed — ordering and failure semantics are untouched.
|
|
260
|
+
await evidence.append({
|
|
261
|
+
phase: 'hooks',
|
|
262
|
+
status: 'completed',
|
|
263
|
+
hookName: name,
|
|
264
|
+
cached: true,
|
|
265
|
+
cacheKey,
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
225
271
|
let result;
|
|
226
272
|
try {
|
|
227
273
|
result = await hookFn(hook, { root });
|
|
@@ -258,6 +304,26 @@ async function runDeclaredHooks(config, root, evidence, hookFn = runHook) {
|
|
|
258
304
|
);
|
|
259
305
|
}
|
|
260
306
|
|
|
307
|
+
// --- Write cache on success only; failures are never cached ---
|
|
308
|
+
if (cacheEnabled && hook.cacheable === true && cacheKey) {
|
|
309
|
+
const written = await writeHookCache(root, name, cacheKey, {
|
|
310
|
+
exitCode: 0,
|
|
311
|
+
stdoutTail: result.stdout.slice(-4000),
|
|
312
|
+
stderrTail: result.stderr.slice(-4000),
|
|
313
|
+
});
|
|
314
|
+
if (!written.ok) {
|
|
315
|
+
// The cache is an optimisation, not a gate: a write failure must not
|
|
316
|
+
// abort prepare. Surface it as a warning-level evidence event.
|
|
317
|
+
await evidence.append({
|
|
318
|
+
phase: 'hooks',
|
|
319
|
+
status: 'warning',
|
|
320
|
+
hookName: name,
|
|
321
|
+
warning: 'hook cache write failed; continuing without caching',
|
|
322
|
+
error: written.error,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
261
327
|
await evidence.append({
|
|
262
328
|
phase: 'hooks',
|
|
263
329
|
status: 'completed',
|
|
@@ -731,6 +797,50 @@ function normalizedProductionConfig(unit) {
|
|
|
731
797
|
};
|
|
732
798
|
}
|
|
733
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Derive the deterministic freeze timestamp for planVersion 2 plans (design:
|
|
802
|
+
* t1-2-digest-decoupling.md §4.2): the baseline headCommit's committer date,
|
|
803
|
+
* read via `git show -s --format=%cI` and normalized to canonical UTC second
|
|
804
|
+
* precision with `normalizeGitTimestamp`.
|
|
805
|
+
*
|
|
806
|
+
* Same source commit -> same freeze timestamp -> byte-identical frozen
|
|
807
|
+
* release commit on every re-prepare. Failures fail closed with GATE_FAILED
|
|
808
|
+
* (prepare requires a readable Git repository); there is deliberately no
|
|
809
|
+
* fallback clock.
|
|
810
|
+
*
|
|
811
|
+
* @param {string} root - Repository root (git cwd).
|
|
812
|
+
* @param {string} headCommit - The baseline head commit object id.
|
|
813
|
+
* @param {Function} [exec] - Injectable exec (tests); defaults to execFile.
|
|
814
|
+
* @returns {Promise<string>} Canonical `YYYY-MM-DDTHH:MM:SS+00:00` timestamp.
|
|
815
|
+
* @throws {ReleaseError} GATE_FAILED when the committer date cannot be read
|
|
816
|
+
* or normalized.
|
|
817
|
+
*/
|
|
818
|
+
export async function readHeadCommitTimestamp(root, headCommit, exec = execFile) {
|
|
819
|
+
try {
|
|
820
|
+
const { stdout } = await exec(
|
|
821
|
+
'git',
|
|
822
|
+
['show', '-s', '--format=%cI', headCommit],
|
|
823
|
+
{ cwd: root, shell: false },
|
|
824
|
+
);
|
|
825
|
+
const committerDate = stdout.trim();
|
|
826
|
+
if (!committerDate) {
|
|
827
|
+
throw new ReleaseError(
|
|
828
|
+
GATE_FAILED,
|
|
829
|
+
'plan freeze timestamp derivation returned an empty headCommit committer date',
|
|
830
|
+
{ headCommit },
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
return normalizeGitTimestamp(committerDate, 'plan freeze timestamp');
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error instanceof ReleaseError) throw error;
|
|
836
|
+
throw new ReleaseError(
|
|
837
|
+
GATE_FAILED,
|
|
838
|
+
'plan freeze timestamp could not be derived from the headCommit committer date (git show -s --format=%cI failed); no fallback clock is used',
|
|
839
|
+
{ headCommit, cause: error?.message ?? String(error) },
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
734
844
|
async function buildProductionAssets(
|
|
735
845
|
unitResults,
|
|
736
846
|
resolvedVersions,
|
|
@@ -849,12 +959,6 @@ async function buildProductionAssets(
|
|
|
849
959
|
function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
850
960
|
const actions = [];
|
|
851
961
|
|
|
852
|
-
const marketplaceIdentity = (distribution) => ({
|
|
853
|
-
plugin: distribution.plugin,
|
|
854
|
-
marketplace: distribution.marketplace,
|
|
855
|
-
entrySkill: distribution.entrySkill,
|
|
856
|
-
});
|
|
857
|
-
|
|
858
962
|
if (!productionAssets) {
|
|
859
963
|
for (let index = 0; index < unitResults.length; index += 1) {
|
|
860
964
|
const { unit } = unitResults[index];
|
|
@@ -909,88 +1013,44 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
909
1013
|
parameters: { publicRepo: unit.publicRepo, version },
|
|
910
1014
|
status: 'PENDING',
|
|
911
1015
|
});
|
|
912
|
-
// Consumer marketplace install actions (only when distribution
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
marketplace: identity.marketplace,
|
|
926
|
-
repo: unit.publicRepo,
|
|
927
|
-
version,
|
|
928
|
-
entrySkill: identity.entrySkill,
|
|
929
|
-
timeoutMs: claudeTimeoutMs,
|
|
930
|
-
},
|
|
931
|
-
expected: {
|
|
932
|
-
installed: true,
|
|
933
|
-
plugin: identity.plugin,
|
|
934
|
-
marketplace: identity.marketplace,
|
|
935
|
-
version,
|
|
936
|
-
entrySkill: identity.entrySkill,
|
|
937
|
-
},
|
|
938
|
-
status: 'PENDING',
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
|
|
942
|
-
if (codexDist) {
|
|
943
|
-
const identity = marketplaceIdentity(codexDist);
|
|
944
|
-
const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
|
|
1016
|
+
// Consumer marketplace install actions (only when distribution
|
|
1017
|
+
// declared), driven by the platform registry (T2.2 step 3): one loop
|
|
1018
|
+
// body for every platform; the registry declares the per-platform
|
|
1019
|
+
// differences (actionType, distributionType, adapter, and — via the
|
|
1020
|
+
// schema required fields — marketplace identity, which kimi does not
|
|
1021
|
+
// carry: Kimi Code has no non-interactive install/marketplace API, so
|
|
1022
|
+
// the kimi action carries no marketplace identity (MINOR-1); plugin +
|
|
1023
|
+
// entrySkill are the meaningful identity fields there).
|
|
1024
|
+
for (const platform of PLATFORMS) {
|
|
1025
|
+
const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1026
|
+
if (!dist) continue;
|
|
1027
|
+
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
1028
|
+
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
945
1029
|
actions.push({
|
|
946
|
-
id:
|
|
947
|
-
type:
|
|
948
|
-
adapter:
|
|
1030
|
+
id: `${platform.actionType}-${unit.id}`,
|
|
1031
|
+
type: platform.actionType,
|
|
1032
|
+
adapter: platform.adapter,
|
|
949
1033
|
unitId: unit.id,
|
|
950
1034
|
parameters: {
|
|
951
|
-
consumer:
|
|
952
|
-
plugin:
|
|
953
|
-
marketplace:
|
|
1035
|
+
consumer: platform.id,
|
|
1036
|
+
plugin: dist.plugin,
|
|
1037
|
+
...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
|
|
954
1038
|
repo: unit.publicRepo,
|
|
955
1039
|
version,
|
|
956
|
-
entrySkill:
|
|
957
|
-
timeoutMs
|
|
1040
|
+
entrySkill: dist.entrySkill,
|
|
1041
|
+
timeoutMs,
|
|
1042
|
+
// Payload verification contract for new plans (T1.3): installed
|
|
1043
|
+
// payload is verified by declared-manifest containment; host-added
|
|
1044
|
+
// files are recorded, not failed. Frozen plans without this marker
|
|
1045
|
+
// keep the legacy full-tree equality semantics byte-for-byte.
|
|
1046
|
+
payloadContract: 'declared-manifest-v1',
|
|
958
1047
|
},
|
|
959
1048
|
expected: {
|
|
960
1049
|
installed: true,
|
|
961
|
-
plugin:
|
|
962
|
-
marketplace:
|
|
1050
|
+
plugin: dist.plugin,
|
|
1051
|
+
...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
|
|
963
1052
|
version,
|
|
964
|
-
entrySkill:
|
|
965
|
-
},
|
|
966
|
-
status: 'PENDING',
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
const kimiDist = (unit.distributions ?? []).find((d) => d.type === 'kimi-plugin');
|
|
970
|
-
if (kimiDist) {
|
|
971
|
-
const identity = marketplaceIdentity(kimiDist);
|
|
972
|
-
const kimiTimeoutMs = Number.isInteger(kimiDist.timeoutMs) ? kimiDist.timeoutMs : 300000;
|
|
973
|
-
// Kimi Code has no non-interactive install/marketplace API, so the kimi
|
|
974
|
-
// action carries no marketplace identity (MINOR-1). plugin + entrySkill
|
|
975
|
-
// are the meaningful identity fields.
|
|
976
|
-
actions.push({
|
|
977
|
-
id: `kimi-marketplace-install-${unit.id}`,
|
|
978
|
-
type: 'kimi-marketplace-install',
|
|
979
|
-
adapter: 'plugin-marketplace',
|
|
980
|
-
unitId: unit.id,
|
|
981
|
-
parameters: {
|
|
982
|
-
consumer: 'kimi',
|
|
983
|
-
plugin: identity.plugin,
|
|
984
|
-
repo: unit.publicRepo,
|
|
985
|
-
version,
|
|
986
|
-
entrySkill: identity.entrySkill,
|
|
987
|
-
timeoutMs: kimiTimeoutMs,
|
|
988
|
-
},
|
|
989
|
-
expected: {
|
|
990
|
-
installed: true,
|
|
991
|
-
plugin: identity.plugin,
|
|
992
|
-
version,
|
|
993
|
-
entrySkill: identity.entrySkill,
|
|
1053
|
+
entrySkill: dist.entrySkill,
|
|
994
1054
|
},
|
|
995
1055
|
status: 'PENDING',
|
|
996
1056
|
});
|
|
@@ -1136,109 +1196,48 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
1136
1196
|
status: 'PENDING',
|
|
1137
1197
|
});
|
|
1138
1198
|
|
|
1139
|
-
// Consumer marketplace install actions (only when distribution
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1199
|
+
// Consumer marketplace install actions (only when distribution
|
|
1200
|
+
// declared), driven by the platform registry (T2.2 step 3): mirrors the
|
|
1201
|
+
// non-production loop above plus the production-only bindings
|
|
1202
|
+
// (ref/snapshotPath/manifestDigest parameters; consumer/repo/ref/
|
|
1203
|
+
// entrySkillFound/manifestDigest expected). Marketplace identity follows
|
|
1204
|
+
// the registry's schema required fields — kimi carries none (MINOR-1).
|
|
1205
|
+
for (const platform of PLATFORMS) {
|
|
1206
|
+
const dist = (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1207
|
+
if (!dist) continue;
|
|
1208
|
+
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
1209
|
+
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
1144
1210
|
actions.push({
|
|
1145
|
-
id:
|
|
1146
|
-
type:
|
|
1147
|
-
adapter:
|
|
1211
|
+
id: `${platform.actionType}-${unit.id}`,
|
|
1212
|
+
type: platform.actionType,
|
|
1213
|
+
adapter: platform.adapter,
|
|
1148
1214
|
unitId: unit.id,
|
|
1149
1215
|
parameters: {
|
|
1150
|
-
consumer:
|
|
1151
|
-
plugin:
|
|
1152
|
-
marketplace:
|
|
1216
|
+
consumer: platform.id,
|
|
1217
|
+
plugin: dist.plugin,
|
|
1218
|
+
...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
|
|
1153
1219
|
repo: unit.publicRepo,
|
|
1154
1220
|
ref: resolvedTag,
|
|
1155
1221
|
version: unitVersion,
|
|
1156
|
-
entrySkill:
|
|
1222
|
+
entrySkill: dist.entrySkill,
|
|
1157
1223
|
snapshotPath: asset.snapshotPath,
|
|
1158
1224
|
manifestDigest: asset.manifestDigest,
|
|
1159
|
-
timeoutMs
|
|
1225
|
+
timeoutMs,
|
|
1226
|
+
// Payload verification contract for new plans (T1.3): installed
|
|
1227
|
+
// payload is verified by declared-manifest containment; host-added
|
|
1228
|
+
// files are recorded, not failed. Frozen plans without this marker
|
|
1229
|
+
// keep the legacy full-tree equality semantics byte-for-byte.
|
|
1230
|
+
payloadContract: 'declared-manifest-v1',
|
|
1160
1231
|
},
|
|
1161
1232
|
expected: {
|
|
1162
1233
|
installed: true,
|
|
1163
|
-
consumer:
|
|
1164
|
-
plugin:
|
|
1165
|
-
marketplace:
|
|
1234
|
+
consumer: platform.id,
|
|
1235
|
+
plugin: dist.plugin,
|
|
1236
|
+
...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
|
|
1166
1237
|
repo: unit.publicRepo,
|
|
1167
1238
|
version: unitVersion,
|
|
1168
1239
|
ref: resolvedTag,
|
|
1169
|
-
entrySkill:
|
|
1170
|
-
entrySkillFound: true,
|
|
1171
|
-
manifestDigest: asset.manifestDigest,
|
|
1172
|
-
},
|
|
1173
|
-
status: 'PENDING',
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
|
|
1177
|
-
if (codexDist) {
|
|
1178
|
-
const identity = marketplaceIdentity(codexDist);
|
|
1179
|
-
const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
|
|
1180
|
-
actions.push({
|
|
1181
|
-
id: `codex-marketplace-install-${unit.id}`,
|
|
1182
|
-
type: 'codex-marketplace-install',
|
|
1183
|
-
adapter: 'plugin-marketplace',
|
|
1184
|
-
unitId: unit.id,
|
|
1185
|
-
parameters: {
|
|
1186
|
-
consumer: 'codex',
|
|
1187
|
-
plugin: identity.plugin,
|
|
1188
|
-
marketplace: identity.marketplace,
|
|
1189
|
-
repo: unit.publicRepo,
|
|
1190
|
-
ref: resolvedTag,
|
|
1191
|
-
version: unitVersion,
|
|
1192
|
-
entrySkill: identity.entrySkill,
|
|
1193
|
-
snapshotPath: asset.snapshotPath,
|
|
1194
|
-
manifestDigest: asset.manifestDigest,
|
|
1195
|
-
timeoutMs: codexTimeoutMs,
|
|
1196
|
-
},
|
|
1197
|
-
expected: {
|
|
1198
|
-
installed: true,
|
|
1199
|
-
consumer: 'codex',
|
|
1200
|
-
plugin: identity.plugin,
|
|
1201
|
-
marketplace: identity.marketplace,
|
|
1202
|
-
repo: unit.publicRepo,
|
|
1203
|
-
version: unitVersion,
|
|
1204
|
-
ref: resolvedTag,
|
|
1205
|
-
entrySkill: identity.entrySkill,
|
|
1206
|
-
entrySkillFound: true,
|
|
1207
|
-
manifestDigest: asset.manifestDigest,
|
|
1208
|
-
},
|
|
1209
|
-
status: 'PENDING',
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
const kimiDist = (unit.distributions ?? []).find((d) => d.type === 'kimi-plugin');
|
|
1213
|
-
if (kimiDist) {
|
|
1214
|
-
const identity = marketplaceIdentity(kimiDist);
|
|
1215
|
-
const kimiTimeoutMs = Number.isInteger(kimiDist.timeoutMs) ? kimiDist.timeoutMs : 300000;
|
|
1216
|
-
// No marketplace identity for kimi (MINOR-1): Kimi Code has an interactive
|
|
1217
|
-
// marketplace but no non-interactive install API.
|
|
1218
|
-
actions.push({
|
|
1219
|
-
id: `kimi-marketplace-install-${unit.id}`,
|
|
1220
|
-
type: 'kimi-marketplace-install',
|
|
1221
|
-
adapter: 'plugin-marketplace',
|
|
1222
|
-
unitId: unit.id,
|
|
1223
|
-
parameters: {
|
|
1224
|
-
consumer: 'kimi',
|
|
1225
|
-
plugin: identity.plugin,
|
|
1226
|
-
repo: unit.publicRepo,
|
|
1227
|
-
ref: resolvedTag,
|
|
1228
|
-
version: unitVersion,
|
|
1229
|
-
entrySkill: identity.entrySkill,
|
|
1230
|
-
snapshotPath: asset.snapshotPath,
|
|
1231
|
-
manifestDigest: asset.manifestDigest,
|
|
1232
|
-
timeoutMs: kimiTimeoutMs,
|
|
1233
|
-
},
|
|
1234
|
-
expected: {
|
|
1235
|
-
installed: true,
|
|
1236
|
-
consumer: 'kimi',
|
|
1237
|
-
plugin: identity.plugin,
|
|
1238
|
-
repo: unit.publicRepo,
|
|
1239
|
-
version: unitVersion,
|
|
1240
|
-
ref: resolvedTag,
|
|
1241
|
-
entrySkill: identity.entrySkill,
|
|
1240
|
+
entrySkill: dist.entrySkill,
|
|
1242
1241
|
entrySkillFound: true,
|
|
1243
1242
|
manifestDigest: asset.manifestDigest,
|
|
1244
1243
|
},
|
|
@@ -1280,6 +1279,9 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
1280
1279
|
* @param {Function} [options.runHookFn] - Hook runner passed to
|
|
1281
1280
|
* runDeclaredHooks (default runHook); tests inject a spy that records call
|
|
1282
1281
|
* order while delegating to the real implementation.
|
|
1282
|
+
* @param {boolean} [options.hookCache=true] - When false (CLI --no-hook-cache),
|
|
1283
|
+
* every declared hook runs in full and the incremental hook cache is neither
|
|
1284
|
+
* read nor written.
|
|
1283
1285
|
*
|
|
1284
1286
|
* @returns {Promise<{ planPath: string, planDigest: string, evidenceDir: string }>}
|
|
1285
1287
|
*
|
|
@@ -1486,7 +1488,9 @@ export async function prepareRelease(options) {
|
|
|
1486
1488
|
|
|
1487
1489
|
// --- Step 3: Run declared hooks ---
|
|
1488
1490
|
await evidence.append({ phase: 'hooks', status: 'started' });
|
|
1489
|
-
await runDeclaredHooks(config, realRoot, evidence, options.runHookFn ?? runHook
|
|
1491
|
+
await runDeclaredHooks(config, realRoot, evidence, options.runHookFn ?? runHook, {
|
|
1492
|
+
hookCache: options.hookCache,
|
|
1493
|
+
});
|
|
1490
1494
|
await evidence.append({ phase: 'hooks', status: 'completed' });
|
|
1491
1495
|
|
|
1492
1496
|
// --- Step 3b: Re-check release-document freshness AFTER hooks ---
|
|
@@ -1822,16 +1826,25 @@ export async function prepareRelease(options) {
|
|
|
1822
1826
|
// --- Step 7: Build plan object ---
|
|
1823
1827
|
await evidence.append({ phase: 'plan-assembly', status: 'started' });
|
|
1824
1828
|
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
// before any Git write
|
|
1829
|
+
// New prepares emit planVersion 2 (design: t1-2-digest-decoupling.md
|
|
1830
|
+
// §4.2/§7). Production freeze timestamps are derived deterministically
|
|
1831
|
+
// from the baseline headCommit's committer date, before the first frozen
|
|
1832
|
+
// Git object exists. This single canonical value becomes
|
|
1833
|
+
// GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for every unit's frozen commit and
|
|
1834
|
+
// every unit's frozenSnapshot.commitTimestamp; identical sources freeze
|
|
1835
|
+
// byte-identical Git objects on every re-prepare. The wall-clock sample
|
|
1836
|
+
// is still validated here (fail closed before any Git write) and becomes
|
|
1837
|
+
// plan.createdAt -- record-layer real clock behind the 24h approval
|
|
1838
|
+
// window, no longer equal to the freeze timestamp for v2 plans. The v1
|
|
1839
|
+
// legacy path used this same sample as the freeze timestamp itself
|
|
1840
|
+
// (commitTimestamp == createdAt). publish, retry, and reconcile consume
|
|
1841
|
+
// the frozen value from the plan and never re-read the wall clock or
|
|
1842
|
+
// re-derive it.
|
|
1843
|
+
const createdAtTimestamp = production
|
|
1844
|
+
? normalizeGitTimestamp(clock ? clock() : new Date().toISOString(), 'plan createdAt timestamp')
|
|
1845
|
+
: null;
|
|
1833
1846
|
const freezeTimestamp = production
|
|
1834
|
-
?
|
|
1847
|
+
? await readHeadCommitTimestamp(realRoot, baseline.gitHead)
|
|
1835
1848
|
: null;
|
|
1836
1849
|
|
|
1837
1850
|
const productionAssets = production
|
|
@@ -1884,7 +1897,7 @@ export async function prepareRelease(options) {
|
|
|
1884
1897
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
1885
1898
|
|
|
1886
1899
|
const plan = {
|
|
1887
|
-
planVersion:
|
|
1900
|
+
planVersion: 2,
|
|
1888
1901
|
status: 'PREPARED',
|
|
1889
1902
|
baseline: {
|
|
1890
1903
|
gitTreeHash: baseline.gitTreeHash,
|
|
@@ -1905,7 +1918,7 @@ export async function prepareRelease(options) {
|
|
|
1905
1918
|
} : {}),
|
|
1906
1919
|
units,
|
|
1907
1920
|
externalActions,
|
|
1908
|
-
createdAt: production ?
|
|
1921
|
+
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
1909
1922
|
};
|
|
1910
1923
|
|
|
1911
1924
|
await evidence.append({
|