release-skill 0.9.37 → 0.10.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.
Files changed (84) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.qoder-plugin/plugin.json +1 -1
  7. package/CHANGELOG.md +56 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +26 -14
  11. package/README.zh-CN.md +26 -14
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +1918 -1450
  14. package/adapters/claude/schemas/release-plan.schema.json +1 -0
  15. package/adapters/claude/schemas/test-intake.schema.json +1 -0
  16. package/adapters/claude/skills/release-help/SKILL.md +36 -41
  17. package/adapters/claude/skills/release-prepare/SKILL.md +35 -3
  18. package/adapters/claude/skills/release-publish/SKILL.md +11 -1
  19. package/adapters/claude/skills/release-reconcile/SKILL.md +14 -0
  20. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  21. package/adapters/codex/bin/release-skill.bundle.mjs +1918 -1450
  22. package/adapters/codex/schemas/release-plan.schema.json +1 -0
  23. package/adapters/codex/schemas/test-intake.schema.json +1 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +36 -41
  25. package/adapters/codex/skills/release-prepare/SKILL.md +35 -3
  26. package/adapters/codex/skills/release-publish/SKILL.md +11 -1
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +14 -0
  28. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  29. package/adapters/cursor/bin/release-skill.bundle.mjs +1918 -1450
  30. package/adapters/cursor/schemas/release-plan.schema.json +1 -0
  31. package/adapters/cursor/schemas/test-intake.schema.json +1 -0
  32. package/adapters/cursor/skills/release-help/SKILL.md +36 -41
  33. package/adapters/cursor/skills/release-prepare/SKILL.md +35 -3
  34. package/adapters/cursor/skills/release-publish/SKILL.md +11 -1
  35. package/adapters/cursor/skills/release-reconcile/SKILL.md +14 -0
  36. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  37. package/adapters/kimi/bin/release-skill.bundle.mjs +1918 -1450
  38. package/adapters/kimi/schemas/release-plan.schema.json +1 -0
  39. package/adapters/kimi/schemas/test-intake.schema.json +1 -0
  40. package/adapters/kimi/skills/release-help/SKILL.md +36 -41
  41. package/adapters/kimi/skills/release-prepare/SKILL.md +35 -3
  42. package/adapters/kimi/skills/release-publish/SKILL.md +11 -1
  43. package/adapters/kimi/skills/release-reconcile/SKILL.md +14 -0
  44. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  45. package/adapters/qoder/bin/release-skill.bundle.mjs +1918 -1450
  46. package/adapters/qoder/schemas/release-plan.schema.json +1 -0
  47. package/adapters/qoder/schemas/test-intake.schema.json +1 -0
  48. package/adapters/qoder/skills/release-help/SKILL.md +36 -41
  49. package/adapters/qoder/skills/release-prepare/SKILL.md +35 -3
  50. package/adapters/qoder/skills/release-publish/SKILL.md +11 -1
  51. package/adapters/qoder/skills/release-reconcile/SKILL.md +14 -0
  52. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  53. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1918 -1450
  54. package/adapters/workbuddy/schemas/release-plan.schema.json +1 -0
  55. package/adapters/workbuddy/schemas/test-intake.schema.json +1 -0
  56. package/adapters/workbuddy/skills/release-help/SKILL.md +36 -41
  57. package/adapters/workbuddy/skills/release-prepare/SKILL.md +35 -3
  58. package/adapters/workbuddy/skills/release-publish/SKILL.md +11 -1
  59. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +14 -0
  60. package/bin/release-skill-cli.mjs +5 -0
  61. package/bin/release-skill.bundle.mjs +1918 -1450
  62. package/package.json +3 -2
  63. package/platform-manifest.json +4 -4
  64. package/references/01-state-machine.md +26 -0
  65. package/references/02-project-config.md +20 -0
  66. package/schemas/release-plan.schema.json +1 -0
  67. package/schemas/test-intake.schema.json +1 -0
  68. package/skills/release-help/SKILL.md +36 -41
  69. package/skills/release-prepare/SKILL.md +35 -3
  70. package/skills/release-publish/SKILL.md +11 -1
  71. package/skills/release-reconcile/SKILL.md +14 -0
  72. package/skills-src/release-help/SKILL.md +36 -41
  73. package/skills-src/release-prepare/SKILL.md +35 -3
  74. package/skills-src/release-publish/SKILL.md +11 -1
  75. package/skills-src/release-reconcile/SKILL.md +14 -0
  76. package/src/commands/approve.mjs +33 -24
  77. package/src/commands/post-release-finish.mjs +9 -0
  78. package/src/commands/prepare.mjs +9 -2
  79. package/src/commands/publish.mjs +10 -2
  80. package/src/commands/reconcile.mjs +2 -0
  81. package/src/commands/route.mjs +2 -159
  82. package/src/core/plan-replacement.mjs +371 -0
  83. package/src/core/pre-authority-failure.mjs +242 -0
  84. package/src/core/test-candidate.mjs +15 -71
