release-skill 0.9.1 → 0.9.4
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 +78 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +56 -26
- package/README.zh-CN.md +47 -23
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/release-project.schema.json +4 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-config/SKILL.md +5 -3
- package/adapters/claude/skills/release-docs/SKILL.md +4 -2
- package/adapters/claude/skills/release-finish/SKILL.md +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +18 -3
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +12 -2
- package/adapters/claude/skills/release-publish/SKILL.md +17 -10
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/claude/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/claude/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/release-project.schema.json +4 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-config/SKILL.md +5 -3
- package/adapters/codex/skills/release-docs/SKILL.md +4 -2
- package/adapters/codex/skills/release-finish/SKILL.md +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +18 -3
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +12 -2
- package/adapters/codex/skills/release-publish/SKILL.md +17 -10
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/codex/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/release-project.schema.json +4 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-config/SKILL.md +5 -3
- package/adapters/kimi/skills/release-docs/SKILL.md +4 -2
- package/adapters/kimi/skills/release-finish/SKILL.md +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +18 -3
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +12 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +17 -10
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/kimi/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4564 -1997
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/release-project.schema.json +4 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-config/SKILL.md +5 -3
- package/adapters/workbuddy/skills/release-docs/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-finish/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +18 -3
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +12 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +17 -10
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +58 -2
- package/bin/release-skill.bundle.mjs +4564 -1997
- package/bin/rules.json +1 -1
- package/package.json +5 -5
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/01-state-machine.md +34 -1
- package/references/02-project-config.md +20 -0
- package/references/05-evidence-and-errors.md +2 -2
- package/schemas/release-project.schema.json +4 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-config/SKILL.md +5 -3
- package/skills/release-docs/SKILL.md +4 -2
- package/skills/release-finish/SKILL.md +2 -0
- package/skills/release-help/SKILL.md +18 -3
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +12 -2
- package/skills/release-publish/SKILL.md +17 -10
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-config/SKILL.md +5 -3
- package/skills-src/release-docs/SKILL.md +4 -2
- package/skills-src/release-finish/SKILL.md +2 -0
- package/skills-src/release-help/SKILL.md +18 -3
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +12 -2
- package/skills-src/release-publish/SKILL.md +17 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/post-release-local.mjs +169 -16
- package/src/commands/prepare.mjs +321 -11
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +183 -2
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- package/src/core/foundation-plugin-verification.mjs +233 -1
- package/src/core/hook-cache.mjs +318 -26
- package/src/core/hooks.mjs +8 -1
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- package/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
package/src/commands/setup.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
deriveGateSuggestions,
|
|
47
47
|
deriveHookDurations,
|
|
48
48
|
deriveLongHookSuggestions,
|
|
49
|
+
derivePreHookPublicSurfaceFinding,
|
|
49
50
|
scanGateDeclarationFindings,
|
|
50
51
|
FINDING_CATEGORY,
|
|
51
52
|
ASSESSMENT_STATUS,
|
|
@@ -3209,6 +3210,7 @@ export async function assessAdoption({ root } = {}) {
|
|
|
3209
3210
|
currentProducerVersion: resolveProducerVersion(),
|
|
3210
3211
|
});
|
|
3211
3212
|
findings.push(...deriveLongHookSuggestions(hookDurations));
|
|
3213
|
+
findings.push(derivePreHookPublicSurfaceFinding({ config, hookDurations }));
|
|
3212
3214
|
findings.push(...deriveCheckOnlySuggestions(config.hooks));
|
|
3213
3215
|
|
|
3214
3216
|
// --- Gate suggestions (scenarios 5 & 6) ---
|
package/src/commands/ship.mjs
CHANGED
|
@@ -15,11 +15,92 @@ import {
|
|
|
15
15
|
} from './post-release-local.mjs';
|
|
16
16
|
import {
|
|
17
17
|
ReleaseError,
|
|
18
|
+
CONFIG_INVALID,
|
|
18
19
|
GATE_FAILED,
|
|
19
20
|
PLAN_DIGEST_MISMATCH,
|
|
20
21
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
21
22
|
} from '../core/errors.mjs';
|
|
22
23
|
|
|
24
|
+
function normalizeNewShipUnitIds(releaseUnits, requestedUnitIds) {
|
|
25
|
+
if (requestedUnitIds === undefined) return undefined;
|
|
26
|
+
const availableUnitIds = (releaseUnits ?? []).map((unit) => unit.id);
|
|
27
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
28
|
+
throw new ReleaseError(
|
|
29
|
+
CONFIG_INVALID,
|
|
30
|
+
'explicit ship release-unit selection must contain at least one unit id',
|
|
31
|
+
{ requestedUnitIds, availableUnitIds },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const requestedSet = new Set(requestedUnitIds);
|
|
35
|
+
const invalid = requestedUnitIds.filter((unitId) => typeof unitId !== 'string' || unitId.length === 0);
|
|
36
|
+
const unknown = requestedUnitIds.filter((unitId) => !availableUnitIds.includes(unitId));
|
|
37
|
+
if (invalid.length > 0 || requestedSet.size !== requestedUnitIds.length || unknown.length > 0) {
|
|
38
|
+
throw new ReleaseError(
|
|
39
|
+
CONFIG_INVALID,
|
|
40
|
+
'ship release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
41
|
+
{ requestedUnitIds, availableUnitIds },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return availableUnitIds.filter((unitId) => requestedSet.has(unitId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameUnitSelection(left, right) {
|
|
48
|
+
return Array.isArray(left)
|
|
49
|
+
&& Array.isArray(right)
|
|
50
|
+
&& left.length === right.length
|
|
51
|
+
&& new Set(left).size === left.length
|
|
52
|
+
&& new Set(right).size === right.length
|
|
53
|
+
&& left.every((unitId) => right.includes(unitId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sameUnitOrder(left, right) {
|
|
57
|
+
return Array.isArray(left)
|
|
58
|
+
&& Array.isArray(right)
|
|
59
|
+
&& left.length === right.length
|
|
60
|
+
&& left.every((unitId, index) => unitId === right[index]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unitScopeMismatch(statePath, stateUnitIds, requestedUnitIds, message) {
|
|
64
|
+
return new ReleaseError(
|
|
65
|
+
GATE_FAILED,
|
|
66
|
+
message,
|
|
67
|
+
{
|
|
68
|
+
reason: 'SHIP_STATE_UNIT_SCOPE_MISMATCH',
|
|
69
|
+
statePath,
|
|
70
|
+
stateUnitIds: stateUnitIds ?? null,
|
|
71
|
+
requestedUnitIds: requestedUnitIds ?? null,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertFrozenPlanUnitScope({ statePath, stateUnitIds, expectedUnitIds, plan }) {
|
|
77
|
+
const planUnitIds = (plan.units ?? []).map((unit) => unit.id);
|
|
78
|
+
const exactMatch = Array.isArray(expectedUnitIds)
|
|
79
|
+
&& planUnitIds.length === expectedUnitIds.length
|
|
80
|
+
&& planUnitIds.every((unitId, index) => unitId === expectedUnitIds[index]);
|
|
81
|
+
if (!exactMatch) {
|
|
82
|
+
throw unitScopeMismatch(
|
|
83
|
+
statePath,
|
|
84
|
+
stateUnitIds,
|
|
85
|
+
planUnitIds,
|
|
86
|
+
'ship state release-unit scope does not match frozen plan.units',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readFrozenPlanForUnitScope(planPath, statePath, stateUnitIds) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await readFile(planPath, 'utf8'));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw unitScopeMismatch(
|
|
96
|
+
statePath,
|
|
97
|
+
stateUnitIds,
|
|
98
|
+
null,
|
|
99
|
+
`cannot verify ship state release-unit scope against frozen plan: ${error.message}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
23
104
|
async function writeJsonAtomic(path, value) {
|
|
24
105
|
await mkdir(dirname(path), { recursive: true });
|
|
25
106
|
const { stateDigest: _oldDigest, ...body } = value;
|
|
@@ -102,6 +183,7 @@ function publicState(state) {
|
|
|
102
183
|
...(state.hooks && state.hooks.length > 0 ? {
|
|
103
184
|
hooks: state.hooks,
|
|
104
185
|
} : {}),
|
|
186
|
+
...(state.selectedUnitIds ? { selectedUnitIds: state.selectedUnitIds } : {}),
|
|
105
187
|
...(state.planPath ? {
|
|
106
188
|
planPath: state.planPath,
|
|
107
189
|
planDigest: state.planDigest,
|
|
@@ -315,6 +397,43 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
315
397
|
? injected
|
|
316
398
|
: await defaultDependencies();
|
|
317
399
|
let state = await readState(statePath);
|
|
400
|
+
const requestedTargetVersion = options.targetVersion ?? null;
|
|
401
|
+
if (state && requestedTargetVersion !== null) {
|
|
402
|
+
const stateTargetVersion = state.targetVersion ?? null;
|
|
403
|
+
if (stateTargetVersion === null || stateTargetVersion !== requestedTargetVersion) {
|
|
404
|
+
const suffix = stateTargetVersion === null
|
|
405
|
+
? 'state targetVersion is missing; provide a new --state path to start the requested version'
|
|
406
|
+
: `state targetVersion ${stateTargetVersion} does not match requested targetVersion ${requestedTargetVersion}`;
|
|
407
|
+
throw new ReleaseError(
|
|
408
|
+
GATE_FAILED,
|
|
409
|
+
`ship state target version mismatch: ${suffix}`,
|
|
410
|
+
{
|
|
411
|
+
reason: 'SHIP_STATE_TARGET_VERSION_MISMATCH',
|
|
412
|
+
statePath,
|
|
413
|
+
stateTargetVersion,
|
|
414
|
+
requestedTargetVersion,
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (state && options.unitIds !== undefined) {
|
|
420
|
+
if (!Array.isArray(state.selectedUnitIds)) {
|
|
421
|
+
throw unitScopeMismatch(
|
|
422
|
+
statePath,
|
|
423
|
+
null,
|
|
424
|
+
options.unitIds,
|
|
425
|
+
'an existing full-scope or legacy ship state cannot acquire --unit; provide a new --state path',
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (!sameUnitSelection(state.selectedUnitIds, options.unitIds)) {
|
|
429
|
+
throw unitScopeMismatch(
|
|
430
|
+
statePath,
|
|
431
|
+
state.selectedUnitIds,
|
|
432
|
+
options.unitIds,
|
|
433
|
+
'ship state release-unit scope does not match the requested --unit values',
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
318
437
|
if (state?.gitTransport) {
|
|
319
438
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
320
439
|
}
|
|
@@ -322,6 +441,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
322
441
|
if (!state) {
|
|
323
442
|
const loaded = await deps.loadProjectConfig({ root });
|
|
324
443
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
444
|
+
const selectedUnitIds = normalizeNewShipUnitIds(loaded.config.releaseUnits, options.unitIds);
|
|
325
445
|
state = {
|
|
326
446
|
schemaVersion: 2,
|
|
327
447
|
root,
|
|
@@ -329,6 +449,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
329
449
|
targetVersion: options.targetVersion ?? null,
|
|
330
450
|
configDigest: loaded.configDigest,
|
|
331
451
|
hooks,
|
|
452
|
+
...(selectedUnitIds ? { selectedUnitIds } : {}),
|
|
332
453
|
status: 'NEW',
|
|
333
454
|
createdAt: new Date().toISOString(),
|
|
334
455
|
updatedAt: new Date().toISOString(),
|
|
@@ -345,8 +466,53 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
345
466
|
await writeJsonAtomic(statePath, state);
|
|
346
467
|
}
|
|
347
468
|
|
|
469
|
+
if (state.planPath && Array.isArray(state.selectedUnitIds)) {
|
|
470
|
+
const frozenPlan = await readFrozenPlanForUnitScope(
|
|
471
|
+
state.planPath,
|
|
472
|
+
statePath,
|
|
473
|
+
state.selectedUnitIds,
|
|
474
|
+
);
|
|
475
|
+
assertFrozenPlanUnitScope({
|
|
476
|
+
statePath,
|
|
477
|
+
stateUnitIds: state.selectedUnitIds,
|
|
478
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
479
|
+
plan: frozenPlan,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
348
483
|
if (state.status === 'NEW') {
|
|
349
484
|
const loaded = await deps.loadProjectConfig({ root });
|
|
485
|
+
let stateChanged = false;
|
|
486
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
487
|
+
const normalizedStateUnitIds = normalizeNewShipUnitIds(
|
|
488
|
+
loaded.config.releaseUnits,
|
|
489
|
+
state.selectedUnitIds,
|
|
490
|
+
);
|
|
491
|
+
if (!sameUnitSelection(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
492
|
+
throw unitScopeMismatch(
|
|
493
|
+
statePath,
|
|
494
|
+
state.selectedUnitIds,
|
|
495
|
+
normalizedStateUnitIds,
|
|
496
|
+
'ship state release-unit scope no longer matches project configuration',
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (!sameUnitOrder(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
500
|
+
if (state.planPath) {
|
|
501
|
+
throw unitScopeMismatch(
|
|
502
|
+
statePath,
|
|
503
|
+
state.selectedUnitIds,
|
|
504
|
+
normalizedStateUnitIds,
|
|
505
|
+
'frozen ship state release-unit order no longer matches project configuration',
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
state = {
|
|
509
|
+
...state,
|
|
510
|
+
selectedUnitIds: normalizedStateUnitIds,
|
|
511
|
+
updatedAt: new Date().toISOString(),
|
|
512
|
+
};
|
|
513
|
+
stateChanged = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
350
516
|
if (loaded.configDigest !== state.configDigest) {
|
|
351
517
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
352
518
|
state = {
|
|
@@ -356,8 +522,9 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
356
522
|
status: 'NEW',
|
|
357
523
|
updatedAt: new Date().toISOString(),
|
|
358
524
|
};
|
|
359
|
-
|
|
525
|
+
stateChanged = true;
|
|
360
526
|
}
|
|
527
|
+
if (stateChanged) await writeJsonAtomic(statePath, state);
|
|
361
528
|
const prepared = await deps.prepareRelease({
|
|
362
529
|
root,
|
|
363
530
|
version: state.targetVersion ?? undefined,
|
|
@@ -366,10 +533,24 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
366
533
|
hooksAuthorized: true,
|
|
367
534
|
verificationGatesAuthorized: true,
|
|
368
535
|
hookCache: true,
|
|
536
|
+
...(state.selectedUnitIds ? { unitIds: state.selectedUnitIds } : {}),
|
|
369
537
|
});
|
|
538
|
+
let frozenPlan = null;
|
|
539
|
+
if (Array.isArray(state.selectedUnitIds) || deps.preflightGitTransports) {
|
|
540
|
+
frozenPlan = Array.isArray(state.selectedUnitIds)
|
|
541
|
+
? await readFrozenPlanForUnitScope(prepared.planPath, statePath, state.selectedUnitIds)
|
|
542
|
+
: JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
545
|
+
assertFrozenPlanUnitScope({
|
|
546
|
+
statePath,
|
|
547
|
+
stateUnitIds: state.selectedUnitIds,
|
|
548
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
549
|
+
plan: frozenPlan,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
370
552
|
let transportPreflight = null;
|
|
371
553
|
if (deps.preflightGitTransports) {
|
|
372
|
-
const frozenPlan = JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
373
554
|
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
374
555
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
375
556
|
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -1045,6 +1045,27 @@ export async function verifyRelease(options) {
|
|
|
1045
1045
|
requireDigest: Boolean(plan.production),
|
|
1046
1046
|
...(plan.production ? { authorityPlanPath: planPath } : {}),
|
|
1047
1047
|
});
|
|
1048
|
+
|
|
1049
|
+
// `distribute` runs are deliberately not valid verify sources. Report the
|
|
1050
|
+
// actionable lineage correction before the generic lineage validator's
|
|
1051
|
+
// unsupported-command error, while keeping the existing source authority
|
|
1052
|
+
// and automatic distribute discovery semantics unchanged.
|
|
1053
|
+
if (sourceRun.command === 'distribute') {
|
|
1054
|
+
throw new ReleaseError(
|
|
1055
|
+
GATE_FAILED,
|
|
1056
|
+
'verify --run must point to a PUBLISHED publish or reconcile run from the same plan; distribute runs are automatically discovered by verify and do not become the verify authority',
|
|
1057
|
+
{
|
|
1058
|
+
sourceRunCommand: sourceRun.command,
|
|
1059
|
+
sourceRunId: sourceRun.runId,
|
|
1060
|
+
sourceRunStatus: sourceRun.status,
|
|
1061
|
+
expectedSourceCommands: ['publish', 'reconcile'],
|
|
1062
|
+
requiredSourceStatus: 'PUBLISHED',
|
|
1063
|
+
distributeRunsAutoDiscovered: true,
|
|
1064
|
+
verifyAuthority: 'publish-or-reconcile-source-run',
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1048
1069
|
await validateRunLineage(sourceRun, {
|
|
1049
1070
|
plan,
|
|
1050
1071
|
planPath,
|
|
@@ -506,6 +506,65 @@ export function deriveLongHookSuggestions(hookDurations) {
|
|
|
506
506
|
return findings;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Report the opt-in R93-01 pre-hook public-surface check without changing the
|
|
511
|
+
* base adoption conclusion. A candidate is based only on hookDurations that
|
|
512
|
+
* already passed the current-producer, command/cwd-matching and real
|
|
513
|
+
* started/completed-pair checks above; timeout, cache hits and labels are not
|
|
514
|
+
* consulted here.
|
|
515
|
+
*
|
|
516
|
+
* @param {object} options
|
|
517
|
+
* @param {object} options.config - Loaded project configuration.
|
|
518
|
+
* @param {Array<object>} options.hookDurations - Trusted duration facts.
|
|
519
|
+
* @returns {object} One adoption finding for the policy field.
|
|
520
|
+
*/
|
|
521
|
+
export function derivePreHookPublicSurfaceFinding({ config, hookDurations }) {
|
|
522
|
+
const fieldPath = 'policy.preHookPublicSurfaceCheck';
|
|
523
|
+
if (config?.policy?.preHookPublicSurfaceCheck === true) {
|
|
524
|
+
return createFinding({
|
|
525
|
+
category: FINDING_CATEGORY.SATISFIED,
|
|
526
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_ADOPTED',
|
|
527
|
+
fieldPath,
|
|
528
|
+
message: '已启用 Hook 前公开表面检查;prepare 会在首个 Hook 前验证候选公开表面,并在 Hook 后重新执行正式检查。',
|
|
529
|
+
evidence: { enabled: true },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const candidate = (hookDurations ?? [])
|
|
534
|
+
.filter((duration) => (
|
|
535
|
+
duration?.basis === 'evidence'
|
|
536
|
+
&& Number.isFinite(duration.durationMs)
|
|
537
|
+
&& duration.durationMs >= LONG_HOOK_THRESHOLD_MS
|
|
538
|
+
&& duration.realRuns >= 1
|
|
539
|
+
))
|
|
540
|
+
.sort((a, b) => (
|
|
541
|
+
(b.durationMs - a.durationMs) || a.hookName.localeCompare(b.hookName)
|
|
542
|
+
))[0];
|
|
543
|
+
if (candidate) {
|
|
544
|
+
return createFinding({
|
|
545
|
+
category: FINDING_CATEGORY.OPTIONAL_SUGGESTION,
|
|
546
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_CANDIDATE',
|
|
547
|
+
fieldPath,
|
|
548
|
+
message: `当前 hook "${candidate.hookName}" 有可信的实际运行证据(至少 30 分钟);可人工评估是否启用 Hook 前公开表面检查。`,
|
|
549
|
+
evidence: {
|
|
550
|
+
hookName: candidate.hookName,
|
|
551
|
+
durationMs: candidate.durationMs,
|
|
552
|
+
realRuns: candidate.realRuns,
|
|
553
|
+
lastObservedAt: candidate.lastObservedAt,
|
|
554
|
+
},
|
|
555
|
+
action: '确认公开文件不会由 Hook 生成或修复后,再在配置中将 policy.preHookPublicSurfaceCheck 设为 true;评估不会自动修改配置。',
|
|
556
|
+
severity: 'suggestion',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return createFinding({
|
|
561
|
+
category: FINDING_CATEGORY.NOT_APPLICABLE,
|
|
562
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_NOT_APPLICABLE',
|
|
563
|
+
fieldPath,
|
|
564
|
+
message: '当前没有可信且匹配当前 Hook 的至少 30 分钟实际运行证据,暂不建议启用 Hook 前公开表面检查。',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
509
568
|
const STATUS_LABEL = Object.freeze({
|
|
510
569
|
NOT_CONFIGURED: '尚未配置',
|
|
511
570
|
PARTIALLY_ADOPTED: '部分接入(存在必选缺口)',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Release-domain thin adapter for Foundation 0.
|
|
2
|
+
* Release-domain thin adapter for Foundation 0.15 complete-plugin observation.
|
|
3
3
|
*
|
|
4
4
|
* The frozen snapshot remains the only payload authority. This module derives
|
|
5
5
|
* transient Foundation inputs from that tree, calls the released Kit API, and
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from 'skill-family-engineering-kit';
|
|
20
20
|
import {
|
|
21
21
|
createFilesystemRootBinding,
|
|
22
|
+
digestBytes,
|
|
23
|
+
observeFilesystemTree,
|
|
22
24
|
readFileBound,
|
|
23
25
|
} from 'skill-family-harness-node';
|
|
24
26
|
|
|
@@ -30,6 +32,196 @@ import { ReleaseError, POST_PUBLISH_VERIFY_FAILED } from './errors.mjs';
|
|
|
30
32
|
// contract at 1.0.0. The real API integration test intentionally exercises
|
|
31
33
|
// this value so an upstream contract change fails closed during adoption.
|
|
32
34
|
const FOUNDATION_PLUGIN_DRIVER_VERSION = '1.0.0';
|
|
35
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
36
|
+
|
|
37
|
+
function isSafeRelativePath(value) {
|
|
38
|
+
return typeof value === 'string'
|
|
39
|
+
&& value.length > 0
|
|
40
|
+
&& !value.startsWith('/')
|
|
41
|
+
&& !value.includes('\\')
|
|
42
|
+
&& !value.includes(':')
|
|
43
|
+
&& !value.includes('\0')
|
|
44
|
+
&& value.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function decodeCanonicalBase64(value) {
|
|
48
|
+
if (typeof value !== 'string' || value.length % 4 !== 0) return null;
|
|
49
|
+
let paddingStart = -1;
|
|
50
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
51
|
+
const code = value.charCodeAt(index);
|
|
52
|
+
if (code === 61) {
|
|
53
|
+
if (paddingStart === -1) paddingStart = index;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (paddingStart !== -1 || !(
|
|
57
|
+
code >= 65 && code <= 90
|
|
58
|
+
|| code >= 97 && code <= 122
|
|
59
|
+
|| code >= 48 && code <= 57
|
|
60
|
+
|| code === 43
|
|
61
|
+
|| code === 47
|
|
62
|
+
)) return null;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
paddingStart !== -1
|
|
66
|
+
&& value.length - paddingStart > 2
|
|
67
|
+
|| paddingStart !== -1
|
|
68
|
+
&& paddingStart % 4 < 2
|
|
69
|
+
) return null;
|
|
70
|
+
const decoded = Buffer.from(value, 'base64');
|
|
71
|
+
return decoded.toString('base64') === value ? decoded : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasExactKeys(value, keys) {
|
|
75
|
+
return Object.keys(value).sort().join(',') === keys.join(',');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compareUnicodeCodePoints(left, right) {
|
|
79
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0));
|
|
80
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0));
|
|
81
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
82
|
+
for (let index = 0; index < length; index += 1) {
|
|
83
|
+
if (leftPoints[index] !== rightPoints[index]) return leftPoints[index] - rightPoints[index];
|
|
84
|
+
}
|
|
85
|
+
return leftPoints.length - rightPoints.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installCommandExited(result) {
|
|
89
|
+
const commands = result?.facts?.install?.commands;
|
|
90
|
+
if (!Array.isArray(commands)) return false;
|
|
91
|
+
return commands.every((command) => {
|
|
92
|
+
const execution = command?.execution;
|
|
93
|
+
return execution?.exitStatus === 0 && execution.processStatus === 'exited';
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertStableInstallTreeState(state) {
|
|
98
|
+
if (state === undefined) return;
|
|
99
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
100
|
+
throw fail('Foundation install tree state is invalid');
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
state.commandExited === false
|
|
104
|
+
|| state.isolated === false
|
|
105
|
+
|| state.stable === false
|
|
106
|
+
|| state.namespaceWriterActive === true
|
|
107
|
+
) {
|
|
108
|
+
throw fail('Foundation install tree is not a stable isolated post-command tree', {
|
|
109
|
+
commandExited: state.commandExited ?? null,
|
|
110
|
+
isolated: state.isolated ?? null,
|
|
111
|
+
stable: state.stable ?? null,
|
|
112
|
+
namespaceWriterActive: state.namespaceWriterActive ?? null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compareRecordedInstallTree(observation, expectedMembers) {
|
|
118
|
+
if (
|
|
119
|
+
!observation
|
|
120
|
+
|| typeof observation !== 'object'
|
|
121
|
+
|| observation.schemaVersion !== 1
|
|
122
|
+
|| observation.kind !== 'skill-family.filesystem-tree-observation'
|
|
123
|
+
|| !/^[0-9a-f]{64}$/u.test(observation.membersDigest ?? '')
|
|
124
|
+
|| !Array.isArray(observation.members)
|
|
125
|
+
) {
|
|
126
|
+
throw fail('Foundation install tree observation has an invalid shape');
|
|
127
|
+
}
|
|
128
|
+
if (digestDocument(observation.members) !== observation.membersDigest) {
|
|
129
|
+
throw fail('Foundation install tree observation digest does not match its members');
|
|
130
|
+
}
|
|
131
|
+
const actual = new Map();
|
|
132
|
+
const foldedPaths = new Set();
|
|
133
|
+
let previousPath = null;
|
|
134
|
+
for (const member of observation.members) {
|
|
135
|
+
const foldedPath = member?.path?.toLowerCase();
|
|
136
|
+
if (
|
|
137
|
+
!member
|
|
138
|
+
|| !isSafeRelativePath(member.path)
|
|
139
|
+
|| !['directory', 'file', 'symlink'].includes(member.type)
|
|
140
|
+
|| actual.has(member.path)
|
|
141
|
+
|| foldedPaths.has(foldedPath)
|
|
142
|
+
|| previousPath !== null && compareUnicodeCodePoints(previousPath, member.path) >= 0
|
|
143
|
+
) {
|
|
144
|
+
throw fail('Foundation install tree observation contains duplicate or invalid paths');
|
|
145
|
+
}
|
|
146
|
+
actual.set(member.path, member);
|
|
147
|
+
foldedPaths.add(foldedPath);
|
|
148
|
+
const requiredKeys = member.type === 'directory'
|
|
149
|
+
? ['path', 'statMode', 'type']
|
|
150
|
+
: member.type === 'file'
|
|
151
|
+
? ['bytes', 'contentBase64', 'path', 'sha256', 'statMode', 'type']
|
|
152
|
+
: ['bytes', 'path', 'statMode', 'targetBase64', 'type'];
|
|
153
|
+
if (!hasExactKeys(member, requiredKeys)) {
|
|
154
|
+
throw fail('Foundation install tree member has unknown or missing fields', { path: member.path });
|
|
155
|
+
}
|
|
156
|
+
previousPath = member.path;
|
|
157
|
+
if (
|
|
158
|
+
typeof member.statMode !== 'number'
|
|
159
|
+
|| !Number.isInteger(member.statMode)
|
|
160
|
+
|| member.statMode < 0
|
|
161
|
+
|| member.statMode > 0xffff
|
|
162
|
+
) {
|
|
163
|
+
throw fail('Foundation install tree member mode is invalid', { path: member.path });
|
|
164
|
+
}
|
|
165
|
+
if (member.type === 'file') {
|
|
166
|
+
const content = decodeCanonicalBase64(member.contentBase64);
|
|
167
|
+
if (
|
|
168
|
+
!content
|
|
169
|
+
|| typeof member.sha256 !== 'string'
|
|
170
|
+
|| !SHA256_PATTERN.test(member.sha256)
|
|
171
|
+
|| !Number.isSafeInteger(member.bytes)
|
|
172
|
+
|| member.bytes < 0
|
|
173
|
+
|| member.bytes !== content.length
|
|
174
|
+
|| member.sha256 !== digestBytes(content)
|
|
175
|
+
) {
|
|
176
|
+
throw fail('Foundation install tree file record is invalid', { path: member.path });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (member.type === 'symlink') {
|
|
180
|
+
const target = decodeCanonicalBase64(member.targetBase64);
|
|
181
|
+
if (
|
|
182
|
+
!target
|
|
183
|
+
|| target.length === 0
|
|
184
|
+
|| !Number.isSafeInteger(member.bytes)
|
|
185
|
+
|| member.bytes < 1
|
|
186
|
+
|| member.bytes !== target.length
|
|
187
|
+
) {
|
|
188
|
+
throw fail('Foundation install tree link record is invalid', { path: member.path });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const expected = new Map(expectedMembers.map((member) => [member.path, member]));
|
|
193
|
+
for (const [path, expectedMember] of expected) {
|
|
194
|
+
const member = actual.get(path);
|
|
195
|
+
if (!member || member.type === 'symlink' || member.type !== expectedMember.type) {
|
|
196
|
+
throw fail('declared install tree member is missing or unsafe', { path });
|
|
197
|
+
}
|
|
198
|
+
if (member.type === 'file' && (
|
|
199
|
+
member.sha256 !== expectedMember.sha256
|
|
200
|
+
|| member.bytes !== expectedMember.bytes
|
|
201
|
+
|| ((member.statMode & 0o111) !== 0) !== expectedMember.executable
|
|
202
|
+
)) {
|
|
203
|
+
throw fail('declared install tree member drifted from the frozen payload', { path });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const extras = [...actual.values()].filter((member) => !expected.has(member.path));
|
|
207
|
+
const extraInstalledPaths = extras
|
|
208
|
+
.map((member) => member.path)
|
|
209
|
+
.sort(compareUnicodeCodePoints);
|
|
210
|
+
const extraInstalledLinks = extras
|
|
211
|
+
.filter((member) => member.type === 'symlink')
|
|
212
|
+
.map((member) => {
|
|
213
|
+
// Keep the target bytes supplied by Foundation. No path resolution or
|
|
214
|
+
// target read occurs here, so an added link can never be followed.
|
|
215
|
+
return {
|
|
216
|
+
path: member.path,
|
|
217
|
+
targetBase64: member.targetBase64,
|
|
218
|
+
bytes: member.bytes,
|
|
219
|
+
statMode: member.statMode,
|
|
220
|
+
};
|
|
221
|
+
})
|
|
222
|
+
.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
|
|
223
|
+
return { extraInstalledPaths, extraInstalledLinks };
|
|
224
|
+
}
|
|
33
225
|
|
|
34
226
|
function fail(message, details = {}) {
|
|
35
227
|
return new ReleaseError(POST_PUBLISH_VERIFY_FAILED, message, details);
|
|
@@ -135,6 +327,8 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
135
327
|
root,
|
|
136
328
|
runDir,
|
|
137
329
|
runPluginVerificationFn = runPluginVerification,
|
|
330
|
+
observeFilesystemTreeFn = observeFilesystemTree,
|
|
331
|
+
installTreeState,
|
|
138
332
|
clock = () => new Date().toISOString(),
|
|
139
333
|
} = {}) {
|
|
140
334
|
const unit = (plan?.units ?? []).find((candidate) => candidate.id === action?.unitId);
|
|
@@ -231,6 +425,38 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
231
425
|
const result = await runPluginVerificationFn({ request, bindings, hostsRoot });
|
|
232
426
|
assertResultMatchesRequest(result, request);
|
|
233
427
|
|
|
428
|
+
// A3: the installation command (if Foundation reports one) must have
|
|
429
|
+
// exited before the isolated tree is observed. The install-only Foundation
|
|
430
|
+
// path reports no commands, which is the completed local materialization.
|
|
431
|
+
if (!installCommandExited(result)) {
|
|
432
|
+
throw fail('Foundation install tree observation requires an exited install command');
|
|
433
|
+
}
|
|
434
|
+
assertStableInstallTreeState(installTreeState);
|
|
435
|
+
|
|
436
|
+
// Foundation's local install-only contract materializes the payload at the
|
|
437
|
+
// stable child of the isolated install container. Observe that payload root
|
|
438
|
+
// rather than the container, whose private siblings are not plugin bytes.
|
|
439
|
+
const installRoot = join(runDir, 'install', 'payload');
|
|
440
|
+
let treeObservation;
|
|
441
|
+
try {
|
|
442
|
+
const rootBinding = await createFilesystemRootBinding(installRoot);
|
|
443
|
+
treeObservation = await observeFilesystemTreeFn({
|
|
444
|
+
root: installRoot,
|
|
445
|
+
rootBinding,
|
|
446
|
+
symlinkPolicy: { mode: 'record' },
|
|
447
|
+
});
|
|
448
|
+
if (treeObservation?.rootBinding?.digest !== rootBinding.digest) {
|
|
449
|
+
throw fail('Foundation install tree observation root binding drifted');
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof ReleaseError) throw error;
|
|
453
|
+
throw fail('Foundation install tree observation failed closed', {
|
|
454
|
+
cause: error?.message ?? String(error),
|
|
455
|
+
code: error?.code ?? null,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const extraInstallAudit = compareRecordedInstallTree(treeObservation, sourceMembers);
|
|
459
|
+
|
|
234
460
|
return {
|
|
235
461
|
planDigest: plan.digest,
|
|
236
462
|
actionId: action.id,
|
|
@@ -243,5 +469,11 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
243
469
|
payloadMatches: result.facts.install.payloadMatches,
|
|
244
470
|
observationDigest: result.facts.install.observationDigest,
|
|
245
471
|
observedAt: clock(),
|
|
472
|
+
...(extraInstallAudit.extraInstalledPaths.length > 0
|
|
473
|
+
? { extraInstalledPaths: extraInstallAudit.extraInstalledPaths }
|
|
474
|
+
: {}),
|
|
475
|
+
...(extraInstallAudit.extraInstalledLinks.length > 0
|
|
476
|
+
? { extraInstalledLinks: extraInstallAudit.extraInstalledLinks }
|
|
477
|
+
: {}),
|
|
246
478
|
};
|
|
247
479
|
}
|