release-skill 0.2.4 → 0.2.6
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 +45 -0
- package/INSTALL.md +32 -8
- package/INSTALL.zh-CN.md +28 -6
- package/README.md +29 -15
- package/README.zh-CN.md +27 -15
- 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 +2202 -708
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +158 -0
- package/adapters/claude/schemas/release-project.schema.json +6 -0
- package/adapters/claude/schemas/release-run.schema.json +150 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2202 -708
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +158 -0
- package/adapters/codex/schemas/release-project.schema.json +6 -0
- package/adapters/codex/schemas/release-run.schema.json +150 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2202 -708
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +158 -0
- package/adapters/kimi/schemas/release-project.schema.json +6 -0
- package/adapters/kimi/schemas/release-run.schema.json +150 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2202 -708
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +158 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +6 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +150 -0
- package/bin/release-skill.bundle.mjs +2202 -708
- package/package.json +1 -1
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +28 -2
- package/references/05-evidence-and-errors.md +6 -0
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +158 -0
- package/schemas/release-project.schema.json +6 -0
- package/schemas/release-run.schema.json +150 -0
- package/scripts/sync-public-files.mjs +20 -9
- package/src/adapters/plugin-marketplace.mjs +82 -3
- package/src/commands/prepare.mjs +232 -1
- package/src/commands/publish.mjs +134 -0
- package/src/commands/reconcile.mjs +19 -0
- package/src/commands/setup.mjs +26 -0
- package/src/commands/verify.mjs +183 -1
- package/src/core/errors.mjs +12 -0
- package/src/core/plan.mjs +29 -0
- package/src/core/skill-resource-closure.mjs +425 -0
- package/src/core/source-authority.mjs +547 -0
- package/src/platforms/codebuddy.mjs +16 -3
- package/src/platforms/kimi.mjs +36 -9
package/src/commands/prepare.mjs
CHANGED
|
@@ -33,6 +33,11 @@ import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
|
33
33
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
34
34
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
35
35
|
import { sha256Hex } from '../core/digest.mjs';
|
|
36
|
+
import {
|
|
37
|
+
CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
|
|
38
|
+
checkSkillResourceClosure,
|
|
39
|
+
createSkillResourceClosureReceipt,
|
|
40
|
+
} from '../core/skill-resource-closure.mjs';
|
|
36
41
|
import { buildPublicStaging } from '../snapshot/public-map.mjs';
|
|
37
42
|
import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
|
|
38
43
|
import { scanSnapshot } from '../snapshot/scan.mjs';
|
|
@@ -44,7 +49,13 @@ import {
|
|
|
44
49
|
normalizeGitTimestamp,
|
|
45
50
|
sealFrozenSnapshot,
|
|
46
51
|
} from '../snapshot/frozen.mjs';
|
|
47
|
-
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE } from '../core/errors.mjs';
|
|
52
|
+
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT } from '../core/errors.mjs';
|
|
53
|
+
import {
|
|
54
|
+
SOURCE_INPUT_ALGORITHM_VERSION,
|
|
55
|
+
computeSourceInputClosure,
|
|
56
|
+
checkSourceInputDirty,
|
|
57
|
+
verifySnapshotSourcesMatchClosure,
|
|
58
|
+
} from '../core/source-authority.mjs';
|
|
48
59
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
49
60
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
50
61
|
import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
|
|
@@ -1914,6 +1925,87 @@ export async function prepareRelease(options) {
|
|
|
1914
1925
|
});
|
|
1915
1926
|
}
|
|
1916
1927
|
|
|
1928
|
+
// --- Step 3c: Source authority content closure gate ---
|
|
1929
|
+
// After hooks complete, compute the deterministic source-input closure
|
|
1930
|
+
// and verify that closure inputs are clean (no staged/unstaged/untracked
|
|
1931
|
+
// changes). Production configs must declare sourceRepository.
|
|
1932
|
+
const sourceRepository = config.project?.sourceRepository ?? null;
|
|
1933
|
+
const configDefaultBranch = config.project?.defaultBranch ?? null;
|
|
1934
|
+
|
|
1935
|
+
let sourceAuthority = null;
|
|
1936
|
+
let sourceInputClosure = null;
|
|
1937
|
+
if (production) {
|
|
1938
|
+
if (!sourceRepository || typeof sourceRepository !== 'string') {
|
|
1939
|
+
throw new ReleaseError(
|
|
1940
|
+
CONFIG_MISSING,
|
|
1941
|
+
'production prepare requires project.sourceRepository in configuration; source authority content gate needs a workspace source repository',
|
|
1942
|
+
{ configPath },
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
await evidence.append({ phase: 'source-authority', status: 'started' });
|
|
1947
|
+
|
|
1948
|
+
// Compute source-input closure from resolved unit configs
|
|
1949
|
+
const unitConfigsForClosure = configUnits.map((unit, idx) => ({
|
|
1950
|
+
...unit,
|
|
1951
|
+
version: { ...unit.version },
|
|
1952
|
+
}));
|
|
1953
|
+
sourceInputClosure = await computeSourceInputClosure({
|
|
1954
|
+
units: unitConfigsForClosure,
|
|
1955
|
+
root: realRoot,
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
await evidence.append({
|
|
1959
|
+
phase: 'source-authority',
|
|
1960
|
+
step: 'closure-computed',
|
|
1961
|
+
entryCount: sourceInputClosure.entries.length,
|
|
1962
|
+
inputDigest: sourceInputClosure.digest,
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Check only closure inputs for dirty (not whole workspace)
|
|
1966
|
+
const dirtyResult = await checkSourceInputDirty({
|
|
1967
|
+
closure: sourceInputClosure,
|
|
1968
|
+
root: realRoot,
|
|
1969
|
+
});
|
|
1970
|
+
if (dirtyResult.dirty) {
|
|
1971
|
+
await evidence.append({
|
|
1972
|
+
phase: 'source-authority',
|
|
1973
|
+
status: 'blocking',
|
|
1974
|
+
reason: 'DIRTY_SOURCE_INPUT',
|
|
1975
|
+
dirtyPaths: dirtyResult.dirtyPaths,
|
|
1976
|
+
});
|
|
1977
|
+
throw new ReleaseError(
|
|
1978
|
+
DIRTY_SOURCE_INPUT,
|
|
1979
|
+
`source-input closure files have uncommitted changes: ${dirtyResult.dirtyPaths.join(', ')}`,
|
|
1980
|
+
{ dirtyPaths: dirtyResult.dirtyPaths },
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
await evidence.append({
|
|
1985
|
+
phase: 'source-authority',
|
|
1986
|
+
step: 'dirty-check',
|
|
1987
|
+
status: 'clean',
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Build sourceAuthority binding for plan digest
|
|
1991
|
+
sourceAuthority = {
|
|
1992
|
+
sourceRepository,
|
|
1993
|
+
defaultBranch: configDefaultBranch,
|
|
1994
|
+
entries: sourceInputClosure.entries,
|
|
1995
|
+
inputDigest: sourceInputClosure.digest,
|
|
1996
|
+
algorithmVersion: SOURCE_INPUT_ALGORITHM_VERSION,
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
await evidence.append({
|
|
2000
|
+
phase: 'source-authority',
|
|
2001
|
+
status: 'completed',
|
|
2002
|
+
sourceRepository,
|
|
2003
|
+
defaultBranch: configDefaultBranch,
|
|
2004
|
+
inputDigest: sourceInputClosure.digest,
|
|
2005
|
+
remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1917
2009
|
// --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
|
|
1918
2010
|
// reflects any file changes introduced by hooks) ---
|
|
1919
2011
|
await evidence.append({ phase: 'baseline', status: 'started' });
|
|
@@ -2178,6 +2270,75 @@ export async function prepareRelease(options) {
|
|
|
2178
2270
|
gateCount: snapshotGateResults.length,
|
|
2179
2271
|
});
|
|
2180
2272
|
|
|
2273
|
+
// Bind the remote source-authority proof to the exact bytes that entered
|
|
2274
|
+
// the frozen snapshots, not merely to an earlier read of the workspace.
|
|
2275
|
+
// Then re-read the complete closure and dirty state once more so version
|
|
2276
|
+
// sources and non-snapshot closure entries cannot drift during prepare.
|
|
2277
|
+
if (production) {
|
|
2278
|
+
const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
|
|
2279
|
+
closure: sourceInputClosure,
|
|
2280
|
+
unitResults,
|
|
2281
|
+
});
|
|
2282
|
+
if (!snapshotSourceResult.passed) {
|
|
2283
|
+
throw new ReleaseError(
|
|
2284
|
+
DIRTY_SOURCE_INPUT,
|
|
2285
|
+
'source inputs changed between closure calculation and frozen snapshot construction',
|
|
2286
|
+
{
|
|
2287
|
+
reason: 'SNAPSHOT_SOURCE_DRIFT',
|
|
2288
|
+
dirtyPaths: snapshotSourceResult.error.paths,
|
|
2289
|
+
},
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
const finalClosure = await computeSourceInputClosure({
|
|
2294
|
+
units: configUnits,
|
|
2295
|
+
root: realRoot,
|
|
2296
|
+
});
|
|
2297
|
+
if (finalClosure.digest !== sourceInputClosure.digest) {
|
|
2298
|
+
const initialByPath = new Map(
|
|
2299
|
+
sourceInputClosure.entries.map((entry) => [entry.path, entry]),
|
|
2300
|
+
);
|
|
2301
|
+
const finalByPath = new Map(
|
|
2302
|
+
finalClosure.entries.map((entry) => [entry.path, entry]),
|
|
2303
|
+
);
|
|
2304
|
+
const changedPaths = [...new Set([
|
|
2305
|
+
...sourceInputClosure.entries.map((entry) => entry.path),
|
|
2306
|
+
...finalClosure.entries.map((entry) => entry.path),
|
|
2307
|
+
])].filter((path) => (
|
|
2308
|
+
JSON.stringify(initialByPath.get(path) ?? null)
|
|
2309
|
+
!== JSON.stringify(finalByPath.get(path) ?? null)
|
|
2310
|
+
)).sort();
|
|
2311
|
+
throw new ReleaseError(
|
|
2312
|
+
DIRTY_SOURCE_INPUT,
|
|
2313
|
+
'source-input closure changed while preparing frozen snapshots',
|
|
2314
|
+
{ reason: 'SOURCE_CLOSURE_DRIFT', dirtyPaths: changedPaths },
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const finalDirtyResult = await checkSourceInputDirty({
|
|
2319
|
+
closure: finalClosure,
|
|
2320
|
+
root: realRoot,
|
|
2321
|
+
});
|
|
2322
|
+
if (finalDirtyResult.dirty) {
|
|
2323
|
+
throw new ReleaseError(
|
|
2324
|
+
DIRTY_SOURCE_INPUT,
|
|
2325
|
+
`source-input closure files have uncommitted changes after snapshot construction: ${finalDirtyResult.dirtyPaths.join(', ')}`,
|
|
2326
|
+
{
|
|
2327
|
+
reason: 'DIRTY_AFTER_SNAPSHOT',
|
|
2328
|
+
dirtyPaths: finalDirtyResult.dirtyPaths,
|
|
2329
|
+
},
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
await evidence.append({
|
|
2334
|
+
phase: 'source-authority',
|
|
2335
|
+
step: 'snapshot-binding',
|
|
2336
|
+
status: 'completed',
|
|
2337
|
+
inputDigest: sourceInputClosure.digest,
|
|
2338
|
+
snapshotSourceCount: snapshotSourceResult.observation.snapshotSourceCount,
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2181
2342
|
// --- Step 6: Remote uniqueness (deferred to publish preflight) ---
|
|
2182
2343
|
// Prepare only observes the previous public baseline (already done above).
|
|
2183
2344
|
// Remote uniqueness checks (tag, GitHub Release, npm version) are deferred
|
|
@@ -2234,6 +2395,67 @@ export async function prepareRelease(options) {
|
|
|
2234
2395
|
)
|
|
2235
2396
|
: null;
|
|
2236
2397
|
|
|
2398
|
+
// --- Step 7b: Skill resource closure gate ---
|
|
2399
|
+
// Production snapshots are sealed inside buildProductionAssets. Scan only
|
|
2400
|
+
// after that transition so the receipt binds the exact byte/mode identity
|
|
2401
|
+
// publish will re-verify, rather than the writable pre-freeze staging tree.
|
|
2402
|
+
// Non-production plans scan the final staging tree at the same point.
|
|
2403
|
+
// This gate is built in, read-only, and cannot be disabled by overlays.
|
|
2404
|
+
const skillResourceClosureResults = [];
|
|
2405
|
+
for (const { unit, manifest } of unitResults) {
|
|
2406
|
+
await evidence.append({
|
|
2407
|
+
phase: 'skill-resource-closure',
|
|
2408
|
+
status: 'started',
|
|
2409
|
+
unitId: unit.id,
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
const closureResult = await checkSkillResourceClosure({
|
|
2413
|
+
snapshotDir: manifest.outputDir,
|
|
2414
|
+
host: 'root',
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2417
|
+
const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
|
|
2418
|
+
skillResourceClosureResults.push(receipt);
|
|
2419
|
+
|
|
2420
|
+
if (closureResult.findings.length > 0) {
|
|
2421
|
+
await evidence.append({
|
|
2422
|
+
phase: 'skill-resource-closure',
|
|
2423
|
+
status: 'blocking',
|
|
2424
|
+
unitId: unit.id,
|
|
2425
|
+
findingCount: closureResult.findings.length,
|
|
2426
|
+
findings: closureResult.findings.map((f) => ({
|
|
2427
|
+
skill: f.skill,
|
|
2428
|
+
line: f.line,
|
|
2429
|
+
reference: f.reference,
|
|
2430
|
+
classification: f.classification,
|
|
2431
|
+
code: f.code,
|
|
2432
|
+
})),
|
|
2433
|
+
});
|
|
2434
|
+
throw new ReleaseError(
|
|
2435
|
+
GATE_FAILED,
|
|
2436
|
+
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2437
|
+
{
|
|
2438
|
+
unitId: unit.id,
|
|
2439
|
+
findingCount: closureResult.findings.length,
|
|
2440
|
+
findings: closureResult.findings,
|
|
2441
|
+
},
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
await evidence.append({
|
|
2446
|
+
phase: 'skill-resource-closure',
|
|
2447
|
+
status: 'completed',
|
|
2448
|
+
unitId: unit.id,
|
|
2449
|
+
checkerVersion: closureResult.checkerVersion,
|
|
2450
|
+
surfaceCount: receipt.surfaceCount,
|
|
2451
|
+
skillCount: receipt.skillCount,
|
|
2452
|
+
referenceCount: closureResult.referenceCount,
|
|
2453
|
+
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2454
|
+
findingCount: 0,
|
|
2455
|
+
receiptDigest: closureResult.receiptDigest,
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2237
2459
|
// Freeze external independent marketplace HEADs (production + online only):
|
|
2238
2460
|
// for each claude/codex/codebuddy distribution declaring marketplaceRepo,
|
|
2239
2461
|
// resolve the external repo's HEAD sha + default branch and validate the
|
|
@@ -2581,6 +2803,14 @@ export async function prepareRelease(options) {
|
|
|
2581
2803
|
capturedAt: baseline.capturedAt,
|
|
2582
2804
|
},
|
|
2583
2805
|
configDigest,
|
|
2806
|
+
skillResourceClosure: {
|
|
2807
|
+
checkerVersion: SKILL_RESOURCE_CHECKER_VERSION,
|
|
2808
|
+
unitReceipts: skillResourceClosureResults,
|
|
2809
|
+
totalSkillCount: skillResourceClosureResults.reduce((sum, item) => sum + item.skillCount, 0),
|
|
2810
|
+
totalReferenceCount: skillResourceClosureResults.reduce((sum, item) => sum + item.referenceCount, 0),
|
|
2811
|
+
totalSourceOnlyCount: skillResourceClosureResults.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
|
|
2812
|
+
totalFindingCount: 0,
|
|
2813
|
+
},
|
|
2584
2814
|
verificationGates: config.verificationGates ?? [],
|
|
2585
2815
|
snapshotDigest: overallSnapshotDigest,
|
|
2586
2816
|
...(production ? {
|
|
@@ -2591,6 +2821,7 @@ export async function prepareRelease(options) {
|
|
|
2591
2821
|
} : {}),
|
|
2592
2822
|
units,
|
|
2593
2823
|
externalActions,
|
|
2824
|
+
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
2594
2825
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
2595
2826
|
};
|
|
2596
2827
|
|
package/src/commands/publish.mjs
CHANGED
|
@@ -41,6 +41,12 @@ import {
|
|
|
41
41
|
reObservePreviousPublicBaseline,
|
|
42
42
|
} from '../core/previous-public-baseline.mjs';
|
|
43
43
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
44
|
+
import {
|
|
45
|
+
CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
|
|
46
|
+
assertSkillResourceClosureReceipt,
|
|
47
|
+
checkSkillResourceClosure,
|
|
48
|
+
createSkillResourceClosureReceipt,
|
|
49
|
+
} from '../core/skill-resource-closure.mjs';
|
|
44
50
|
import {
|
|
45
51
|
ADAPTER_ACTION_TYPE_MAP,
|
|
46
52
|
TIER_TABLE,
|
|
@@ -53,10 +59,15 @@ import { appendRunState, createProductionRunDir, writeRunAtomic, resolveDefaultR
|
|
|
53
59
|
import {
|
|
54
60
|
ReleaseError,
|
|
55
61
|
GATE_FAILED,
|
|
62
|
+
CONFIG_MISSING,
|
|
56
63
|
BASELINE_CHANGED,
|
|
57
64
|
PARTIAL_RELEASE,
|
|
58
65
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
59
66
|
} from '../core/errors.mjs';
|
|
67
|
+
import {
|
|
68
|
+
verifyRemoteSourceContent,
|
|
69
|
+
createSourceAuthorityReceipt,
|
|
70
|
+
} from '../core/source-authority.mjs';
|
|
60
71
|
import { assertTransition, PUBLISHING, PUBLISHED, PARTIAL } from '../core/state-machine.mjs';
|
|
61
72
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
62
73
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY, isPropagatingMissing } from '../core/observe-retry.mjs';
|
|
@@ -521,6 +532,14 @@ export async function publishRelease(options) {
|
|
|
521
532
|
if (productionMode && !isProductionPlan) {
|
|
522
533
|
throw new ReleaseError(GATE_FAILED, 'production publish requires a github-npm-v1 frozen plan');
|
|
523
534
|
}
|
|
535
|
+
if (isProductionPlan && !plan.sourceAuthority) {
|
|
536
|
+
throw new ReleaseError(
|
|
537
|
+
CONFIG_MISSING,
|
|
538
|
+
'production publish requires a frozen sourceAuthority binding; re-run prepare with project.sourceRepository configured',
|
|
539
|
+
{ gate: 'source-authority' },
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const verifiedFrozenSnapshots = new Map();
|
|
524
543
|
if (isProductionPlan) {
|
|
525
544
|
if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
|
|
526
545
|
throw new ReleaseError(GATE_FAILED, 'production plan requires a dedicated assetRoot');
|
|
@@ -566,6 +585,7 @@ export async function publishRelease(options) {
|
|
|
566
585
|
snapshotPath: frozen.path,
|
|
567
586
|
expectedDigest: frozen.manifestDigest,
|
|
568
587
|
});
|
|
588
|
+
verifiedFrozenSnapshots.set(unit.id, snapshot.snapshotDir);
|
|
569
589
|
assertInsideAssetRoot(assetRoot, snapshot.snapshotDir, 'frozen snapshot');
|
|
570
590
|
const git = await verifyFrozenGitRepository({
|
|
571
591
|
root,
|
|
@@ -599,6 +619,60 @@ export async function publishRelease(options) {
|
|
|
599
619
|
await evidence.append({ phase: 'safety-gate', gate: 'frozen-artifacts', status: 'passed' });
|
|
600
620
|
}
|
|
601
621
|
|
|
622
|
+
// =======================================================================
|
|
623
|
+
// Safety Gate 2c: Skill resource closure recheck
|
|
624
|
+
// Re-run the skill resource closure check on each unit's frozen snapshot.
|
|
625
|
+
// Requires findingCount=0 and receipt matches the plan.
|
|
626
|
+
// =======================================================================
|
|
627
|
+
if (plan.skillResourceClosure) {
|
|
628
|
+
await evidence.append({ phase: 'safety-gate', gate: 'skill-resource-closure', status: 'started' });
|
|
629
|
+
const expectedReceipts = plan.skillResourceClosure.unitReceipts ?? [];
|
|
630
|
+
if (plan.skillResourceClosure.checkerVersion !== SKILL_RESOURCE_CHECKER_VERSION) {
|
|
631
|
+
throw new ReleaseError(
|
|
632
|
+
GATE_FAILED,
|
|
633
|
+
`skill resource closure checker version mismatch: plan=${plan.skillResourceClosure.checkerVersion}, current=${SKILL_RESOURCE_CHECKER_VERSION}`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (expectedReceipts.length !== plan.units.length) {
|
|
637
|
+
throw new ReleaseError(GATE_FAILED, 'skill resource closure receipt set does not cover every release unit');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (const [unitId, snapshotDir] of verifiedFrozenSnapshots) {
|
|
641
|
+
const expected = expectedReceipts.find((item) => item.unitId === unitId);
|
|
642
|
+
if (!expected) {
|
|
643
|
+
throw new ReleaseError(GATE_FAILED, `skill resource closure receipt missing for unit "${unitId}"`);
|
|
644
|
+
}
|
|
645
|
+
const closureResult = await checkSkillResourceClosure({
|
|
646
|
+
snapshotDir,
|
|
647
|
+
host: 'root',
|
|
648
|
+
});
|
|
649
|
+
if (closureResult.findings.length > 0) {
|
|
650
|
+
await evidence.append({
|
|
651
|
+
phase: 'safety-gate',
|
|
652
|
+
gate: 'skill-resource-closure',
|
|
653
|
+
status: 'failed',
|
|
654
|
+
unitId,
|
|
655
|
+
findingCount: closureResult.findings.length,
|
|
656
|
+
});
|
|
657
|
+
throw new ReleaseError(
|
|
658
|
+
GATE_FAILED,
|
|
659
|
+
`skill resource closure recheck failed for unit "${unitId}": ${closureResult.findings.length} finding(s)`,
|
|
660
|
+
{ unitId, findings: closureResult.findings },
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
const observed = createSkillResourceClosureReceipt(closureResult, { unitId });
|
|
664
|
+
assertSkillResourceClosureReceipt(expected, observed, `unit "${unitId}"`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
await evidence.append({
|
|
668
|
+
phase: 'safety-gate',
|
|
669
|
+
gate: 'skill-resource-closure',
|
|
670
|
+
status: 'passed',
|
|
671
|
+
receiptCount: expectedReceipts.length,
|
|
672
|
+
recheckedReceiptCount: verifiedFrozenSnapshots.size,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
602
676
|
// =======================================================================
|
|
603
677
|
// Safety Gates 3-7: Load and validate approval record (shared)
|
|
604
678
|
// =======================================================================
|
|
@@ -870,6 +944,65 @@ export async function publishRelease(options) {
|
|
|
870
944
|
}
|
|
871
945
|
}
|
|
872
946
|
|
|
947
|
+
// =======================================================================
|
|
948
|
+
// Safety Gate 11: Source authority content closure verification
|
|
949
|
+
// Verifies that the frozen source-input closure content exists in the
|
|
950
|
+
// workspace's remote default branch. Must pass before ANY adapter execute.
|
|
951
|
+
// Failure => zero adapter execute calls.
|
|
952
|
+
// =======================================================================
|
|
953
|
+
let sourceAuthorityReceipt = null;
|
|
954
|
+
if (plan.sourceAuthority) {
|
|
955
|
+
await evidence.append({ phase: 'safety-gate', gate: 'source-authority', status: 'started' });
|
|
956
|
+
|
|
957
|
+
const sa = plan.sourceAuthority;
|
|
958
|
+
|
|
959
|
+
// Verify the remote source tree against the entries frozen into the
|
|
960
|
+
// digest-bound plan. Publish must never rebuild authority from the
|
|
961
|
+
// mutable workspace or current config.
|
|
962
|
+
const frozenClosure = {
|
|
963
|
+
algorithmVersion: sa.algorithmVersion,
|
|
964
|
+
digest: sa.inputDigest,
|
|
965
|
+
entries: sa.entries,
|
|
966
|
+
};
|
|
967
|
+
const remoteResult = await verifyRemoteSourceContent({
|
|
968
|
+
sourceRepository: sa.sourceRepository,
|
|
969
|
+
defaultBranch: sa.defaultBranch,
|
|
970
|
+
closure: frozenClosure,
|
|
971
|
+
readRemoteFn: options.readRemoteSourceFn,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
if (!remoteResult.passed) {
|
|
975
|
+
const errorCode = remoteResult.error?.code ?? GATE_FAILED;
|
|
976
|
+
await evidence.append({
|
|
977
|
+
phase: 'safety-gate',
|
|
978
|
+
gate: 'source-authority',
|
|
979
|
+
status: 'failed',
|
|
980
|
+
error: remoteResult.error,
|
|
981
|
+
});
|
|
982
|
+
throw new ReleaseError(
|
|
983
|
+
errorCode,
|
|
984
|
+
`source authority content gate failed: ${remoteResult.error?.message}`,
|
|
985
|
+
remoteResult.error,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Create receipt for successful verification
|
|
990
|
+
sourceAuthorityReceipt = createSourceAuthorityReceipt({
|
|
991
|
+
plan,
|
|
992
|
+
result: 'CONSISTENT',
|
|
993
|
+
observation: remoteResult.observation,
|
|
994
|
+
clock: clockFn,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
await evidence.append({
|
|
998
|
+
phase: 'safety-gate',
|
|
999
|
+
gate: 'source-authority',
|
|
1000
|
+
status: 'passed',
|
|
1001
|
+
sourceRepository: sa.sourceRepository,
|
|
1002
|
+
defaultBranch: sa.defaultBranch,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
873
1006
|
// =======================================================================
|
|
874
1007
|
// All safety gates passed -- prepare for execution
|
|
875
1008
|
// =======================================================================
|
|
@@ -1007,6 +1140,7 @@ export async function publishRelease(options) {
|
|
|
1007
1140
|
: {}),
|
|
1008
1141
|
})),
|
|
1009
1142
|
startedAt,
|
|
1143
|
+
...(sourceAuthorityReceipt ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] } : {}),
|
|
1010
1144
|
...(finishedAt ? { finishedAt } : {}),
|
|
1011
1145
|
});
|
|
1012
1146
|
let stateSequence = 0;
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
60
60
|
} from '../core/errors.mjs';
|
|
61
61
|
import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
|
|
62
|
+
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
62
63
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
63
64
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
|
|
64
65
|
|
|
@@ -233,6 +234,18 @@ export async function reconcileRelease(options) {
|
|
|
233
234
|
`reconcile source command must be publish or reconcile, got "${sourceRun.command}"`,
|
|
234
235
|
);
|
|
235
236
|
}
|
|
237
|
+
let sourceAuthorityReceipt = null;
|
|
238
|
+
if (plan.sourceAuthority) {
|
|
239
|
+
const receiptResult = verifySourceAuthorityReceipt({ plan, run: sourceRun });
|
|
240
|
+
if (!receiptResult.passed) {
|
|
241
|
+
throw new ReleaseError(
|
|
242
|
+
GATE_FAILED,
|
|
243
|
+
`reconcile source run has no valid source-authority receipt: ${receiptResult.reason}`,
|
|
244
|
+
{ gate: 'source-authority', sourceRunId: sourceRun.runId },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
sourceAuthorityReceipt = receiptResult.receipt;
|
|
248
|
+
}
|
|
236
249
|
|
|
237
250
|
let consumedApprovalPath = sourceRun.approvalPath;
|
|
238
251
|
let consumedApprovalDigest = sourceRun.approvalDigest;
|
|
@@ -956,6 +969,9 @@ export async function reconcileRelease(options) {
|
|
|
956
969
|
sourceRunDigest: sourceAuthorityDigest,
|
|
957
970
|
sourceRunPath,
|
|
958
971
|
status,
|
|
972
|
+
...(sourceAuthorityReceipt
|
|
973
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
974
|
+
: {}),
|
|
959
975
|
checkpoints: planActions.map((action) => {
|
|
960
976
|
const value = actionResults.get(action.id);
|
|
961
977
|
const normalized = value === 'deferred' ? 'deferred'
|
|
@@ -1303,6 +1319,9 @@ export async function reconcileRelease(options) {
|
|
|
1303
1319
|
sourceRunDigest,
|
|
1304
1320
|
sourceRunPath,
|
|
1305
1321
|
status: overallStatus,
|
|
1322
|
+
...(sourceAuthorityReceipt
|
|
1323
|
+
? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
|
|
1324
|
+
: {}),
|
|
1306
1325
|
checkpoints: planActions.map((a) => {
|
|
1307
1326
|
const status = actionResults.get(a.id) ?? 'pending';
|
|
1308
1327
|
const normalized = status === 'deferred' ? 'deferred'
|
package/src/commands/setup.mjs
CHANGED
|
@@ -976,6 +976,21 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
976
976
|
const legacyOwner = facts.legacyReleaseConfigs
|
|
977
977
|
.map((c) => c.owner)
|
|
978
978
|
.find(Boolean);
|
|
979
|
+
const sourceRepositoryCandidates = [...new Set(
|
|
980
|
+
facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
981
|
+
)].sort();
|
|
982
|
+
if (sourceRepositoryCandidates.length !== 1) {
|
|
983
|
+
return {
|
|
984
|
+
answers: null,
|
|
985
|
+
conflicts: [{
|
|
986
|
+
code: sourceRepositoryCandidates.length === 0
|
|
987
|
+
? 'SOURCE_REPOSITORY_MISSING'
|
|
988
|
+
: 'SOURCE_REPOSITORY_AMBIGUOUS',
|
|
989
|
+
candidates: sourceRepositoryCandidates,
|
|
990
|
+
}],
|
|
991
|
+
assumptions,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
979
994
|
|
|
980
995
|
const units = [];
|
|
981
996
|
for (const unit of candidates.units) {
|
|
@@ -1156,6 +1171,7 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
1156
1171
|
project: {
|
|
1157
1172
|
name: facts.packages[0]?.name ?? 'project',
|
|
1158
1173
|
defaultBranch: facts.legacyReleaseConfigs[0]?.defaultBranch ?? facts.git.branch ?? 'main',
|
|
1174
|
+
sourceRepository: sourceRepositoryCandidates[0],
|
|
1159
1175
|
},
|
|
1160
1176
|
releaseUnits: units,
|
|
1161
1177
|
...(selectedGateIds.length > 0 ? {
|
|
@@ -1517,6 +1533,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1517
1533
|
|
|
1518
1534
|
const facts = await discoverFacts(rootReal);
|
|
1519
1535
|
const candidates = buildCandidates(facts);
|
|
1536
|
+
const sourceRepositoryCandidates = [...new Set(
|
|
1537
|
+
facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1538
|
+
)].sort();
|
|
1520
1539
|
let answers = null;
|
|
1521
1540
|
if (answersPath) {
|
|
1522
1541
|
const resolvedAnswers = isAbsolute(answersPath) ? answersPath : resolve(rootReal, answersPath);
|
|
@@ -1527,6 +1546,7 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1527
1546
|
const digestAuthority = {
|
|
1528
1547
|
setupVersion: 1,
|
|
1529
1548
|
facts,
|
|
1549
|
+
sourceRepositoryCandidates,
|
|
1530
1550
|
releaseUnitCandidates: candidates.units,
|
|
1531
1551
|
gateCandidates: candidates.gates,
|
|
1532
1552
|
selectedGateIds,
|
|
@@ -1599,6 +1619,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1599
1619
|
const lockedAuthority = {
|
|
1600
1620
|
setupVersion: 1,
|
|
1601
1621
|
facts: lockedFacts,
|
|
1622
|
+
sourceRepositoryCandidates: [...new Set(
|
|
1623
|
+
lockedFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1624
|
+
)].sort(),
|
|
1602
1625
|
releaseUnitCandidates: lockedCandidates.units,
|
|
1603
1626
|
gateCandidates: lockedCandidates.gates,
|
|
1604
1627
|
selectedGateIds: lockedAnswers.selectedGateIds,
|
|
@@ -1622,6 +1645,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
|
|
|
1622
1645
|
const finalAuthority = {
|
|
1623
1646
|
setupVersion: 1,
|
|
1624
1647
|
facts: finalFacts,
|
|
1648
|
+
sourceRepositoryCandidates: [...new Set(
|
|
1649
|
+
finalFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
|
|
1650
|
+
)].sort(),
|
|
1625
1651
|
releaseUnitCandidates: finalCandidates.units,
|
|
1626
1652
|
gateCandidates: finalCandidates.gates,
|
|
1627
1653
|
selectedGateIds: finalAnswers.selectedGateIds,
|