@@ -1,3 +1,4 @@
1
+ import { readFormalPlans, bindReplacement, canonicalFormalPlanPath, overlaps } from './plan-replacement.mjs';
1
2
  /**
2
3
  * One test-intake candidate: exclusive save, strict read, and the no-git
3
4
  * handoff decision shared by prepare and ship.
@@ -271,78 +272,12 @@ function targetVersionsOf(plan) {
271
272
  .filter((version) => typeof version === 'string' && version.length > 0))];
272
273
  }
273
274
 
274
- function sameTargetVersions(left, right) {
275
- return left.length === right.length && left.every((version) => right.includes(version));
276
- }
277
-
278
275
  /**
279
276
  * Formal plans under `.release-skill/plans`. The convenience pointer is not
280
277
  * an acceptance basis. Does not run git.
281
278
  *
282
279
  * @param {string} root
283
280
  */
284
- async function readFormalPlans(root) {
285
- const plansPath = resolve(root, '.release-skill', 'plans');
286
- let names;
287
- try {
288
- names = await readdir(plansPath);
289
- } catch (err) {
290
- if (err?.code === 'ENOENT') {
291
- try {
292
- await lstat(plansPath);
293
- } catch (statError) {
294
- if (statError?.code === 'ENOENT') return [];
295
- throw new ReleaseError(CONFIG_INVALID, 'formal test acceptance plans could not be read', {
296
- reason: 'TEST_CANDIDATE_AUTHORITY_INVALID',
297
- field: 'plansPath',
298
- cause: statError?.code ?? 'unknown',
299
- });
300
- }
301
- }
302
- throw new ReleaseError(CONFIG_INVALID, 'formal test acceptance plans could not be read', {
303
- reason: 'TEST_CANDIDATE_AUTHORITY_INVALID',
304
- field: 'plansPath',
305
- cause: err?.code ?? 'unknown',
306
- });
307
- }
308
- const plans = [];
309
- for (const name of names) {
310
- if (!name.endsWith('.json')) continue;
311
- const rel = `.release-skill/plans/${name}`;
312
- let receipt;
313
- try {
314
- receipt = await readFileStrict(root, rel, { encoding: 'utf8' });
315
- } catch (err) {
316
- const cause = harnessKind(err) ?? 'unknown';
317
- throw new ReleaseError(CONFIG_INVALID, 'formal test acceptance plan could not be read', {
318
- reason: 'TEST_CANDIDATE_AUTHORITY_INVALID',
319
- field: 'planPath',
320
- path: rel,
321
- cause,
322
- });
323
- }
324
- let plan;
325
- try {
326
- plan = JSON.parse(receipt.content);
327
- } catch {
328
- throw new ReleaseError(CONFIG_INVALID, 'formal test acceptance plan is not valid JSON', {
329
- reason: 'TEST_CANDIDATE_AUTHORITY_INVALID',
330
- field: 'planPath',
331
- path: rel,
332
- });
333
- }
334
- plans.push({
335
- planPath: rel,
336
- planDigest: typeof plan?.digest === 'string' ? plan.digest : name.slice(0, -'.json'.length),
337
- status: plan?.status,
338
- subjectDigest: typeof plan?.testAcceptance?.subjectDigest === 'string'
339
- ? plan.testAcceptance.subjectDigest
340
- : null,
341
- targetVersions: targetVersionsOf(plan),
342
- });
343
- }
344
- return plans;
345
- }
346
281
 
