release-skill 0.1.9 → 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 +53 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +18 -33
- package/README.zh-CN.md +17 -23
- 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 +2740 -1844
- 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 +2740 -1844
- 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 +2740 -1844
- 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 +2740 -1844
- 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 -730
- 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 +16 -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/snapshot/frozen.mjs +29 -5
package/src/core/plan.mjs
CHANGED
|
@@ -22,6 +22,13 @@ import addFormats from 'ajv-formats';
|
|
|
22
22
|
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
23
23
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
24
24
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
25
|
+
// NOTE (T2.2 step 3): plan.mjs and the platform registry form a module cycle
|
|
26
|
+
// (plan -> registry -> platforms/kimi -> plan, because the kimi strategies
|
|
27
|
+
// bind computePlanDigest). PLATFORMS is therefore only ever read inside
|
|
28
|
+
// function bodies (after the import graph has settled), never at plan.mjs
|
|
29
|
+
// module-init time — a top-level read would hit the registry's
|
|
30
|
+
// not-yet-initialized bindings when the cycle is entered registry-first.
|
|
31
|
+
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
25
32
|
|
|
26
33
|
// ---------------------------------------------------------------------------
|
|
27
34
|
// Schema loaded from the authoritative JSON file (single source of truth)
|
|
@@ -159,6 +166,37 @@ export function validatePlan(plan) {
|
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
/**
|
|
170
|
+
* planVersion 2 digest scope (design: t1-2-digest-decoupling.md §4.1).
|
|
171
|
+
*
|
|
172
|
+
* The record layer -- top-level `digest`, lifecycle `status`, `createdAt`,
|
|
173
|
+
* the whole `baseline` object, and each action's runtime `status` -- is
|
|
174
|
+
* still written to the plan file (schema unchanged, evidence chain intact)
|
|
175
|
+
* but is excluded from the digest. Everything else (planVersion, units with
|
|
176
|
+
* every frozenSnapshot artifact identity field, externalActions identity,
|
|
177
|
+
* production, configDigest, verificationGates, snapshotDigest) binds.
|
|
178
|
+
*
|
|
179
|
+
* This is the ONLY place the v2 record layer is defined.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} plan - A plan object.
|
|
182
|
+
* @returns {object} The binding-layer projection of the plan.
|
|
183
|
+
*/
|
|
184
|
+
function stripRecordLayerV2(plan) {
|
|
185
|
+
const {
|
|
186
|
+
digest: _digest,
|
|
187
|
+
status: _status,
|
|
188
|
+
createdAt: _createdAt,
|
|
189
|
+
baseline: _baseline,
|
|
190
|
+
...rest
|
|
191
|
+
} = plan;
|
|
192
|
+
return {
|
|
193
|
+
...rest,
|
|
194
|
+
externalActions: (plan.externalActions ?? []).map(
|
|
195
|
+
({ status: _actionStatus, ...action }) => action,
|
|
196
|
+
),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
162
200
|
/**
|
|
163
201
|
* Compute a deterministic SHA-256 digest of a plan object.
|
|
164
202
|
*
|
|
@@ -167,11 +205,22 @@ export function validatePlan(plan) {
|
|
|
167
205
|
* self-consistent: `computePlanDigest(plan) === plan.digest` when the plan
|
|
168
206
|
* was written by `writePlanAtomic`.
|
|
169
207
|
*
|
|
208
|
+
* Digest scope forks on `plan.planVersion` (centralized here, never spread
|
|
209
|
+
* elsewhere):
|
|
210
|
+
* - planVersion 2: record-layer fields (status/createdAt/baseline/action
|
|
211
|
+
* status/digest) are stripped before hashing, so re-preparing over the
|
|
212
|
+
* same sources yields the same digest. See `stripRecordLayerV2`.
|
|
213
|
+
* - planVersion 1 or absent (legacy): only the top-level `digest` field is
|
|
214
|
+
* stripped. Byte-for-byte legacy semantics -- this path must never change.
|
|
215
|
+
*
|
|
170
216
|
* @param {object} plan - A plan object (must not include a `digest` field,
|
|
171
217
|
* or the field will be stripped before hashing).
|
|
172
218
|
* @returns {string} Lowercase 64-char hex SHA-256 digest.
|
|
173
219
|
*/
|
|
174
220
|
export function computePlanDigest(plan) {
|
|
221
|
+
if (plan.planVersion === 2) {
|
|
222
|
+
return sha256Hex(canonicalJson(stripRecordLayerV2(plan)));
|
|
223
|
+
}
|
|
175
224
|
// Strip the digest field if present so the hash is self-consistent.
|
|
176
225
|
const { digest: _digest, ...rest } = plan;
|
|
177
226
|
return sha256Hex(canonicalJson(rest));
|
|
@@ -260,6 +309,28 @@ export async function writePlanImmutable(planPath, plan) {
|
|
|
260
309
|
if (error.code !== 'EEXIST') throw error;
|
|
261
310
|
const existing = await readFile(planPath, 'utf8');
|
|
262
311
|
if (existing !== json) {
|
|
312
|
+
// planVersion 2 (design: t1-2-digest-decoupling.md §4.1): record-layer
|
|
313
|
+
// fields (status/createdAt/baseline) are excluded from the digest, so
|
|
314
|
+
// re-preparing over identical binding content yields the SAME digest
|
|
315
|
+
// with legitimately different bytes. Reuse the existing authority IFF
|
|
316
|
+
// it is genuinely self-consistent (its embedded digest and recomputed
|
|
317
|
+
// digest both equal this digest) -- the authority is never replaced.
|
|
318
|
+
// Any other divergence (tampering, malformed file) still fails closed.
|
|
319
|
+
if (plan.planVersion === 2) {
|
|
320
|
+
let existingPlan = null;
|
|
321
|
+
try {
|
|
322
|
+
existingPlan = JSON.parse(existing);
|
|
323
|
+
} catch {
|
|
324
|
+
existingPlan = null;
|
|
325
|
+
}
|
|
326
|
+
if (
|
|
327
|
+
existingPlan && typeof existingPlan === 'object' &&
|
|
328
|
+
existingPlan.digest === planDigest &&
|
|
329
|
+
computePlanDigest(existingPlan) === planDigest
|
|
330
|
+
) {
|
|
331
|
+
return { planPath, planDigest };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
263
334
|
throw new ReleaseError(
|
|
264
335
|
GATE_FAILED,
|
|
265
336
|
'immutable plan authority already exists with different bytes',
|
|
@@ -277,17 +348,33 @@ export async function writePlanImmutable(planPath, plan) {
|
|
|
277
348
|
// Plan action completeness gate
|
|
278
349
|
// ---------------------------------------------------------------------------
|
|
279
350
|
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Canonical normalized freeze timestamp shape (`YYYY-MM-DDTHH:MM:SS+00:00`).
|
|
353
|
+
* Mirrors the frozenSnapshot.commitTimestamp pattern in the release-plan
|
|
354
|
+
* schema; used by the planVersion 2 completeness gate, which requires a
|
|
355
|
+
* canonical normalized timestamp without re-binding it to plan.createdAt.
|
|
356
|
+
*/
|
|
357
|
+
const CANONICAL_COMMIT_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$/;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Build the expected-adapter map for each action type. The marketplace
|
|
361
|
+
* install entries are derived from the platform registry (the single source
|
|
362
|
+
* of platform knowledge: actionType -> adapter); the remaining entries are
|
|
363
|
+
* fixed non-platform action types.
|
|
364
|
+
*
|
|
365
|
+
* Built lazily on demand (see the PLATFORMS import note above): this must
|
|
366
|
+
* never run at plan.mjs module-init time.
|
|
367
|
+
*/
|
|
368
|
+
function buildExpectedAdapterMap() {
|
|
369
|
+
return {
|
|
370
|
+
'push-snapshot': 'git-github',
|
|
371
|
+
'create-tag': 'git-github',
|
|
372
|
+
'github-release': 'github',
|
|
373
|
+
'npm-publish': 'npm',
|
|
374
|
+
...Object.fromEntries(PLATFORMS.map((p) => [p.actionType, p.adapter])),
|
|
375
|
+
'set-default-branch': 'git-github',
|
|
376
|
+
};
|
|
377
|
+
}
|
|
291
378
|
|
|
292
379
|
/** Required action types for every unit. */
|
|
293
380
|
const REQUIRED_ACTION_TYPES = ['push-snapshot', 'create-tag', 'github-release'];
|
|
@@ -300,9 +387,9 @@ const REQUIRED_ACTION_TYPES = ['push-snapshot', 'create-tag', 'github-release'];
|
|
|
300
387
|
* - 1 create-tag (adapter: git-github)
|
|
301
388
|
* - 1 github-release (adapter: github)
|
|
302
389
|
* - 1 npm-publish (adapter: npm) — only if the unit has an npm distribution
|
|
303
|
-
* - 1
|
|
304
|
-
*
|
|
305
|
-
*
|
|
390
|
+
* - 1 <platform>-marketplace-install (adapter from the platform registry) —
|
|
391
|
+
* one per registry platform whose distribution the unit declares
|
|
392
|
+
* (currently claude / codex / kimi; driven by src/platforms/registry.mjs)
|
|
306
393
|
*
|
|
307
394
|
* Every action must bind to the correct unitId, correct adapter, correct
|
|
308
395
|
* version, correct publicRepo (where applicable), and correct tag derived
|
|
@@ -329,6 +416,7 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
329
416
|
|
|
330
417
|
const units = Array.isArray(plan.units) ? plan.units : [];
|
|
331
418
|
const actions = Array.isArray(plan.externalActions) ? plan.externalActions : [];
|
|
419
|
+
const expectedAdapterMap = buildExpectedAdapterMap();
|
|
332
420
|
|
|
333
421
|
// --- Must have at least one action ---
|
|
334
422
|
if (actions.length === 0) {
|
|
@@ -394,6 +482,15 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
394
482
|
}
|
|
395
483
|
if (!frozen.commitTimestamp || typeof frozen.commitTimestamp !== 'string') {
|
|
396
484
|
failures.push(`unit "${unitId}" frozenSnapshot.commitTimestamp is missing; legacy production plans without a freeze timestamp are rejected, never silently backfilled`);
|
|
485
|
+
} else if (plan.planVersion === 2) {
|
|
486
|
+
// planVersion 2 (design: t1-2-digest-decoupling.md §4.2): the freeze
|
|
487
|
+
// timestamp is derived from the headCommit committer date, so it no
|
|
488
|
+
// longer equals plan.createdAt (record-layer real clock). It must
|
|
489
|
+
// still be a canonical normalized timestamp -- the same pattern the
|
|
490
|
+
// release-plan schema enforces.
|
|
491
|
+
if (!CANONICAL_COMMIT_TIMESTAMP_RE.test(frozen.commitTimestamp)) {
|
|
492
|
+
failures.push(`unit "${unitId}" frozenSnapshot.commitTimestamp is not a canonical normalized timestamp (expected YYYY-MM-DDTHH:MM:SS+00:00)`);
|
|
493
|
+
}
|
|
397
494
|
} else if (plan.createdAt !== frozen.commitTimestamp) {
|
|
398
495
|
failures.push(`unit "${unitId}" frozenSnapshot.commitTimestamp must equal plan.createdAt`);
|
|
399
496
|
}
|
|
@@ -425,7 +522,7 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
425
522
|
// Required actions for this unit (always required)
|
|
426
523
|
for (const actionType of REQUIRED_ACTION_TYPES) {
|
|
427
524
|
expectedCount++;
|
|
428
|
-
const expectedAdapter =
|
|
525
|
+
const expectedAdapter = expectedAdapterMap[actionType];
|
|
429
526
|
const expectedActionId = `${actionType}-${unitId}`;
|
|
430
527
|
const matchingActions = actions.filter(
|
|
431
528
|
(a) => a.unitId === unitId && a.type === actionType,
|
|
@@ -617,242 +714,46 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
617
714
|
}
|
|
618
715
|
}
|
|
619
716
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
)
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
);
|
|
643
|
-
} else {
|
|
644
|
-
const action = claudeActions[0];
|
|
645
|
-
|
|
646
|
-
if (action.id !== expectedActionId) {
|
|
647
|
-
failures.push(
|
|
648
|
-
`unit "${unitId}", action "${action.id}": id is "${action.id}", expected "${expectedActionId}"`,
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
if (action.unitId !== unitId) {
|
|
652
|
-
failures.push(
|
|
653
|
-
`unit "${unitId}", action "${action.id}": unitId is "${action.unitId}", expected "${unitId}"`,
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
if (action.adapter !== 'plugin-marketplace') {
|
|
657
|
-
failures.push(
|
|
658
|
-
`unit "${unitId}", action "${action.id}": adapter is "${action.adapter}", expected "plugin-marketplace"`,
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
if (action.status !== 'PENDING') {
|
|
662
|
-
failures.push(
|
|
663
|
-
`unit "${unitId}", action "${action.id}": status is "${action.status ?? '(missing)'}", expected "PENDING"`,
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Parameter checks
|
|
668
|
-
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer, 'claude', unitId, failures);
|
|
669
|
-
_checkRequired(action, 'parameters.plugin', action.parameters?.plugin, plugin, unitId, failures);
|
|
670
|
-
_checkRequired(action, 'parameters.marketplace', action.parameters?.marketplace, marketplace, unitId, failures);
|
|
671
|
-
_checkRequired(action, 'parameters.repo', action.parameters?.repo, publicRepo, unitId, failures);
|
|
672
|
-
_checkRequired(action, 'parameters.version', action.parameters?.version, targetVersion, unitId, failures);
|
|
673
|
-
_checkRequired(action, 'parameters.entrySkill', action.parameters?.entrySkill, entrySkill, unitId, failures);
|
|
674
|
-
if (production) {
|
|
675
|
-
_checkRequired(action, 'parameters.snapshotPath', action.parameters?.snapshotPath, frozen?.path, unitId, failures);
|
|
676
|
-
_checkRequired(action, 'parameters.ref', action.parameters?.ref, expectedTag, unitId, failures);
|
|
677
|
-
_checkRequired(action, 'parameters.manifestDigest', action.parameters?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
678
|
-
}
|
|
679
|
-
// timeoutMs is mandatory for all marketplace install actions.
|
|
680
|
-
// Legacy plans (pre-v0.1.5) lack this field; legacyCompatibility
|
|
681
|
-
// relaxes the check for reconcile/verify paths only, and only when
|
|
682
|
-
// the property is genuinely absent (undefined). An explicit null is
|
|
683
|
-
// NOT "absent" -- it is an invalid value and always fails closed,
|
|
684
|
-
// like strings or out-of-range numbers, even in legacyCompatibility.
|
|
685
|
-
{
|
|
686
|
-
const raw = action.parameters?.timeoutMs;
|
|
687
|
-
if (raw === undefined) {
|
|
688
|
-
if (!options.legacyCompatibility) {
|
|
689
|
-
failures.push(
|
|
690
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs is missing, expected a valid timeout (30000-900000)`,
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
} else {
|
|
694
|
-
// Field is present -- always validate range/type, even in legacy mode
|
|
695
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
696
|
-
failures.push(
|
|
697
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be a finite integer, got: ${JSON.stringify(raw)}`,
|
|
698
|
-
);
|
|
699
|
-
} else if (raw < 30000 || raw > 900000) {
|
|
700
|
-
failures.push(
|
|
701
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be between 30000 and 900000, got: ${raw}`,
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Expected checks
|
|
708
|
-
_checkRequired(action, 'expected.installed', action.expected?.installed, true, unitId, failures);
|
|
709
|
-
_checkRequired(action, 'expected.plugin', action.expected?.plugin, plugin, unitId, failures);
|
|
710
|
-
_checkRequired(action, 'expected.marketplace', action.expected?.marketplace, marketplace, unitId, failures);
|
|
711
|
-
_checkRequired(action, 'expected.version', action.expected?.version, targetVersion, unitId, failures);
|
|
712
|
-
_checkRequired(action, 'expected.entrySkill', action.expected?.entrySkill, entrySkill, unitId, failures);
|
|
713
|
-
if (production) {
|
|
714
|
-
_checkRequired(action, 'expected.consumer', action.expected?.consumer, 'claude', unitId, failures);
|
|
715
|
-
_checkRequired(action, 'expected.repo', action.expected?.repo, publicRepo, unitId, failures);
|
|
716
|
-
_checkRequired(action, 'expected.ref', action.expected?.ref, expectedTag, unitId, failures);
|
|
717
|
-
_checkRequired(action, 'expected.entrySkillFound', action.expected?.entrySkillFound, true, unitId, failures);
|
|
718
|
-
_checkRequired(action, 'expected.manifestDigest', action.expected?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// codex-marketplace-install: only required if unit has codex-plugin distribution
|
|
724
|
-
const codexDist = distributions.find((d) => d.type === 'codex-plugin');
|
|
725
|
-
if (codexDist) {
|
|
726
|
-
const plugin = codexDist.plugin;
|
|
727
|
-
const marketplace = codexDist.marketplace;
|
|
728
|
-
const entrySkill = codexDist.entrySkill;
|
|
729
|
-
if (!plugin || !marketplace || !entrySkill) {
|
|
730
|
-
failures.push(`unit "${unitId}": codex-plugin distribution requires plugin, marketplace, and entrySkill`);
|
|
731
|
-
}
|
|
732
|
-
expectedCount++;
|
|
733
|
-
const expectedActionId = `codex-marketplace-install-${unitId}`;
|
|
734
|
-
const codexActions = actions.filter(
|
|
735
|
-
(a) => a.unitId === unitId && a.type === 'codex-marketplace-install',
|
|
736
|
-
);
|
|
737
|
-
|
|
738
|
-
if (codexActions.length === 0) {
|
|
739
|
-
failures.push(
|
|
740
|
-
`unit "${unitId}": codex-plugin distribution declared but "codex-marketplace-install" action is missing`,
|
|
741
|
-
);
|
|
742
|
-
} else if (codexActions.length > 1) {
|
|
743
|
-
failures.push(
|
|
744
|
-
`unit "${unitId}": duplicate codex-marketplace-install actions (${codexActions.length} found, expected 1)`,
|
|
745
|
-
);
|
|
746
|
-
} else {
|
|
747
|
-
const action = codexActions[0];
|
|
748
|
-
|
|
749
|
-
if (action.id !== expectedActionId) {
|
|
750
|
-
failures.push(
|
|
751
|
-
`unit "${unitId}", action "${action.id}": id is "${action.id}", expected "${expectedActionId}"`,
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
if (action.unitId !== unitId) {
|
|
755
|
-
failures.push(
|
|
756
|
-
`unit "${unitId}", action "${action.id}": unitId is "${action.unitId}", expected "${unitId}"`,
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
if (action.adapter !== 'plugin-marketplace') {
|
|
760
|
-
failures.push(
|
|
761
|
-
`unit "${unitId}", action "${action.id}": adapter is "${action.adapter}", expected "plugin-marketplace"`,
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
if (action.status !== 'PENDING') {
|
|
765
|
-
failures.push(
|
|
766
|
-
`unit "${unitId}", action "${action.id}": status is "${action.status ?? '(missing)'}", expected "PENDING"`,
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Parameter checks
|
|
771
|
-
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer, 'codex', unitId, failures);
|
|
772
|
-
_checkRequired(action, 'parameters.plugin', action.parameters?.plugin, plugin, unitId, failures);
|
|
773
|
-
_checkRequired(action, 'parameters.marketplace', action.parameters?.marketplace, marketplace, unitId, failures);
|
|
774
|
-
_checkRequired(action, 'parameters.repo', action.parameters?.repo, publicRepo, unitId, failures);
|
|
775
|
-
_checkRequired(action, 'parameters.version', action.parameters?.version, targetVersion, unitId, failures);
|
|
776
|
-
_checkRequired(action, 'parameters.entrySkill', action.parameters?.entrySkill, entrySkill, unitId, failures);
|
|
777
|
-
if (production) {
|
|
778
|
-
_checkRequired(action, 'parameters.snapshotPath', action.parameters?.snapshotPath, frozen?.path, unitId, failures);
|
|
779
|
-
_checkRequired(action, 'parameters.ref', action.parameters?.ref, expectedTag, unitId, failures);
|
|
780
|
-
_checkRequired(action, 'parameters.manifestDigest', action.parameters?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
781
|
-
}
|
|
782
|
-
// timeoutMs is mandatory for all marketplace install actions.
|
|
783
|
-
// Legacy plans (pre-v0.1.5) lack this field; legacyCompatibility
|
|
784
|
-
// relaxes the check for reconcile/verify paths only, and only when
|
|
785
|
-
// the property is genuinely absent (undefined). An explicit null is
|
|
786
|
-
// NOT "absent" -- it is an invalid value and always fails closed,
|
|
787
|
-
// like strings or out-of-range numbers, even in legacyCompatibility.
|
|
788
|
-
{
|
|
789
|
-
const raw = action.parameters?.timeoutMs;
|
|
790
|
-
if (raw === undefined) {
|
|
791
|
-
if (!options.legacyCompatibility) {
|
|
792
|
-
failures.push(
|
|
793
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs is missing, expected a valid timeout (30000-900000)`,
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
} else {
|
|
797
|
-
// Field is present -- always validate range/type, even in legacy mode
|
|
798
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
799
|
-
failures.push(
|
|
800
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be a finite integer, got: ${JSON.stringify(raw)}`,
|
|
801
|
-
);
|
|
802
|
-
} else if (raw < 30000 || raw > 900000) {
|
|
803
|
-
failures.push(
|
|
804
|
-
`unit "${unitId}", action "${action.id}": parameters.timeoutMs must be between 30000 and 900000, got: ${raw}`,
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Expected checks
|
|
811
|
-
_checkRequired(action, 'expected.installed', action.expected?.installed, true, unitId, failures);
|
|
812
|
-
_checkRequired(action, 'expected.plugin', action.expected?.plugin, plugin, unitId, failures);
|
|
813
|
-
_checkRequired(action, 'expected.marketplace', action.expected?.marketplace, marketplace, unitId, failures);
|
|
814
|
-
_checkRequired(action, 'expected.version', action.expected?.version, targetVersion, unitId, failures);
|
|
815
|
-
_checkRequired(action, 'expected.entrySkill', action.expected?.entrySkill, entrySkill, unitId, failures);
|
|
816
|
-
if (production) {
|
|
817
|
-
_checkRequired(action, 'expected.consumer', action.expected?.consumer, 'codex', unitId, failures);
|
|
818
|
-
_checkRequired(action, 'expected.repo', action.expected?.repo, publicRepo, unitId, failures);
|
|
819
|
-
_checkRequired(action, 'expected.ref', action.expected?.ref, expectedTag, unitId, failures);
|
|
820
|
-
_checkRequired(action, 'expected.entrySkillFound', action.expected?.entrySkillFound, true, unitId, failures);
|
|
821
|
-
_checkRequired(action, 'expected.manifestDigest', action.expected?.manifestDigest, frozen?.manifestDigest, unitId, failures);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// kimi-marketplace-install: only required if unit has kimi-plugin distribution
|
|
827
|
-
const kimiDist = distributions.find((d) => d.type === 'kimi-plugin');
|
|
828
|
-
if (kimiDist) {
|
|
829
|
-
const plugin = kimiDist.plugin;
|
|
830
|
-
// marketplace is NOT a required identity field for kimi: Kimi Code has an
|
|
831
|
-
// interactive marketplace but no non-interactive install API, so the kimi
|
|
832
|
-
// action carries no executable marketplace identity (MINOR-1). Only plugin
|
|
833
|
-
// and entrySkill are required; a declared marketplace is tolerated as an
|
|
834
|
-
// optional legacy value but never bound as a required condition.
|
|
835
|
-
const marketplace = kimiDist.marketplace;
|
|
836
|
-
const entrySkill = kimiDist.entrySkill;
|
|
837
|
-
if (!plugin || !entrySkill) {
|
|
838
|
-
failures.push(`unit "${unitId}": kimi-plugin distribution requires plugin and entrySkill`);
|
|
717
|
+
// Consumer marketplace install actions: one required per declared
|
|
718
|
+
// platform distribution, driven by the platform registry (T2.2 step 3).
|
|
719
|
+
// The loop body is shared by every platform; the registry declares the
|
|
720
|
+
// per-platform differences (actionType, distributionType, adapter,
|
|
721
|
+
// consumer id, and — via the schema required fields — whether marketplace
|
|
722
|
+
// identity is a required binding). Platforms without a required
|
|
723
|
+
// marketplace (kimi, MINOR-1: Kimi Code has an interactive marketplace
|
|
724
|
+
// but no non-interactive install API) tolerate a declared marketplace as
|
|
725
|
+
// an optional legacy value but never bind it as a required condition, in
|
|
726
|
+
// parameters or in expected.
|
|
727
|
+
for (const platform of PLATFORMS) {
|
|
728
|
+
const dist = distributions.find((d) => d.type === platform.distributionType);
|
|
729
|
+
if (!dist) continue;
|
|
730
|
+
const plugin = dist.plugin;
|
|
731
|
+
const marketplace = dist.marketplace;
|
|
732
|
+
const entrySkill = dist.entrySkill;
|
|
733
|
+
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
734
|
+
if (requiresMarketplace) {
|
|
735
|
+
if (!plugin || !marketplace || !entrySkill) {
|
|
736
|
+
failures.push(`unit "${unitId}": ${platform.distributionType} distribution requires plugin, marketplace, and entrySkill`);
|
|
737
|
+
}
|
|
738
|
+
} else if (!plugin || !entrySkill) {
|
|
739
|
+
failures.push(`unit "${unitId}": ${platform.distributionType} distribution requires plugin and entrySkill`);
|
|
839
740
|
}
|
|
840
741
|
expectedCount++;
|
|
841
|
-
const expectedActionId =
|
|
842
|
-
const
|
|
843
|
-
(a) => a.unitId === unitId && a.type ===
|
|
742
|
+
const expectedActionId = `${platform.actionType}-${unitId}`;
|
|
743
|
+
const matchingActions = actions.filter(
|
|
744
|
+
(a) => a.unitId === unitId && a.type === platform.actionType,
|
|
844
745
|
);
|
|
845
746
|
|
|
846
|
-
if (
|
|
747
|
+
if (matchingActions.length === 0) {
|
|
847
748
|
failures.push(
|
|
848
|
-
`unit "${unitId}":
|
|
749
|
+
`unit "${unitId}": ${platform.distributionType} distribution declared but "${platform.actionType}" action is missing`,
|
|
849
750
|
);
|
|
850
|
-
} else if (
|
|
751
|
+
} else if (matchingActions.length > 1) {
|
|
851
752
|
failures.push(
|
|
852
|
-
`unit "${unitId}": duplicate
|
|
753
|
+
`unit "${unitId}": duplicate ${platform.actionType} actions (${matchingActions.length} found, expected 1)`,
|
|
853
754
|
);
|
|
854
755
|
} else {
|
|
855
|
-
const action =
|
|
756
|
+
const action = matchingActions[0];
|
|
856
757
|
|
|
857
758
|
if (action.id !== expectedActionId) {
|
|
858
759
|
failures.push(
|
|
@@ -864,9 +765,9 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
864
765
|
`unit "${unitId}", action "${action.id}": unitId is "${action.unitId}", expected "${unitId}"`,
|
|
865
766
|
);
|
|
866
767
|
}
|
|
867
|
-
if (action.adapter !==
|
|
768
|
+
if (action.adapter !== platform.adapter) {
|
|
868
769
|
failures.push(
|
|
869
|
-
`unit "${unitId}", action "${action.id}": adapter is "${action.adapter}", expected "
|
|
770
|
+
`unit "${unitId}", action "${action.id}": adapter is "${action.adapter}", expected "${platform.adapter}"`,
|
|
870
771
|
);
|
|
871
772
|
}
|
|
872
773
|
if (action.status !== 'PENDING') {
|
|
@@ -876,13 +777,17 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
876
777
|
}
|
|
877
778
|
|
|
878
779
|
// Parameter checks
|
|
879
|
-
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer,
|
|
780
|
+
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer, platform.id, unitId, failures);
|
|
880
781
|
_checkRequired(action, 'parameters.plugin', action.parameters?.plugin, plugin, unitId, failures);
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
&& action.parameters.marketplace !== marketplace
|
|
782
|
+
if (requiresMarketplace) {
|
|
783
|
+
_checkRequired(action, 'parameters.marketplace', action.parameters?.marketplace, marketplace, unitId, failures);
|
|
784
|
+
} else if (
|
|
785
|
+
action.parameters?.marketplace !== undefined && marketplace !== undefined
|
|
786
|
+
&& action.parameters.marketplace !== marketplace
|
|
787
|
+
) {
|
|
788
|
+
// marketplace is optional here (MINOR-1): a legacy plan may still
|
|
789
|
+
// carry it, but it must be consistent with the declared
|
|
790
|
+
// distribution and is never a required identity condition.
|
|
886
791
|
failures.push(
|
|
887
792
|
`unit "${unitId}", action "${action.id}": parameters.marketplace is "${action.parameters.marketplace}", expected optional legacy value "${marketplace}"`,
|
|
888
793
|
);
|
|
@@ -923,14 +828,18 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
923
828
|
}
|
|
924
829
|
}
|
|
925
830
|
|
|
926
|
-
// Expected checks.
|
|
927
|
-
//
|
|
831
|
+
// Expected checks. Marketplace identity is bound only on platforms
|
|
832
|
+
// whose schema requires it (MINOR-1): a non-marketplace platform's
|
|
833
|
+
// observation never binds a marketplace.
|
|
928
834
|
_checkRequired(action, 'expected.installed', action.expected?.installed, true, unitId, failures);
|
|
929
835
|
_checkRequired(action, 'expected.plugin', action.expected?.plugin, plugin, unitId, failures);
|
|
836
|
+
if (requiresMarketplace) {
|
|
837
|
+
_checkRequired(action, 'expected.marketplace', action.expected?.marketplace, marketplace, unitId, failures);
|
|
838
|
+
}
|
|
930
839
|
_checkRequired(action, 'expected.version', action.expected?.version, targetVersion, unitId, failures);
|
|
931
840
|
_checkRequired(action, 'expected.entrySkill', action.expected?.entrySkill, entrySkill, unitId, failures);
|
|
932
841
|
if (production) {
|
|
933
|
-
_checkRequired(action, 'expected.consumer', action.expected?.consumer,
|
|
842
|
+
_checkRequired(action, 'expected.consumer', action.expected?.consumer, platform.id, unitId, failures);
|
|
934
843
|
_checkRequired(action, 'expected.repo', action.expected?.repo, publicRepo, unitId, failures);
|
|
935
844
|
_checkRequired(action, 'expected.ref', action.expected?.ref, expectedTag, unitId, failures);
|
|
936
845
|
_checkRequired(action, 'expected.entrySkillFound', action.expected?.entrySkillFound, true, unitId, failures);
|