release-skill 0.9.33 → 0.9.34
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/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +31 -13
- package/README.zh-CN.md +17 -13
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/claude/schemas/.render-manifest.json +8 -4
- package/adapters/claude/schemas/release-plan.schema.json +85 -0
- package/adapters/claude/schemas/release-project.schema.json +18 -0
- package/adapters/claude/schemas/test-intake.schema.json +217 -0
- package/adapters/claude/skills/release-help/SKILL.md +4 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +9 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/codex/schemas/.render-manifest.json +8 -4
- package/adapters/codex/schemas/release-plan.schema.json +85 -0
- package/adapters/codex/schemas/release-project.schema.json +18 -0
- package/adapters/codex/schemas/test-intake.schema.json +217 -0
- package/adapters/codex/skills/release-help/SKILL.md +4 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +9 -2
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/cursor/schemas/.render-manifest.json +8 -4
- package/adapters/cursor/schemas/release-plan.schema.json +85 -0
- package/adapters/cursor/schemas/release-project.schema.json +18 -0
- package/adapters/cursor/schemas/test-intake.schema.json +217 -0
- package/adapters/cursor/skills/release-help/SKILL.md +4 -1
- package/adapters/cursor/skills/release-prepare/SKILL.md +9 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/kimi/schemas/.render-manifest.json +8 -4
- package/adapters/kimi/schemas/release-plan.schema.json +85 -0
- package/adapters/kimi/schemas/release-project.schema.json +18 -0
- package/adapters/kimi/schemas/test-intake.schema.json +217 -0
- package/adapters/kimi/skills/release-help/SKILL.md +4 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +9 -2
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/qoder/schemas/.render-manifest.json +8 -4
- package/adapters/qoder/schemas/release-plan.schema.json +85 -0
- package/adapters/qoder/schemas/release-project.schema.json +18 -0
- package/adapters/qoder/schemas/test-intake.schema.json +217 -0
- package/adapters/qoder/skills/release-help/SKILL.md +4 -1
- package/adapters/qoder/skills/release-prepare/SKILL.md +9 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +5847 -4041
- package/adapters/workbuddy/schemas/.render-manifest.json +8 -4
- package/adapters/workbuddy/schemas/release-plan.schema.json +85 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +18 -0
- package/adapters/workbuddy/schemas/test-intake.schema.json +217 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +4 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +9 -2
- package/bin/release-skill-cli.mjs +37 -2
- package/bin/release-skill.bundle.mjs +5847 -4041
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +6 -6
- package/references/01-state-machine.md +12 -1
- package/references/02-project-config.md +20 -0
- package/references/05-evidence-and-errors.md +30 -0
- package/schemas/.render-manifest.json +8 -4
- package/schemas/release-plan.schema.json +85 -0
- package/schemas/release-project.schema.json +18 -0
- package/schemas/test-intake.schema.json +217 -0
- package/skills/release-help/SKILL.md +4 -1
- package/skills/release-prepare/SKILL.md +9 -2
- package/skills-src/release-help/SKILL.md +4 -1
- package/skills-src/release-prepare/SKILL.md +9 -2
- package/src/commands/approve.mjs +13 -0
- package/src/commands/prepare.mjs +501 -5
- package/src/commands/publish.mjs +2 -0
- package/src/commands/reconcile.mjs +2 -0
- package/src/commands/route.mjs +32 -7
- package/src/commands/ship.mjs +168 -5
- package/src/core/config.mjs +5 -0
- package/src/core/plan.mjs +3 -1
- package/src/core/test-candidate.mjs +649 -0
- package/src/core/test-intake.mjs +645 -0
|
@@ -27,6 +27,7 @@ import { readFile, mkdir } from 'node:fs/promises';
|
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
|
|
29
29
|
import { assertImmutablePlanAuthority, computePlanDigest, validatePlan, validatePlanActionCompleteness } from '../core/plan.mjs';
|
|
30
|
+
import { verifyTestAcceptance } from '../core/test-intake.mjs';
|
|
30
31
|
import {
|
|
31
32
|
assertImmutableApprovalAuthority,
|
|
32
33
|
computeApprovalDigest,
|
|
@@ -148,6 +149,7 @@ export async function reconcileRelease(options) {
|
|
|
148
149
|
}
|
|
149
150
|
validatePlan(plan);
|
|
150
151
|
assertImmutablePlanAuthority(planPath, plan);
|
|
152
|
+
await verifyTestAcceptance({ root, plan });
|
|
151
153
|
const isProductionPlan = plan.production?.mode === 'github-npm-v1';
|
|
152
154
|
|
|
153
155
|
// --- Set up directories ---
|
package/src/commands/route.mjs
CHANGED
|
@@ -354,7 +354,7 @@ export async function classifyBaselineSurface(root, prevCommit) {
|
|
|
354
354
|
* read. Invalid authority is diagnostic, never permission for a new release.
|
|
355
355
|
*
|
|
356
356
|
* @param {string} root - project root directory
|
|
357
|
-
* @returns {Promise<{hasPartialRun: boolean, recoveryActionCode: string|null, runs: object[]}>}
|
|
357
|
+
* @returns {Promise<{hasPartialRun: boolean, recoveryActionCode: string|null, runs: object[], observedRuns: object[]}>}
|
|
358
358
|
*/
|
|
359
359
|
export async function readRunRouting(root, options = {}) {
|
|
360
360
|
const cwd = resolve(root);
|
|
@@ -364,7 +364,9 @@ export async function readRunRouting(root, options = {}) {
|
|
|
364
364
|
try {
|
|
365
365
|
runDirs = await readdir(runsDir);
|
|
366
366
|
} catch (error) {
|
|
367
|
-
if (error.code === 'ENOENT')
|
|
367
|
+
if (error.code === 'ENOENT') {
|
|
368
|
+
return { hasPartialRun: false, recoveryActionCode: null, runs: [], observedRuns: [] };
|
|
369
|
+
}
|
|
368
370
|
const relativePath = relative(cwd, runsDir);
|
|
369
371
|
const diagnostic = {
|
|
370
372
|
code: error.code ?? 'RUNS_DIRECTORY_UNREADABLE',
|
|
@@ -383,6 +385,7 @@ export async function readRunRouting(root, options = {}) {
|
|
|
383
385
|
hasPartialRun: false,
|
|
384
386
|
recoveryActionCode: null,
|
|
385
387
|
runs: [],
|
|
388
|
+
observedRuns: [],
|
|
386
389
|
diagnostics: [{
|
|
387
390
|
path: relativePath,
|
|
388
391
|
runPath: runsDir,
|
|
@@ -455,6 +458,22 @@ export async function readRunRouting(root, options = {}) {
|
|
|
455
458
|
}
|
|
456
459
|
records.push(record);
|
|
457
460
|
}
|
|
461
|
+
// Runs with a null recovery code, and runs seen without a target version,
|
|
462
|
+
// stay out of `runs`. Callers that need the recorded status still see them.
|
|
463
|
+
// Recovery selection below is unchanged.
|
|
464
|
+
const observedRuns = records.flatMap((record) => {
|
|
465
|
+
const run = record.run;
|
|
466
|
+
if (!run?.planPath || !run.status) return [];
|
|
467
|
+
return [{
|
|
468
|
+
runPath: record.runPath ?? null,
|
|
469
|
+
planPath: run.planPath,
|
|
470
|
+
command: run.command ?? null,
|
|
471
|
+
status: run.status,
|
|
472
|
+
sourceRunPath: run.sourceRunPath ?? null,
|
|
473
|
+
...(run.approvalPath ? { approvalPath: run.approvalPath } : {}),
|
|
474
|
+
recoveryActionCode: record.recoveryActionCode ?? null,
|
|
475
|
+
}];
|
|
476
|
+
}).sort((a, b) => String(a.runPath).localeCompare(String(b.runPath)));
|
|
458
477
|
const targetScopedRecords = [];
|
|
459
478
|
const historicalDiagnostics = [];
|
|
460
479
|
for (const record of records) {
|
|
@@ -536,6 +555,7 @@ export async function readRunRouting(root, options = {}) {
|
|
|
536
555
|
hasPartialRun: unresolved.some((r) => r.run?.status === 'PARTIAL'),
|
|
537
556
|
recoveryActionCode,
|
|
538
557
|
runs,
|
|
558
|
+
observedRuns,
|
|
539
559
|
...(unifiedDiagnostics.length > 0 ? { diagnostics: unifiedDiagnostics } : {}),
|
|
540
560
|
};
|
|
541
561
|
}
|
|
@@ -872,14 +892,15 @@ async function isProvenPreAuthorityFailure(runDir, runName) {
|
|
|
872
892
|
|
|
873
893
|
/**
|
|
874
894
|
* Postverify is a downstream run type that predates the generic recovery
|
|
875
|
-
* reader.
|
|
876
|
-
* with
|
|
895
|
+
* reader. Its completed and open records are consumable only when the summary
|
|
896
|
+
* agrees with the sealed status and the verify parent is fully lineage-valid.
|
|
877
897
|
*/
|
|
878
898
|
async function readLegacyPostverifyRecovery(runPath) {
|
|
879
899
|
try {
|
|
880
900
|
const run = await loadRun(runPath, { requireDigest: true });
|
|
881
|
-
|
|
882
|
-
|
|
901
|
+
const acceptedStatuses = new Set(['DISTRIBUTED', 'PARTIAL', 'BLOCKED', 'NEEDS_INPUT']);
|
|
902
|
+
if (run.command !== 'postverify' || !acceptedStatuses.has(run.status)) {
|
|
903
|
+
throw new ReleaseError(GATE_FAILED, 'postverify status is not a recognized completed or open state');
|
|
883
904
|
}
|
|
884
905
|
const planRaw = await readFile(run.planPath, 'utf8');
|
|
885
906
|
const plan = JSON.parse(planRaw);
|
|
@@ -932,7 +953,8 @@ async function readLegacyPostverifyRecovery(runPath) {
|
|
|
932
953
|
}))
|
|
933
954
|
));
|
|
934
955
|
validateRunCheckpointMapping(run, expectedPostVerifyActions);
|
|
935
|
-
if (run.
|
|
956
|
+
if (run.status === 'DISTRIBUTED'
|
|
957
|
+
&& run.checkpoints.some((checkpoint) => !['succeeded', 'skipped', 'NO_CHANGE'].includes(checkpoint.status))) {
|
|
936
958
|
throw new ReleaseError(GATE_FAILED, 'legacy postverify DISTRIBUTED run has incomplete checkpoints');
|
|
937
959
|
}
|
|
938
960
|
return {
|
|
@@ -940,6 +962,9 @@ async function readLegacyPostverifyRecovery(runPath) {
|
|
|
940
962
|
run,
|
|
941
963
|
plan,
|
|
942
964
|
lineage: [{ run: parent, runPath: resolve(run.sourceRunPath) }],
|
|
965
|
+
// Postverify continuation is owned by the existing ship entry point.
|
|
966
|
+
// Route exposes its validated status but does not turn it into a second
|
|
967
|
+
// release recovery action.
|
|
943
968
|
recoveryActionCode: null,
|
|
944
969
|
legacyPostverify: true,
|
|
945
970
|
};
|
package/src/commands/ship.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, lstat, mkdir } from 'node:fs/promises';
|
|
1
|
+
import { readFile, lstat, mkdir, realpath } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, resolve } from 'node:path';
|
|
3
3
|
import { publishFileOrReplace } from 'skill-family-harness-node';
|
|
4
4
|
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
PLAN_DIGEST_MISMATCH,
|
|
22
22
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
23
23
|
} from '../core/errors.mjs';
|
|
24
|
+
import {
|
|
25
|
+
applyShipPrepareOutcome,
|
|
26
|
+
resolveShipCandidateContinuation,
|
|
27
|
+
} from '../core/test-candidate.mjs';
|
|
24
28
|
|
|
25
29
|
function normalizeNewShipUnitIds(releaseUnits, requestedUnitIds) {
|
|
26
30
|
if (requestedUnitIds === undefined) return undefined;
|
|
@@ -102,6 +106,66 @@ async function readFrozenPlanForUnitScope(planPath, statePath, stateUnitIds) {
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
async function sameAuthorityFile(left, right) {
|
|
110
|
+
const identity = async (value) => {
|
|
111
|
+
const absolute = resolve(value);
|
|
112
|
+
try {
|
|
113
|
+
return await realpath(absolute);
|
|
114
|
+
} catch {
|
|
115
|
+
return absolute;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return (await identity(left)) === (await identity(right));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A VERIFIED reattach keeps the release fact. An observed postverify is
|
|
123
|
+
* carried with its verify parent so open work can re-enter postVerifyRelease
|
|
124
|
+
* and completed work can feed the existing local finish checklist.
|
|
125
|
+
*/
|
|
126
|
+
async function observedPostverifyContinuation(root, planPath) {
|
|
127
|
+
if (!planPath) return null;
|
|
128
|
+
let routing;
|
|
129
|
+
try {
|
|
130
|
+
const { readRunRouting } = await import('./route.mjs');
|
|
131
|
+
routing = await readRunRouting(root, {});
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const formal = resolve(planPath);
|
|
136
|
+
const bound = [];
|
|
137
|
+
for (const run of routing?.observedRuns ?? []) {
|
|
138
|
+
if (!run?.planPath || !run.status || !run.runPath || !run.command) continue;
|
|
139
|
+
if (await sameAuthorityFile(run.planPath, formal)) bound.push(run);
|
|
140
|
+
}
|
|
141
|
+
const postverifyRuns = bound.filter((run) => run.command === 'postverify');
|
|
142
|
+
const completed = postverifyRuns.filter((run) => run.status === 'DISTRIBUTED');
|
|
143
|
+
const open = postverifyRuns.filter((run) => run.status !== 'DISTRIBUTED');
|
|
144
|
+
// A completed authority supersedes open attempts for the same verify
|
|
145
|
+
// parent. Multiple completed records are ambiguous; never fall back to an
|
|
146
|
+
// older open attempt or choose by directory order or timestamp.
|
|
147
|
+
if (completed.length > 1) return null;
|
|
148
|
+
const pending = completed.length === 1 ? completed[0] : open.length === 1 ? open[0] : null;
|
|
149
|
+
if (!pending) return null;
|
|
150
|
+
let verify = null;
|
|
151
|
+
for (const run of bound) {
|
|
152
|
+
if (run.command !== 'verify' || run.status !== 'VERIFIED' || !run.runPath) continue;
|
|
153
|
+
if (pending.sourceRunPath && await sameAuthorityFile(run.runPath, pending.sourceRunPath)) {
|
|
154
|
+
verify = run;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!verify) return null;
|
|
159
|
+
return {
|
|
160
|
+
verifyRunPath: verify.runPath,
|
|
161
|
+
...(verify.approvalPath ? { approvalPath: verify.approvalPath } : {}),
|
|
162
|
+
postVerify: {
|
|
163
|
+
status: pending.status,
|
|
164
|
+
runPath: pending.runPath,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
105
169
|
async function writeJsonAtomic(path, value) {
|
|
106
170
|
await mkdir(dirname(path), { recursive: true });
|
|
107
171
|
const { stateDigest: _oldDigest, ...body } = value;
|
|
@@ -177,9 +241,10 @@ async function defaultDependencies() {
|
|
|
177
241
|
|
|
178
242
|
function publicState(state, { postRelease } = {}) {
|
|
179
243
|
const visiblePostRelease = postRelease === undefined ? state.postRelease : postRelease;
|
|
244
|
+
const showsTestCandidate = state.status === 'NEW' && state.testCandidate;
|
|
180
245
|
return {
|
|
181
246
|
command: 'ship',
|
|
182
|
-
status: state.status,
|
|
247
|
+
status: showsTestCandidate ? 'NEEDS_INPUT' : state.status,
|
|
183
248
|
statePath: state.statePath,
|
|
184
249
|
targetVersion: state.targetVersion,
|
|
185
250
|
...(state.hooks && state.hooks.length > 0 ? {
|
|
@@ -199,6 +264,10 @@ function publicState(state, { postRelease } = {}) {
|
|
|
199
264
|
...(state.distributeRunPath ? { distributeRunPath: state.distributeRunPath } : {}),
|
|
200
265
|
...(state.postVerify ? { postVerify: state.postVerify } : {}),
|
|
201
266
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
267
|
+
...(showsTestCandidate ? { testCandidate: state.testCandidate } : {}),
|
|
268
|
+
...(showsTestCandidate && state.pendingTestRequirements
|
|
269
|
+
? { pendingTestRequirements: state.pendingTestRequirements }
|
|
270
|
+
: {}),
|
|
202
271
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
203
272
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
204
273
|
...(visiblePostRelease ? { postRelease: visiblePostRelease } : {}),
|
|
@@ -483,7 +552,77 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
483
552
|
});
|
|
484
553
|
}
|
|
485
554
|
|
|
486
|
-
if (state.status === 'NEW') {
|
|
555
|
+
if (state.status === 'NEW' && state.testCandidate) {
|
|
556
|
+
const loaded = await deps.loadProjectConfig({ root });
|
|
557
|
+
const continuation = resolveShipCandidateContinuation({
|
|
558
|
+
state,
|
|
559
|
+
loadedConfigDigest: loaded.configDigest,
|
|
560
|
+
requestedUnitIds: options.unitIds,
|
|
561
|
+
});
|
|
562
|
+
if (continuation.kind === 'conflict') {
|
|
563
|
+
throw new ReleaseError(GATE_FAILED, `ship test candidate conflict: ${continuation.reason}`, {
|
|
564
|
+
reason: continuation.reason,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const prepared = await deps.prepareRelease({
|
|
568
|
+
root,
|
|
569
|
+
candidatePath: continuation.candidatePath,
|
|
570
|
+
expectedCandidateSha256: continuation.candidateSha256,
|
|
571
|
+
expectedSubjectDigest: continuation.subjectDigest,
|
|
572
|
+
production: true,
|
|
573
|
+
offline: false,
|
|
574
|
+
offlineExplicit: false,
|
|
575
|
+
workflowExplicit: false,
|
|
576
|
+
version: state.targetVersion ?? undefined,
|
|
577
|
+
versionExplicit: state.targetVersion != null,
|
|
578
|
+
hooksAuthorized: true,
|
|
579
|
+
verificationGatesAuthorized: true,
|
|
580
|
+
hookCache: true,
|
|
581
|
+
...(Array.isArray(state.selectedUnitIds) ? { unitIds: state.selectedUnitIds } : {}),
|
|
582
|
+
});
|
|
583
|
+
if (prepared?.status === 'NEEDS_INPUT') {
|
|
584
|
+
state = {
|
|
585
|
+
...applyShipPrepareOutcome(state, {
|
|
586
|
+
...prepared,
|
|
587
|
+
testCandidate: {
|
|
588
|
+
path: prepared.candidatePath ?? state.testCandidate.path,
|
|
589
|
+
sha256: prepared.candidateSha256 ?? state.testCandidate.sha256,
|
|
590
|
+
subjectDigest: prepared.subjectDigest ?? state.testCandidate.subjectDigest,
|
|
591
|
+
evidenceDir: prepared.evidenceDir ?? state.testCandidate.evidenceDir,
|
|
592
|
+
},
|
|
593
|
+
}),
|
|
594
|
+
updatedAt: new Date().toISOString(),
|
|
595
|
+
};
|
|
596
|
+
await writeJsonAtomic(statePath, state);
|
|
597
|
+
return publicState(state);
|
|
598
|
+
}
|
|
599
|
+
const outcome = applyShipPrepareOutcome(state, prepared);
|
|
600
|
+
// A reattached PARTIAL, PUBLISHED, or VERIFIED record stays that status.
|
|
601
|
+
// Returning here avoids a new approval and avoids falling through into
|
|
602
|
+
// reconcile, distribute, or verify in this same call.
|
|
603
|
+
if (outcome.status !== 'NEEDS_PLAN_APPROVAL') {
|
|
604
|
+
const continuation = outcome.status === 'VERIFIED'
|
|
605
|
+
? await observedPostverifyContinuation(root, outcome.planPath)
|
|
606
|
+
: null;
|
|
607
|
+
state = {
|
|
608
|
+
...outcome,
|
|
609
|
+
...(continuation ?? {}),
|
|
610
|
+
updatedAt: new Date().toISOString(),
|
|
611
|
+
};
|
|
612
|
+
await writeJsonAtomic(statePath, state);
|
|
613
|
+
return publicState(state);
|
|
614
|
+
}
|
|
615
|
+
const maxApprovalMs = typeof deps.readMaxApprovalMs === 'function'
|
|
616
|
+
? await deps.readMaxApprovalMs()
|
|
617
|
+
: MAX_APPROVAL_MS;
|
|
618
|
+
const approvalSummary = await buildApprovalSummary(prepared.planPath, maxApprovalMs);
|
|
619
|
+
state = {
|
|
620
|
+
...outcome,
|
|
621
|
+
approvalSummary,
|
|
622
|
+
updatedAt: new Date().toISOString(),
|
|
623
|
+
};
|
|
624
|
+
await writeJsonAtomic(statePath, state);
|
|
625
|
+
} else if (state.status === 'NEW') {
|
|
487
626
|
const loaded = await deps.loadProjectConfig({ root });
|
|
488
627
|
let stateChanged = false;
|
|
489
628
|
if (Array.isArray(state.selectedUnitIds)) {
|
|
@@ -538,6 +677,23 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
538
677
|
hookCache: true,
|
|
539
678
|
...(state.selectedUnitIds ? { unitIds: state.selectedUnitIds } : {}),
|
|
540
679
|
});
|
|
680
|
+
if (prepared?.status === 'NEEDS_INPUT') {
|
|
681
|
+
state = {
|
|
682
|
+
...applyShipPrepareOutcome(state, {
|
|
683
|
+
...prepared,
|
|
684
|
+
testCandidate: {
|
|
685
|
+
path: prepared.candidatePath,
|
|
686
|
+
sha256: prepared.candidateSha256,
|
|
687
|
+
subjectDigest: prepared.subjectDigest,
|
|
688
|
+
evidenceDir: prepared.evidenceDir,
|
|
689
|
+
},
|
|
690
|
+
requirements: prepared.requirements,
|
|
691
|
+
}),
|
|
692
|
+
updatedAt: new Date().toISOString(),
|
|
693
|
+
};
|
|
694
|
+
await writeJsonAtomic(statePath, state);
|
|
695
|
+
return publicState(state);
|
|
696
|
+
}
|
|
541
697
|
let frozenPlan = null;
|
|
542
698
|
if (Array.isArray(state.selectedUnitIds) || deps.preflightGitTransports) {
|
|
543
699
|
frozenPlan = Array.isArray(state.selectedUnitIds)
|
|
@@ -631,6 +787,9 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
631
787
|
}
|
|
632
788
|
|
|
633
789
|
if (state.status === 'PARTIAL') {
|
|
790
|
+
// A reattached terminal record has no source run. Returning keeps that
|
|
791
|
+
// status; a publish that stored sourceRunPath still reconciles.
|
|
792
|
+
if (!state.sourceRunPath) return publicState(state);
|
|
634
793
|
const reconciled = await deps.reconcileRelease({
|
|
635
794
|
planPath: state.planPath,
|
|
636
795
|
sourceRunPath: state.sourceRunPath,
|
|
@@ -653,6 +812,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
653
812
|
let postVerifyRanThisCall = false;
|
|
654
813
|
|
|
655
814
|
if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
|
|
815
|
+
if (!state.sourceRunPath && !state.distributeRunPath) return publicState(state);
|
|
656
816
|
// Step 1: Check if postPublish requires distribution.
|
|
657
817
|
// Only targets and distribute-phase hooks route through distribute;
|
|
658
818
|
// phase:postVerify hooks belong to their independent post-VERIFIED run.
|
|
@@ -699,10 +859,12 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
699
859
|
}
|
|
700
860
|
|
|
701
861
|
// Step 2: Verify (this will check for distributed run if distribution was required)
|
|
862
|
+
const verifySource = needsDistribution ? state.distributeRunPath : state.sourceRunPath;
|
|
863
|
+
if (!verifySource) return publicState(state);
|
|
702
864
|
try {
|
|
703
865
|
const verified = await deps.verifyRelease({
|
|
704
866
|
planPath: state.planPath,
|
|
705
|
-
sourceRunPath:
|
|
867
|
+
sourceRunPath: verifySource,
|
|
706
868
|
adapterRegistry: options.adapterRegistry,
|
|
707
869
|
root,
|
|
708
870
|
verificationGatesAuthorized: state.verificationGatesAuthorized === true,
|
|
@@ -737,7 +899,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
737
899
|
const postVerifyHooks = normalizePostPublishView(plan)
|
|
738
900
|
.flatMap((declaration) => declaration.hooks ?? [])
|
|
739
901
|
.filter((hook) => hook.phase === 'postVerify');
|
|
740
|
-
if (state.status === 'VERIFIED' && postVerifyHooks.length > 0 && deps.postVerifyRelease) {
|
|
902
|
+
if (state.status === 'VERIFIED' && state.verifyRunPath && postVerifyHooks.length > 0 && deps.postVerifyRelease) {
|
|
741
903
|
postVerifyRanThisCall = true;
|
|
742
904
|
let postVerifyOutcome;
|
|
743
905
|
try {
|
|
@@ -785,6 +947,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
785
947
|
if (
|
|
786
948
|
state.status === 'VERIFIED'
|
|
787
949
|
&& deps.postVerifyRelease
|
|
950
|
+
&& state.verifyRunPath
|
|
788
951
|
&& !postVerifyRanThisCall
|
|
789
952
|
&& (!state.postVerify || state.postVerify.status !== 'DISTRIBUTED')
|
|
790
953
|
) {
|
package/src/core/config.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { validatePublicSurfaceGlob } from './public-surface.mjs';
|
|
|
27
27
|
import { canonicalPublicPath, publicPathCollisionKey } from '../snapshot/public-path.mjs';
|
|
28
28
|
import { isReservedReleaseControlPath } from './baseline.mjs';
|
|
29
29
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
30
|
+
import { assertTestIntakePaths } from './test-intake.mjs';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Constants
|
|
@@ -423,6 +424,10 @@ export async function loadProjectConfig({ root, configPath } = {}) {
|
|
|
423
424
|
);
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
if (config.testIntake) {
|
|
428
|
+
assertTestIntakePaths(config.testIntake);
|
|
429
|
+
}
|
|
430
|
+
|
|
426
431
|
// --- Compile forbiddenContentPatterns (fail closed on invalid regex) ---
|
|
427
432
|
const forbiddenContentPatterns = config.policy?.forbiddenContentPatterns ?? [];
|
|
428
433
|
for (const pattern of forbiddenContentPatterns) {
|
package/src/core/plan.mjs
CHANGED
|
@@ -210,7 +210,9 @@ export function validatePlan(plan) {
|
|
|
210
210
|
* still written to the plan file (schema unchanged, evidence chain intact)
|
|
211
211
|
* but is excluded from the digest. Everything else (planVersion, units with
|
|
212
212
|
* every frozenSnapshot artifact identity field, externalActions identity,
|
|
213
|
-
* production, configDigest, verificationGates, snapshotDigest
|
|
213
|
+
* production, configDigest, verificationGates, snapshotDigest, and
|
|
214
|
+
* testAcceptance when present) binds. testAcceptance is intentionally absent
|
|
215
|
+
* from the exclusion list below: it stays in the binding layer.
|
|
214
216
|
* For v3 the frozen `postPublish` array and every item binding field stay in
|
|
215
217
|
* the binding layer: any order or binding change moves the digest.
|
|
216
218
|
*
|