347
282
  async function sameAuthorityFile(left, right) {
348
283
  const identity = async (value) => {
@@ -432,10 +367,11 @@ export async function decideTestCandidateHandoff({
432
367
  expectedSubjectDigest,
433
368
  configDigest,
434
369
  extraDrifts = [],
370
+ replaceUnpublishedPlan,
435
371
  }) {
436
372
  if (!config?.testIntake) {
437
- if (candidatePath) {
438
- throw new ReleaseError(CONFIG_INVALID, 'candidate path requires testIntake', {
373
+ if (candidatePath || replaceUnpublishedPlan !== undefined) {
374
+ throw new ReleaseError(CONFIG_INVALID, 'candidate or replacement path requires testIntake', {
439
375
  reason: 'TEST_CANDIDATE_WITHOUT_INTAKE',
440
376
  field: 'candidatePath',
441
377
  });
@@ -449,7 +385,8 @@ export async function decideTestCandidateHandoff({
449
385
  });
450
386
  }
451
387
  if (!candidatePath) {
452
- const saved = await writeTestCandidate({ root, runDir, prepared, requirements, prepareOptions });
388
+ const bound = await bindReplacement({ root, prepared, replaceUnpublishedPlan, resultFile: requirements.resultFile });
389
+ const saved = await writeTestCandidate({ root, runDir, prepared: bound, requirements, prepareOptions });
453
390
  const subjectDigest = computePlanDigest(saved.candidate.prepared);
454
391
  return {
455
392
  kind: 'save-candidate',
@@ -483,6 +420,9 @@ export async function decideTestCandidateHandoff({
483
420
  });
484
421
  }
485
422
 
423
+ if (replaceUnpublishedPlan !== undefined && canonicalFormalPlanPath(root, replaceUnpublishedPlan) !== `.release-skill/plans/${loaded.candidate.prepared.replacesPlanDigest}.json`) {
424
+ throw new ReleaseError(GATE_FAILED, 'replacement selection differs from frozen candidate');
425
+ }
486
426
  const options = loaded.candidate.prepareOptions;
487
427
  const drifts = [...extraDrifts];
488
428
  if (loaded.candidate.writerVersion !== WRITER_VERSION) drifts.push('writerVersion');
@@ -510,10 +450,13 @@ export async function decideTestCandidateHandoff({
510
450
  });
511
451
  }
512
452
 
513
- const formalPlans = await readFormalPlans(root);
453
+ const formalPlans = await readFormalPlans(root, loaded.candidate.prepared);
514
454
  const targetVersions = targetVersionsOf(loaded.candidate.prepared);
455
+ const predecessor = loaded.candidate.prepared.replacesPlanDigest;
456
+ const retired = new Set(formalPlans.map((entry) => entry.plan.replacesPlanDigest).filter(Boolean));
457
+ if (predecessor) retired.add(predecessor);
515
458
  const differentBasis = formalPlans.filter((plan) => (
516
- sameTargetVersions(plan.targetVersions, targetVersions) && plan.subjectDigest !== subjectDigest
459
+ !retired.has(plan.planDigest) && overlaps(plan.plan, loaded.candidate.prepared) && plan.subjectDigest !== subjectDigest
517
460
  ));
518
461
  if (differentBasis.length > 0) {
519
462
  throw new ReleaseError(GATE_FAILED, 'a different formal acceptance basis already exists for this target', {
@@ -543,6 +486,7 @@ export async function decideTestCandidateHandoff({
543
486
  };
544
487
  }
545
488
 
489
+ await bindReplacement({ root, prepared: loaded.candidate.prepared, replaceUnpublishedPlan, resultFile: requirements.resultFile });
546
490
  const acceptance = await acceptTestResults({
547
491
  root,
548
492
  requirements,