release-skill 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +13 -21
- package/README.zh-CN.md +14 -22
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/kernel-protocol.json +1 -1
- package/adapters/claude/bin/registry.json +15 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +401 -2
- package/adapters/claude/schemas/release-project.schema.json +291 -0
- package/adapters/claude/schemas/release-run.schema.json +59 -6
- package/adapters/claude/skills/release-config/SKILL.md +140 -0
- package/adapters/claude/skills/release-docs/SKILL.md +106 -0
- package/adapters/claude/skills/release-help/SKILL.md +39 -0
- package/adapters/claude/skills/release-marketplace/SKILL.md +147 -0
- package/adapters/claude/skills/release-publish/SKILL.md +25 -6
- package/adapters/claude/skills/release-verify/SKILL.md +10 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/kernel-protocol.json +1 -1
- package/adapters/codex/bin/registry.json +15 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +401 -2
- package/adapters/codex/schemas/release-project.schema.json +291 -0
- package/adapters/codex/schemas/release-run.schema.json +59 -6
- package/adapters/codex/skills/release-config/SKILL.md +147 -0
- package/adapters/codex/skills/release-docs/SKILL.md +113 -0
- package/adapters/codex/skills/release-help/SKILL.md +39 -0
- package/adapters/codex/skills/release-marketplace/SKILL.md +154 -0
- package/adapters/codex/skills/release-publish/SKILL.md +25 -6
- package/adapters/codex/skills/release-verify/SKILL.md +10 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/kernel-protocol.json +1 -1
- package/adapters/kimi/bin/registry.json +15 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +401 -2
- package/adapters/kimi/schemas/release-project.schema.json +291 -0
- package/adapters/kimi/schemas/release-run.schema.json +59 -6
- package/adapters/kimi/skills/release-config/SKILL.md +147 -0
- package/adapters/kimi/skills/release-docs/SKILL.md +113 -0
- package/adapters/kimi/skills/release-help/SKILL.md +39 -0
- package/adapters/kimi/skills/release-marketplace/SKILL.md +154 -0
- package/adapters/kimi/skills/release-publish/SKILL.md +25 -6
- package/adapters/kimi/skills/release-verify/SKILL.md +10 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/kernel-protocol.json +1 -1
- package/adapters/workbuddy/bin/registry.json +15 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +38147 -23284
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +401 -2
- package/adapters/workbuddy/schemas/release-project.schema.json +291 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +59 -6
- package/adapters/workbuddy/skills/release-config/SKILL.md +140 -0
- package/adapters/workbuddy/skills/release-docs/SKILL.md +106 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +39 -0
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +147 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +25 -6
- package/adapters/workbuddy/skills/release-verify/SKILL.md +10 -0
- package/bin/error-codes.json +1 -1
- package/bin/kernel-protocol.json +1 -1
- package/bin/registry.json +15 -1
- package/bin/release-skill-cli.mjs +235 -7
- package/bin/release-skill.bundle.mjs +38147 -23284
- package/bin/rules.json +1 -1
- package/package.json +5 -4
- package/platform-manifest.json +359 -0
- package/references/.render-manifest.json +9 -9
- package/references/02-project-config.md +35 -0
- package/references/05-evidence-and-errors.md +44 -0
- package/references/06-adapter-contract.md +13 -0
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +401 -2
- package/schemas/release-project.schema.json +291 -0
- package/schemas/release-run.schema.json +59 -6
- package/skills/release-config/SKILL.md +140 -0
- package/skills/release-docs/SKILL.md +106 -0
- package/skills/release-help/SKILL.md +39 -0
- package/skills/release-marketplace/SKILL.md +147 -0
- package/skills/release-publish/SKILL.md +25 -6
- package/skills/release-verify/SKILL.md +10 -0
- package/skills-src/release-config/SKILL.md +140 -0
- package/skills-src/release-docs/SKILL.md +106 -0
- package/skills-src/release-help/SKILL.md +39 -0
- package/skills-src/release-marketplace/SKILL.md +147 -0
- package/skills-src/release-publish/SKILL.md +25 -6
- package/skills-src/release-verify/SKILL.md +10 -0
- package/src/adapters/contract.mjs +5 -0
- package/src/adapters/distribute-git.mjs +625 -0
- package/src/adapters/plugin-marketplace.mjs +48 -4
- package/src/adapters/push-snapshot.mjs +12 -4
- package/src/commands/assess.mjs +171 -0
- package/src/commands/distribute.mjs +1078 -0
- package/src/commands/hooks.mjs +6 -1
- package/src/commands/lineage.mjs +616 -0
- package/src/commands/prepare.mjs +413 -72
- package/src/commands/route.mjs +686 -0
- package/src/commands/ship.mjs +45 -1
- package/src/commands/verify.mjs +176 -0
- package/src/core/baseline-advance.mjs +185 -0
- package/src/core/baseline.mjs +11 -1
- package/src/core/checkpoints.mjs +25 -0
- package/src/core/foundation-inflight.mjs +7 -0
- package/src/core/plan.mjs +12 -2
- package/src/core/postpublish.mjs +315 -0
package/src/commands/ship.mjs
CHANGED
|
@@ -60,6 +60,7 @@ async function defaultDependencies() {
|
|
|
60
60
|
publishModule,
|
|
61
61
|
reconcileModule,
|
|
62
62
|
verifyModule,
|
|
63
|
+
distributeModule,
|
|
63
64
|
transportModule,
|
|
64
65
|
metadataModule,
|
|
65
66
|
] = await Promise.all([
|
|
@@ -69,6 +70,7 @@ async function defaultDependencies() {
|
|
|
69
70
|
import('./publish.mjs'),
|
|
70
71
|
import('./reconcile.mjs'),
|
|
71
72
|
import('./verify.mjs'),
|
|
73
|
+
import('./distribute.mjs'),
|
|
72
74
|
import('../core/git-transport.mjs'),
|
|
73
75
|
import('../core/release-metadata.mjs'),
|
|
74
76
|
]);
|
|
@@ -79,6 +81,7 @@ async function defaultDependencies() {
|
|
|
79
81
|
publishRelease: publishModule.publishRelease,
|
|
80
82
|
reconcileRelease: reconcileModule.reconcileRelease,
|
|
81
83
|
verifyRelease: verifyModule.verifyRelease,
|
|
84
|
+
distributeRelease: distributeModule.distributeRelease,
|
|
82
85
|
preflightGitTransports: transportModule.preflightGitTransports,
|
|
83
86
|
updatePreviousPublicBaselines: metadataModule.updatePreviousPublicBaselines,
|
|
84
87
|
};
|
|
@@ -102,6 +105,7 @@ function publicState(state) {
|
|
|
102
105
|
...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
|
|
103
106
|
...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
|
|
104
107
|
...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
|
|
108
|
+
...(state.distributeRunPath ? { distributeRunPath: state.distributeRunPath } : {}),
|
|
105
109
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
106
110
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
107
111
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
@@ -301,10 +305,49 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
301
305
|
}
|
|
302
306
|
|
|
303
307
|
if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
|
|
308
|
+
// Step 1: Check if postPublish requires distribution
|
|
309
|
+
const plan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
310
|
+
const needsDistribution = plan.postPublish && plan.postPublish.targets && plan.postPublish.targets.length > 0;
|
|
311
|
+
|
|
312
|
+
if (needsDistribution && deps.distributeRelease) {
|
|
313
|
+
state.status = 'DISTRIBUTING';
|
|
314
|
+
await writeJsonAtomic(statePath, state);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const distributed = await deps.distributeRelease({
|
|
318
|
+
sourceRunPath: state.sourceRunPath,
|
|
319
|
+
approvalPath: state.approvalPath,
|
|
320
|
+
adapterRegistry: options.adapterRegistry,
|
|
321
|
+
root,
|
|
322
|
+
dryRun: false,
|
|
323
|
+
planPath: state.planPath,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
state = {
|
|
327
|
+
...state,
|
|
328
|
+
status: distributed.status,
|
|
329
|
+
distributeRunPath: distributed.distributeRunPath,
|
|
330
|
+
updatedAt: new Date().toISOString(),
|
|
331
|
+
};
|
|
332
|
+
await writeJsonAtomic(statePath, state);
|
|
333
|
+
|
|
334
|
+
if (!distributed.checkpoints || distributed.checkpoints.some((cp) => cp.status === 'failed')) {
|
|
335
|
+
state.status = 'PARTIAL';
|
|
336
|
+
await writeJsonAtomic(statePath, state);
|
|
337
|
+
return publicState(state);
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
state.status = 'PARTIAL';
|
|
341
|
+
await writeJsonAtomic(statePath, state);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Step 2: Verify (this will check for distributed run if distribution was required)
|
|
304
347
|
try {
|
|
305
348
|
const verified = await deps.verifyRelease({
|
|
306
349
|
planPath: state.planPath,
|
|
307
|
-
sourceRunPath: state.sourceRunPath,
|
|
350
|
+
sourceRunPath: needsDistribution ? state.distributeRunPath : state.sourceRunPath,
|
|
308
351
|
adapterRegistry: options.adapterRegistry,
|
|
309
352
|
root,
|
|
310
353
|
verificationGatesAuthorized: state.verificationGatesAuthorized === true,
|
|
@@ -315,6 +358,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
315
358
|
verifyRunPath: verified.runPath,
|
|
316
359
|
requirements: undefined,
|
|
317
360
|
manualFollowUps: verified.manualFollowUps ?? undefined,
|
|
361
|
+
baselineAdvance: verified.baselineAdvance ?? undefined,
|
|
318
362
|
updatedAt: new Date().toISOString(),
|
|
319
363
|
};
|
|
320
364
|
await writeJsonAtomic(statePath, state);
|
package/src/commands/verify.mjs
CHANGED
|
@@ -46,6 +46,13 @@ import {
|
|
|
46
46
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
47
47
|
} from '../core/errors.mjs';
|
|
48
48
|
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
49
|
+
import {
|
|
50
|
+
deriveBaselineAdvances,
|
|
51
|
+
applyBaselineAdvances,
|
|
52
|
+
isWorktreeFileClean,
|
|
53
|
+
commitBaselineAdvance,
|
|
54
|
+
} from '../core/baseline-advance.mjs';
|
|
55
|
+
import { loadProjectConfig } from '../core/config.mjs';
|
|
49
56
|
import { assertTransition, PUBLISHED, VERIFIED } from '../core/state-machine.mjs';
|
|
50
57
|
import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
|
|
51
58
|
import {
|
|
@@ -725,6 +732,95 @@ const defaultNpmExecutor = {
|
|
|
725
732
|
},
|
|
726
733
|
};
|
|
727
734
|
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Distribute run discovery
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Discover sibling distribute runs from the same planDigest within .release-skill/runs.
|
|
741
|
+
* Only accept verified, non-symbolic-link directories matching distribute-* prefix.
|
|
742
|
+
*/
|
|
743
|
+
async function discoverDistributeRuns({ planPath, plan }) {
|
|
744
|
+
const planDir = dirname(planPath);
|
|
745
|
+
const releaseDir = basename(planDir) === 'plans' ? dirname(planDir) : planDir;
|
|
746
|
+
const runsDir = resolve(releaseDir, 'runs');
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
const runsDirStat = await lstat(runsDir);
|
|
750
|
+
if (runsDirStat.isSymbolicLink()) {
|
|
751
|
+
throw new ReleaseError(
|
|
752
|
+
GATE_FAILED,
|
|
753
|
+
'runs directory is a symbolic link; authority identity compromised',
|
|
754
|
+
{ runsDir },
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
if (!runsDirStat.isDirectory()) {
|
|
758
|
+
throw new ReleaseError(
|
|
759
|
+
GATE_FAILED,
|
|
760
|
+
'runs path is not a directory',
|
|
761
|
+
{ runsDir },
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const runsDirReal = realpathSync(runsDir);
|
|
766
|
+
const authorityDirReal = realpathSync(releaseDir);
|
|
767
|
+
if (!runsDirReal.startsWith(authorityDirReal + '/') && runsDirReal !== authorityDirReal) {
|
|
768
|
+
throw new ReleaseError(
|
|
769
|
+
GATE_FAILED,
|
|
770
|
+
'runs directory is not a real child of the plan authority directory',
|
|
771
|
+
{ runsDir, runsDirReal, authorityDirReal },
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
} catch (err) {
|
|
775
|
+
if (err instanceof ReleaseError) throw err;
|
|
776
|
+
// runs directory doesn't exist — this is okay for optional distribution
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const candidates = [];
|
|
781
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
782
|
+
for (const entry of entries) {
|
|
783
|
+
if (!entry.name.startsWith('distribute-')) continue;
|
|
784
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
785
|
+
throw new ReleaseError(
|
|
786
|
+
GATE_FAILED,
|
|
787
|
+
'distribute-* candidate is a symbolic link or not a directory; authority identity compromised',
|
|
788
|
+
{ entry: entry.name, runsDir },
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
const candidateDir = resolve(runsDir, entry.name);
|
|
792
|
+
const candidateStat = await lstat(candidateDir).catch(() => null);
|
|
793
|
+
if (!candidateStat || candidateStat.isSymbolicLink()) {
|
|
794
|
+
throw new ReleaseError(
|
|
795
|
+
GATE_FAILED,
|
|
796
|
+
'distribute-* candidate is a symbolic link; authority identity compromised',
|
|
797
|
+
{ candidateDir, runsDir },
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const candidateReal = realpathSync(candidateDir);
|
|
801
|
+
if (!candidateReal.startsWith(runsDir + '/') && candidateReal !== runsDir) {
|
|
802
|
+
throw new ReleaseError(
|
|
803
|
+
GATE_FAILED,
|
|
804
|
+
'distribute-* candidate real path is not contained in authority runs directory',
|
|
805
|
+
{ candidateDir, candidateReal, runsDir },
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
const candidatePath = resolve(candidateDir, 'release-run.json');
|
|
809
|
+
try {
|
|
810
|
+
const candidate = await loadRun(candidatePath, { requireDigest: true });
|
|
811
|
+
if (candidate.command !== 'distribute') continue;
|
|
812
|
+
if (!candidate.planDigest) continue;
|
|
813
|
+
if (candidate.planDigest !== plan.digest) continue;
|
|
814
|
+
candidates.push(candidate);
|
|
815
|
+
} catch {
|
|
816
|
+
// Skip invalid candidates silently
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return candidates;
|
|
822
|
+
}
|
|
823
|
+
|
|
728
824
|
// ---------------------------------------------------------------------------
|
|
729
825
|
// Public API
|
|
730
826
|
// ---------------------------------------------------------------------------
|
|
@@ -758,6 +854,8 @@ export async function verifyRelease(options) {
|
|
|
758
854
|
verificationGatesAuthorized: _verificationGatesAuthorized,
|
|
759
855
|
gateEnv,
|
|
760
856
|
previousVerifyRun,
|
|
857
|
+
execFn,
|
|
858
|
+
configPath: configPathOpt,
|
|
761
859
|
} = options ?? {};
|
|
762
860
|
|
|
763
861
|
const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
|
|
@@ -866,6 +964,32 @@ export async function verifyRelease(options) {
|
|
|
866
964
|
);
|
|
867
965
|
}
|
|
868
966
|
|
|
967
|
+
// =======================================================================
|
|
968
|
+
// Step 2b: Check for postPublish distribution requirement
|
|
969
|
+
// =======================================================================
|
|
970
|
+
if (plan.postPublish && plan.postPublish.targets && plan.postPublish.targets.length > 0) {
|
|
971
|
+
await evidence.append({ phase: 'verify', step: 'distribute-run-discovery', status: 'started' });
|
|
972
|
+
|
|
973
|
+
const distributeCandidates = await discoverDistributeRuns({ planPath, plan });
|
|
974
|
+
const distRunPath = (distributeCandidates ?? []).find((c) => c.status === 'DISTRIBUTED')?.runPath || null;
|
|
975
|
+
|
|
976
|
+
await evidence.append({
|
|
977
|
+
phase: 'verify',
|
|
978
|
+
step: 'distribute-run-discovery',
|
|
979
|
+
status: 'checked',
|
|
980
|
+
foundCandidate: !!distRunPath,
|
|
981
|
+
distributeRunPath: distRunPath,
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
if (!distRunPath) {
|
|
985
|
+
throw new ReleaseError(
|
|
986
|
+
GATE_FAILED,
|
|
987
|
+
`distribution required by plan.postPublish but no DISTRIBUTED run found; run release-skill distribute --plan ${planPath} --root ${root}`,
|
|
988
|
+
{ requiredDistribution: true, evidenceEvent: 'distribute-run-missing' },
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
869
993
|
if (plan.production) {
|
|
870
994
|
if (!sourceRun.approvalPath || !sourceRun.approvalDigest) {
|
|
871
995
|
throw new ReleaseError(
|
|
@@ -1678,6 +1802,57 @@ export async function verifyRelease(options) {
|
|
|
1678
1802
|
};
|
|
1679
1803
|
const persistedVerifyRun = await writeRunAtomic(verifyRunPath, verifyRunState);
|
|
1680
1804
|
|
|
1805
|
+
// =======================================================================
|
|
1806
|
+
// Baseline advance: move per-unit previousPublicBaseline to the commit
|
|
1807
|
+
// that was just verified. Local bookkeeping only — no remote writes.
|
|
1808
|
+
// Best-effort: a failure here must never demote VERIFIED, and the config
|
|
1809
|
+
// is only auto-committed when the file was clean before the write, so a
|
|
1810
|
+
// dirty worktree leaves the decision to the human.
|
|
1811
|
+
// =======================================================================
|
|
1812
|
+
let baselineAdvance = null;
|
|
1813
|
+
const baselineAdvances = deriveBaselineAdvances(plan);
|
|
1814
|
+
if (baselineAdvances.length > 0) {
|
|
1815
|
+
try {
|
|
1816
|
+
const configAbs = configPathOpt
|
|
1817
|
+
? (isAbsolute(configPathOpt) ? configPathOpt : join(root, configPathOpt))
|
|
1818
|
+
: join(root, '.release-skill/project.yaml');
|
|
1819
|
+
const cleanBefore = await isWorktreeFileClean({ root, filePath: configAbs, execFn });
|
|
1820
|
+
const applyResult = await applyBaselineAdvances({
|
|
1821
|
+
configPath: configAbs,
|
|
1822
|
+
advances: baselineAdvances,
|
|
1823
|
+
validateFn: async () => { await loadProjectConfig({ root, configPath: configAbs }); },
|
|
1824
|
+
});
|
|
1825
|
+
baselineAdvance = { ...applyResult, committed: false };
|
|
1826
|
+
if (applyResult.changed && cleanBefore) {
|
|
1827
|
+
const versionLabel = (plan.units ?? [])
|
|
1828
|
+
.map((u) => `${u.id} v${u.targetVersion}`)
|
|
1829
|
+
.join(', ');
|
|
1830
|
+
await commitBaselineAdvance({
|
|
1831
|
+
root,
|
|
1832
|
+
filePath: configAbs,
|
|
1833
|
+
message: `chore: advance previousPublicBaseline to published ${versionLabel}`,
|
|
1834
|
+
execFn,
|
|
1835
|
+
});
|
|
1836
|
+
baselineAdvance.committed = true;
|
|
1837
|
+
}
|
|
1838
|
+
await evidence.append({
|
|
1839
|
+
phase: 'verify',
|
|
1840
|
+
step: 'baseline-advance',
|
|
1841
|
+
status: applyResult.changed ? 'advanced' : 'already-current',
|
|
1842
|
+
updatedUnits: applyResult.updatedUnits,
|
|
1843
|
+
committed: baselineAdvance.committed,
|
|
1844
|
+
});
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
baselineAdvance = { failed: true, error: err.message };
|
|
1847
|
+
await evidence.append({
|
|
1848
|
+
phase: 'verify',
|
|
1849
|
+
step: 'baseline-advance',
|
|
1850
|
+
status: 'failed',
|
|
1851
|
+
error: { code: err.code, message: err.message },
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1681
1856
|
await evidence.finish({
|
|
1682
1857
|
status: VERIFIED,
|
|
1683
1858
|
planPath,
|
|
@@ -1697,6 +1872,7 @@ export async function verifyRelease(options) {
|
|
|
1697
1872
|
smokeTest,
|
|
1698
1873
|
gateResults: consumerGateResults,
|
|
1699
1874
|
...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
|
|
1875
|
+
...(baselineAdvance ? { baselineAdvance } : {}),
|
|
1700
1876
|
};
|
|
1701
1877
|
} catch (err) {
|
|
1702
1878
|
await evidence.append({
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline advance: after a release reaches VERIFIED, the project config's
|
|
3
|
+
* per-unit `previousPublicBaseline` must be advanced from the previous
|
|
4
|
+
* public commit to the commit that was just published. Doing this by hand
|
|
5
|
+
* is the most error-prone step in the release loop, so it is derived from
|
|
6
|
+
* the frozen plan (never recomputed from workspace state) and written back
|
|
7
|
+
* into `.release-skill/project.yaml` with comment preservation.
|
|
8
|
+
*
|
|
9
|
+
* Fail-safe by construction: any unit whose plan data is missing or
|
|
10
|
+
* malformed is skipped rather than guessed.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
import YAML from 'yaml';
|
|
16
|
+
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFileCb);
|
|
19
|
+
|
|
20
|
+
function defaultExec(command, args, options = {}) {
|
|
21
|
+
return execFileAsync(command, args, { shell: false, encoding: 'utf8', timeout: 120_000, ...options });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SHA_RE = /^[a-f0-9]{40}$/;
|
|
25
|
+
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Derive the baseline advances that a VERIFIED plan implies.
|
|
29
|
+
*
|
|
30
|
+
* For each unit with a `mode: bound` previousPublicBaseline, the published
|
|
31
|
+
* commit/tree/manifestDigest come from the unit's frozen push-snapshot
|
|
32
|
+
* action — the same values that were pushed to the public repo.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ units?: Array<Record<string, unknown>>, externalActions?: Array<Record<string, unknown>> }} plan
|
|
35
|
+
* The frozen release plan.
|
|
36
|
+
* @returns {Array<{ unitId: string, repo: string, previousCommit: string, commit: string, tree: string, manifestDigest: string }>}
|
|
37
|
+
* One advance per unit that is behind its published commit. Units already
|
|
38
|
+
* at the published commit (idempotent re-runs) produce no advance.
|
|
39
|
+
*/
|
|
40
|
+
export function deriveBaselineAdvances(plan) {
|
|
41
|
+
const advances = [];
|
|
42
|
+
for (const unit of plan?.units ?? []) {
|
|
43
|
+
const baseline = unit?.previousPublicBaseline;
|
|
44
|
+
if (!baseline || baseline.mode !== 'bound') continue;
|
|
45
|
+
|
|
46
|
+
const action = (plan?.externalActions ?? []).find((a) => (
|
|
47
|
+
a?.type === 'push-snapshot'
|
|
48
|
+
&& a?.parameters?.publicRepo === unit.publicRepo
|
|
49
|
+
));
|
|
50
|
+
const { commit, tree, manifestDigest } = action?.parameters ?? {};
|
|
51
|
+
|
|
52
|
+
if (!SHA_RE.test(commit ?? '') || !SHA_RE.test(tree ?? '') || !DIGEST_RE.test(manifestDigest ?? '')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (baseline.commit === commit) continue;
|
|
56
|
+
|
|
57
|
+
advances.push({
|
|
58
|
+
unitId: unit.id,
|
|
59
|
+
repo: baseline.repo,
|
|
60
|
+
previousCommit: baseline.commit,
|
|
61
|
+
commit,
|
|
62
|
+
tree,
|
|
63
|
+
manifestDigest,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return advances;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply derived advances to the project config file, preserving comments and
|
|
71
|
+
* unrelated content by editing the YAML AST in place. When a `validateFn` is
|
|
72
|
+
* provided, the rewritten file is re-validated and the original content is
|
|
73
|
+
* restored if validation fails (the advance must never corrupt the config).
|
|
74
|
+
*
|
|
75
|
+
* @param {{ configPath: string, advances: Array<Record<string, unknown>>, validateFn?: (configPath: string) => Promise<void> }} opts
|
|
76
|
+
* @returns {Promise<{ changed: boolean, updatedUnits: string[] }>}
|
|
77
|
+
*/
|
|
78
|
+
export async function applyBaselineAdvances({ configPath, advances, validateFn }) {
|
|
79
|
+
if (!advances || advances.length === 0) {
|
|
80
|
+
return { changed: false, updatedUnits: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = await readFile(configPath, 'utf8');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new ReleaseError(
|
|
88
|
+
GATE_FAILED,
|
|
89
|
+
`baseline advance cannot read project config: ${err.message}`,
|
|
90
|
+
{ configPath, cause: err.code },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const doc = YAML.parseDocument(content);
|
|
95
|
+
const releaseUnits = doc.get('releaseUnits', true);
|
|
96
|
+
if (!YAML.isSeq(releaseUnits)) {
|
|
97
|
+
throw new ReleaseError(
|
|
98
|
+
GATE_FAILED,
|
|
99
|
+
'baseline advance requires a releaseUnits sequence in the project config',
|
|
100
|
+
{ configPath },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const updatedUnits = [];
|
|
105
|
+
for (const advance of advances) {
|
|
106
|
+
const unitNode = releaseUnits.items.find((item) => (
|
|
107
|
+
YAML.isMap(item) && item.get('id') === advance.unitId
|
|
108
|
+
));
|
|
109
|
+
if (!unitNode) continue;
|
|
110
|
+
|
|
111
|
+
let baselineNode = unitNode.get('previousPublicBaseline', true);
|
|
112
|
+
if (!YAML.isMap(baselineNode)) {
|
|
113
|
+
baselineNode = new YAML.YAMLMap();
|
|
114
|
+
unitNode.set('previousPublicBaseline', baselineNode);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let changed = false;
|
|
118
|
+
const setField = (key, value) => {
|
|
119
|
+
if (baselineNode.get(key) !== value) {
|
|
120
|
+
baselineNode.set(key, value);
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
setField('commit', advance.commit);
|
|
125
|
+
setField('tree', advance.tree);
|
|
126
|
+
setField('manifestDigest', advance.manifestDigest);
|
|
127
|
+
|
|
128
|
+
if (changed) updatedUnits.push(advance.unitId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (updatedUnits.length === 0) {
|
|
132
|
+
return { changed: false, updatedUnits: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const next = doc.toString();
|
|
136
|
+
await writeFile(configPath, next, 'utf8');
|
|
137
|
+
|
|
138
|
+
if (typeof validateFn === 'function') {
|
|
139
|
+
try {
|
|
140
|
+
await validateFn(configPath);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// Restore the original content so a bad advance never corrupts config.
|
|
143
|
+
await writeFile(configPath, content, 'utf8');
|
|
144
|
+
throw new ReleaseError(
|
|
145
|
+
GATE_FAILED,
|
|
146
|
+
`baseline advance reverted: rewritten config failed validation: ${err.message}`,
|
|
147
|
+
{ configPath, cause: err.message },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { changed: true, updatedUnits };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Report whether a single file is clean in the git worktree (no staged or
|
|
157
|
+
* unstaged changes). Fails safe to `false` on any git error so that a
|
|
158
|
+
* non-repository or a broken git never authorizes an automatic commit.
|
|
159
|
+
*
|
|
160
|
+
* @param {{ root: string, filePath: string, execFn?: Function }} opts
|
|
161
|
+
* @returns {Promise<boolean>}
|
|
162
|
+
*/
|
|
163
|
+
export async function isWorktreeFileClean({ root, filePath, execFn }) {
|
|
164
|
+
const exec = typeof execFn === 'function' ? execFn : defaultExec;
|
|
165
|
+
try {
|
|
166
|
+
const { stdout } = await exec('git', ['status', '--porcelain', '--', filePath], { cwd: root });
|
|
167
|
+
return stdout.trim() === '';
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Stage and commit only the config file. The caller is responsible for
|
|
175
|
+
* checking `isWorktreeFileClean` *before* writing so that this commit never
|
|
176
|
+
* sweeps in unrelated in-flight work.
|
|
177
|
+
*
|
|
178
|
+
* @param {{ root: string, filePath: string, message: string, execFn?: Function }} opts
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
export async function commitBaselineAdvance({ root, filePath, message, execFn }) {
|
|
182
|
+
const exec = typeof execFn === 'function' ? execFn : defaultExec;
|
|
183
|
+
await exec('git', ['add', '--', filePath], { cwd: root });
|
|
184
|
+
await exec('git', ['commit', '-m', message, '--', filePath], { cwd: root });
|
|
185
|
+
}
|
package/src/core/baseline.mjs
CHANGED
|
@@ -146,7 +146,17 @@ function parseStatusPorcelainZ(statusOut) {
|
|
|
146
146
|
* @returns {Promise<string>} Hex-encoded SHA-256 digest.
|
|
147
147
|
*/
|
|
148
148
|
async function computeWorkspaceDigest(root) {
|
|
149
|
-
|
|
149
|
+
// 64 MiB upper bound (official fix 39c631f): `git ls-files -s -z` emits one
|
|
150
|
+
// line per tracked file and routinely exceeds Node's default 1 MiB
|
|
151
|
+
// execFile maxBuffer on large repositories (measured 8494 tracked files →
|
|
152
|
+
// 1,519,151 B stdout). Large enough for any realistic baseline, small
|
|
153
|
+
// enough to fail closed before exhausting memory.
|
|
154
|
+
const opts = {
|
|
155
|
+
cwd: root,
|
|
156
|
+
shell: false,
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
159
|
+
};
|
|
150
160
|
|
|
151
161
|
const [
|
|
152
162
|
{ stdout: stagedOut },
|
package/src/core/checkpoints.mjs
CHANGED
|
@@ -189,3 +189,28 @@ export function isRemoteWriteAction(actionType) {
|
|
|
189
189
|
export function isMarketplaceAction(actionType) {
|
|
190
190
|
return MARKETPLACE_ACTION_TYPES.has(actionType);
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Post-publish distribution (distribute saga) checkpoint action types.
|
|
195
|
+
*
|
|
196
|
+
* These checkpoints are recorded in distribute run records; they never
|
|
197
|
+
* appear in plan externalActions and intentionally stay OUT of
|
|
198
|
+
* CHECKPOINT_ORDER / TIER_TABLE: the distribute saga schedules targets
|
|
199
|
+
* strictly sequentially in dependsOn order (shared payload + sha backfill),
|
|
200
|
+
* never through the publish/reconcile tier scheduler. If a distribute type
|
|
201
|
+
* ever leaked into a publish plan, groupActionsByTier would fail closed on
|
|
202
|
+
* it as an unknown type — exactly the desired behaviour.
|
|
203
|
+
*/
|
|
204
|
+
export const DISTRIBUTE_ACTION_TYPES = new Set([
|
|
205
|
+
'distribute-probe',
|
|
206
|
+
'distribute-mirror',
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 判断动作类型是否为发布后分发(distribute)动作。
|
|
211
|
+
* @param {string} actionType - 运行检查点中的动作类型
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
export function isDistributeAction(actionType) {
|
|
215
|
+
return DISTRIBUTE_ACTION_TYPES.has(actionType);
|
|
216
|
+
}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* 本桥从 5 级相对路径 import(包外工作树引用)切换为包名 import
|
|
14
14
|
* ('skill-family-harness-node'),随 npm 依赖发布,不再依赖 Foundation 工作树在旁。
|
|
15
15
|
*
|
|
16
|
+
* 0.5.1 依赖提升:skill-family-contracts / skill-family-harness-node 由 0.4.0
|
|
17
|
+
* 提升至 0.5.0(npm latest,2026-08-16 发布)。harness-node 0.5.0 导出面已逐项
|
|
18
|
+
* 核对:publishFileExclusive(atomic.mjs)、acquireFilesystemLock /
|
|
19
|
+
* inspectFilesystemLock / releaseFilesystemLock / recoverFilesystemLock
|
|
20
|
+
* (token-lock.mjs)、HARNESS_ERROR_KINDS(errors.mjs)均在 index.mjs 导出面,
|
|
21
|
+
* 包名 import 无需改动。
|
|
22
|
+
*
|
|
16
23
|
* 发布形态:包名 import 可直接用于包内 src(node_modules 解析),bundle
|
|
17
24
|
* (bin/release-skill.bundle.mjs,esbuild 内联)保持自包含。
|
|
18
25
|
*
|
package/src/core/plan.mjs
CHANGED
|
@@ -534,8 +534,18 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
534
534
|
if (!frozen.parentCommit || frozen.parentCommit !== unit.previousPublicBaseline?.commit) {
|
|
535
535
|
failures.push(`unit "${unitId}" frozenSnapshot.parentCommit does not match previous public baseline commit`);
|
|
536
536
|
}
|
|
537
|
-
} else if (
|
|
538
|
-
|
|
537
|
+
} else if (branchStrategy === 'create-release-branch') {
|
|
538
|
+
// Chain-integrity gate (0.5.1): create-release-branch creates an
|
|
539
|
+
// orphan root commit (no parent). It is only legitimate for the
|
|
540
|
+
// FIRST release of a repository (previousPublicBaseline.mode=none).
|
|
541
|
+
// A later release using it would start a fresh lineage disconnected
|
|
542
|
+
// from the published history — the historical orphan-root channel.
|
|
543
|
+
if (unit.previousPublicBaseline?.mode === 'bound') {
|
|
544
|
+
failures.push(`unit "${unitId}" create-release-branch is only valid for first releases (previousPublicBaseline.mode=none), but a bound previous public baseline exists`);
|
|
545
|
+
}
|
|
546
|
+
if (frozen.parentCommit) {
|
|
547
|
+
failures.push(`unit "${unitId}" create-release-branch must not freeze a parentCommit`);
|
|
548
|
+
}
|
|
539
549
|
}
|
|
540
550
|
if (
|
|
541
551
|
branchStrategy === 'advance-existing-branch' &&
|