@cleocode/contracts 2026.10.27 → 2026.11.1-canary.3

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 (106) hide show
  1. package/README.md +2 -0
  2. package/dist/.source-stamp.json +4 -0
  3. package/dist/__tests__/acceptance-gate.test.js +15 -3
  4. package/dist/__tests__/acceptance-gate.test.js.map +1 -1
  5. package/dist/__tests__/evidence-atom-schema.test.js +3 -1
  6. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  7. package/dist/__tests__/memory-wire-shapes.test.js.map +1 -1
  8. package/dist/acceptance-gate-schema.d.ts +25 -0
  9. package/dist/acceptance-gate-schema.d.ts.map +1 -1
  10. package/dist/acceptance-gate-schema.js +5 -0
  11. package/dist/acceptance-gate-schema.js.map +1 -1
  12. package/dist/acceptance-gate.d.ts +8 -0
  13. package/dist/acceptance-gate.d.ts.map +1 -1
  14. package/dist/data-accessor.d.ts +7 -0
  15. package/dist/data-accessor.d.ts.map +1 -1
  16. package/dist/data-accessor.js.map +1 -1
  17. package/dist/dependency.d.ts +2 -0
  18. package/dist/dependency.d.ts.map +1 -1
  19. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  20. package/dist/dispatch/operations-registry.js +216 -0
  21. package/dist/dispatch/operations-registry.js.map +1 -1
  22. package/dist/docs/modernize.d.ts +435 -0
  23. package/dist/docs/modernize.d.ts.map +1 -0
  24. package/dist/docs/modernize.js +137 -0
  25. package/dist/docs/modernize.js.map +1 -0
  26. package/dist/docs-taxonomy.d.ts +6 -0
  27. package/dist/docs-taxonomy.d.ts.map +1 -1
  28. package/dist/docs-taxonomy.js +38 -1
  29. package/dist/docs-taxonomy.js.map +1 -1
  30. package/dist/done-plan.d.ts +27 -2
  31. package/dist/done-plan.d.ts.map +1 -1
  32. package/dist/evidence-atom-schema.d.ts +78 -4
  33. package/dist/evidence-atom-schema.d.ts.map +1 -1
  34. package/dist/evidence-atom-schema.js +136 -11
  35. package/dist/evidence-atom-schema.js.map +1 -1
  36. package/dist/exit-codes.d.ts +41 -0
  37. package/dist/exit-codes.d.ts.map +1 -1
  38. package/dist/exit-codes.js +41 -0
  39. package/dist/exit-codes.js.map +1 -1
  40. package/dist/index.d.ts +9 -5
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/invariants/sync-write-invariants.d.ts.map +1 -1
  45. package/dist/invariants/sync-write-invariants.js +15 -0
  46. package/dist/invariants/sync-write-invariants.js.map +1 -1
  47. package/dist/memory/search.d.ts +13 -0
  48. package/dist/memory/search.d.ts.map +1 -1
  49. package/dist/merge-close.d.ts +167 -0
  50. package/dist/merge-close.d.ts.map +1 -0
  51. package/dist/merge-close.js +16 -0
  52. package/dist/merge-close.js.map +1 -0
  53. package/dist/operations/admin.d.ts +6 -0
  54. package/dist/operations/admin.d.ts.map +1 -1
  55. package/dist/operations/docs.d.ts +192 -3
  56. package/dist/operations/docs.d.ts.map +1 -1
  57. package/dist/operations/docs.js.map +1 -1
  58. package/dist/operations/worktree.d.ts +63 -0
  59. package/dist/operations/worktree.d.ts.map +1 -1
  60. package/dist/project-context.d.ts +31 -0
  61. package/dist/project-context.d.ts.map +1 -1
  62. package/dist/release/authorization.d.ts +46 -0
  63. package/dist/release/authorization.d.ts.map +1 -0
  64. package/dist/release/authorization.js +10 -0
  65. package/dist/release/authorization.js.map +1 -0
  66. package/dist/review-verdict.d.ts +125 -0
  67. package/dist/review-verdict.d.ts.map +1 -0
  68. package/dist/review-verdict.js +14 -0
  69. package/dist/review-verdict.js.map +1 -0
  70. package/dist/session-journal.d.ts.map +1 -1
  71. package/dist/session-journal.js +7 -1
  72. package/dist/session-journal.js.map +1 -1
  73. package/dist/task-record.d.ts +6 -1
  74. package/dist/task-record.d.ts.map +1 -1
  75. package/dist/task.d.ts +130 -3
  76. package/dist/task.d.ts.map +1 -1
  77. package/package.json +2 -2
  78. package/schemas/acceptance-gate.schema.json +24 -0
  79. package/schemas/task.schema.json +24 -0
  80. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  81. package/src/__tests__/acceptance-gate.test.ts +16 -3
  82. package/src/__tests__/evidence-atom-schema.test.ts +3 -1
  83. package/src/__tests__/memory-wire-shapes.test.ts +2 -0
  84. package/src/acceptance-gate-schema.ts +5 -0
  85. package/src/acceptance-gate.ts +9 -0
  86. package/src/data-accessor.ts +8 -0
  87. package/src/dependency.ts +2 -0
  88. package/src/dispatch/operations-registry.ts +228 -0
  89. package/src/docs/modernize.ts +407 -0
  90. package/src/docs-taxonomy.ts +44 -1
  91. package/src/done-plan.ts +27 -2
  92. package/src/evidence-atom-schema.ts +167 -14
  93. package/src/exit-codes.ts +41 -0
  94. package/src/index.ts +55 -0
  95. package/src/invariants/sync-write-invariants.ts +16 -0
  96. package/src/memory/search.ts +10 -0
  97. package/src/merge-close.ts +168 -0
  98. package/src/operations/admin.ts +6 -0
  99. package/src/operations/docs.ts +199 -4
  100. package/src/operations/worktree.ts +66 -0
  101. package/src/project-context.ts +33 -0
  102. package/src/release/authorization.ts +48 -0
  103. package/src/review-verdict.ts +122 -0
  104. package/src/session-journal.ts +7 -1
  105. package/src/task-record.ts +6 -1
  106. package/src/task.ts +124 -3
@@ -64,6 +64,8 @@ export const EVIDENCE_ATOM_KINDS = [
64
64
  'decision',
65
65
  'pr',
66
66
  'ci',
67
+ 'ci-run',
68
+ 'deploy',
67
69
  'loc-drop',
68
70
  'callsite-coverage',
69
71
  'satisfies',
@@ -81,6 +83,12 @@ const PARSER_PREFIXES = [...EVIDENCE_ATOM_KINDS, 'state'] as const;
81
83
  */
82
84
  const KIND_BOUNDARY_REGEX = new RegExp(`;(?=\\s*(?:${PARSER_PREFIXES.join('|')})\\s*:)`, 'g');
83
85
 
86
+ /** A `deploy:` provider name (T14083): lowercase, e.g. `railway`, `fly`, `cloudflare-pages`. */
87
+ const DEPLOY_PROVIDER_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
88
+
89
+ /** A `deploy:` deployment id (T14083): no whitespace, `@` or `|`. */
90
+ const DEPLOY_ID_REGEX = /^[^\s@|]+$/;
91
+
84
92
  // ---------------------------------------------------------------------------
85
93
  // Per-atom Zod schemas
86
94
  // ---------------------------------------------------------------------------
@@ -262,6 +270,47 @@ export const ciAtomSchema = z.object({
262
270
  .optional(),
263
271
  });
264
272
 
273
+ /**
274
+ * `ci-run:<runId>` atom (T14149): a run of the `repeat-run.yml` workflow
275
+ * (dispatched on the default branch) that ran one test file N times, all green.
276
+ * Validated from GitHub's record of the run and its uploaded report, never from
277
+ * a file the agent supplies. It links an acceptance criterion that needs a heavy
278
+ * or repeated run; it does not satisfy `testsPassed` on its own.
279
+ *
280
+ * Format: `ci-run:<positive integer run id>` (e.g. `ci-run:40812345678`).
281
+ *
282
+ * @task T14149
283
+ */
284
+ export const ciRunAtomSchema = z.object({
285
+ kind: z.literal('ci-run'),
286
+ runId: z.number().int().positive('ci-run atom requires a positive integer run id'),
287
+ });
288
+
289
+ /**
290
+ * `deploy:<provider>/<deploymentId>@<sha>|<probe-url>` atom (T14083): a
291
+ * deployment of commit `<sha>`, proven at verify time by a bounded GET of
292
+ * `<probe-url>` that returns 2xx and names `<sha>` in its body or in an
293
+ * `X-Commit` / `X-Version` header. Satisfies only the gates the project lists
294
+ * in `evidence.deploySatisfies` (no default).
295
+ *
296
+ * Format: `deploy:railway/9f3c2a@1a2b3c4d|https://api.example.com/health`.
297
+ * The provider and deployment id are recorded as given; the probe, not the
298
+ * provider, is the proof.
299
+ *
300
+ * @task T14083
301
+ */
302
+ export const deployAtomSchema = z.object({
303
+ kind: z.literal('deploy'),
304
+ provider: z
305
+ .string()
306
+ .regex(DEPLOY_PROVIDER_REGEX, 'deploy atom provider must be a lowercase name'),
307
+ deploymentId: z.string().regex(DEPLOY_ID_REGEX, 'deploy atom requires a deployment id'),
308
+ sha: z.string().regex(/^[0-9a-f]{7,40}$/, 'deploy atom sha must be 7-40 lowercase hex chars'),
309
+ probeUrl: z
310
+ .string()
311
+ .regex(/^https?:\/\/\S+$/, 'deploy atom probe url must start with http:// or https://'),
312
+ });
313
+
265
314
  /**
266
315
  * `loc-drop:<fromLines>:<toLines>` atom — proves that a migrated engine shed
267
316
  * lines. Required for the `implemented` gate when the task carries the
@@ -428,6 +477,8 @@ export const satisfiesAtomSchema = z.object({
428
477
  * - `note` — free-form note (T832)
429
478
  * - `decision` — brain_decisions row reference (T1875)
430
479
  * - `pr` — merged-PR retroactive proof (T9764 / T9838)
480
+ * - `ci-run` — a green repeat-run.yml run of one test file (T14149)
481
+ * - `deploy` — probed deployment of a commit (T14083)
431
482
  * - `loc-drop` — engine-migration LOC reduction (T1604)
432
483
  * - `callsite-coverage` — exported-symbol production callsite (T1605)
433
484
  * - `satisfies` — cross-task AC binding (T10506 / ADR-079-r2;
@@ -447,6 +498,8 @@ export const EvidenceAtomSchema = z.discriminatedUnion('kind', [
447
498
  decisionAtomSchema,
448
499
  prAtomSchema,
449
500
  ciAtomSchema,
501
+ ciRunAtomSchema,
502
+ deployAtomSchema,
450
503
  locDropAtomSchema,
451
504
  callsiteCoverageAtomSchema,
452
505
  satisfiesAtomSchema,
@@ -546,10 +599,59 @@ export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
546
599
  nexusImpact: { oneOf: [['tool'], ['note']] },
547
600
  });
548
601
 
602
+ /**
603
+ * The gate profiles a task with no code change may declare as a
604
+ * `profile:<name>` label (T13474): operations work, a decision, a census or
605
+ * an audit. The closed set lives here so the gate requirements below and the
606
+ * label rules in core read one list.
607
+ *
608
+ * @task T13474
609
+ * @task T14187
610
+ */
611
+ export const GATE_PROFILE_NAMES = ['ops', 'decision', 'census', 'audit'] as const;
612
+
613
+ /** One of {@link GATE_PROFILE_NAMES}. */
614
+ export type GateProfileName = (typeof GATE_PROFILE_NAMES)[number];
615
+
616
+ /**
617
+ * Atom combinations a gate accepts IN ADDITION to
618
+ * {@link GATE_EVIDENCE_REQUIREMENTS} for a task that declares a gate profile
619
+ * and whose change touches no code (T14187). An operations, census or audit
620
+ * task (a live-data repair, a measurement) has no commit, PR or decision to
621
+ * cite: its proof is the stored document recording what was done or found,
622
+ * so `implemented` also accepts `files` (that document) with a `note`. A
623
+ * decision task still proves itself with its decision. Core applies this
624
+ * only when every `files:` path is a document, so a code change still owes a
625
+ * commit or PR. `cleo done --plan` and the gates read this one table.
626
+ *
627
+ * @task T14187
628
+ */
629
+ export const PROFILE_GATE_EVIDENCE: Readonly<
630
+ Record<
631
+ GateProfileName,
632
+ Partial<Record<VerificationGate, ReadonlyArray<ReadonlyArray<EvidenceAtomKind>>>>
633
+ >
634
+ > = Object.freeze({
635
+ ops: { implemented: [['files', 'note']] },
636
+ decision: {},
637
+ census: { implemented: [['files', 'note']] },
638
+ audit: { implemented: [['files', 'note']] },
639
+ });
640
+
549
641
  // ---------------------------------------------------------------------------
550
642
  // Helpers
551
643
  // ---------------------------------------------------------------------------
552
644
 
645
+ /** The combinations `gate` accepts, with a gate profile's additions. */
646
+ function gateCombinations(
647
+ gate: VerificationGate,
648
+ profile?: GateProfileName | null,
649
+ ): ReadonlyArray<ReadonlyArray<EvidenceAtomKind>> {
650
+ const base = GATE_EVIDENCE_REQUIREMENTS[gate]?.oneOf ?? [];
651
+ const extra = profile ? (PROFILE_GATE_EVIDENCE[profile][gate] ?? []) : [];
652
+ return [...base, ...extra];
653
+ }
654
+
553
655
  /**
554
656
  * Result of {@link validateEvidenceForGate}.
555
657
  *
@@ -582,9 +684,13 @@ export type EvidenceValidationResult = { readonly ok: true } | EvidenceValidatio
582
684
  *
583
685
  * @task T10337
584
686
  */
585
- export function formatGateRequirement(gate: VerificationGate): string {
586
- const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
587
- return requirement.oneOf.map((set) => `[${set.join(' AND ')}]`).join(' OR ');
687
+ export function formatGateRequirement(
688
+ gate: VerificationGate,
689
+ profile?: GateProfileName | null,
690
+ ): string {
691
+ return gateCombinations(gate, profile)
692
+ .map((set) => `[${set.join(' AND ')}]`)
693
+ .join(' OR ');
588
694
  }
589
695
 
590
696
  /**
@@ -605,6 +711,8 @@ const ATOM_EXAMPLES: Readonly<Record<EvidenceAtomKind, string>> = Object.freeze(
605
711
  decision: 'decision:D-arch-001',
606
712
  pr: 'pr:357',
607
713
  ci: 'ci:357',
714
+ 'ci-run': 'ci-run:<runId>',
715
+ deploy: 'deploy:<provider>/<deploymentId>@<sha>|https://<host>/health',
608
716
  'loc-drop': 'loc-drop:<fromLines>:<toLines>',
609
717
  'callsite-coverage': 'callsite-coverage:<symbolName>:<relativeSourcePath>',
610
718
  satisfies: 'satisfies:T1234#AC2',
@@ -654,16 +762,20 @@ function renderCombinationAsEvidence(combination: ReadonlyArray<EvidenceAtomKind
654
762
  * @task T9949
655
763
  * @adr ADR-051 §2.3
656
764
  */
657
- export function formatGateRequirementHint(gate: VerificationGate): string {
765
+ export function formatGateRequirementHint(
766
+ gate: VerificationGate,
767
+ profile?: GateProfileName | null,
768
+ ): string {
658
769
  const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
659
770
  if (!requirement) {
660
771
  // Unknown gate — match validateEvidenceForGate's defensive default.
661
772
  return `Gate '${gate}' has no documented evidence requirements.`;
662
773
  }
774
+ const oneOf = gateCombinations(gate, profile);
663
775
 
664
776
  const lines: string[] = [
665
777
  `Gate '${gate}' requires programmatic evidence. Use ONE of:`,
666
- ...requirement.oneOf.map(
778
+ ...oneOf.map(
667
779
  (combo) =>
668
780
  ` - cleo verify T#### --gate ${gate} --evidence '${renderCombinationAsEvidence(combo)}'`,
669
781
  ),
@@ -671,16 +783,12 @@ export function formatGateRequirementHint(gate: VerificationGate): string {
671
783
 
672
784
  // Note-specific clarification — T9949's headline pain point.
673
785
  // The gate accepts note: alone IFF some single-element combination is ['note'].
674
- const noteAloneAccepted = requirement.oneOf.some(
675
- (combo) => combo.length === 1 && combo[0] === 'note',
676
- );
786
+ const noteAloneAccepted = oneOf.some((combo) => combo.length === 1 && combo[0] === 'note');
677
787
  if (noteAloneAccepted) {
678
788
  lines.push(`Note: 'note:' alone IS accepted for this gate (waiver-style evidence).`);
679
789
  } else {
680
790
  // Surface every alternative pairing involving note for at-a-glance routing.
681
- const notePairings = requirement.oneOf.filter(
682
- (combo) => combo.length > 1 && combo.includes('note'),
683
- );
791
+ const notePairings = oneOf.filter((combo) => combo.length > 1 && combo.includes('note'));
684
792
  if (notePairings.length > 0) {
685
793
  const partners = Array.from(
686
794
  new Set(notePairings.flatMap((combo) => combo.filter((k) => k !== 'note'))),
@@ -715,6 +823,8 @@ export function formatGateRequirementHint(gate: VerificationGate): string {
715
823
  *
716
824
  * @param gate - Verification gate being checked.
717
825
  * @param atoms - Already-parsed evidence atoms.
826
+ * @param profile - The task's gate profile when its change touches no code
827
+ * ({@link PROFILE_GATE_EVIDENCE}, T14187); omit otherwise.
718
828
  * @returns Pass / fail with a human-readable failure message.
719
829
  *
720
830
  * @example
@@ -731,6 +841,7 @@ export function formatGateRequirementHint(gate: VerificationGate): string {
731
841
  export function validateEvidenceForGate(
732
842
  gate: VerificationGate,
733
843
  atoms: ReadonlyArray<{ kind: EvidenceAtomKind }>,
844
+ profile?: GateProfileName | null,
734
845
  ): EvidenceValidationResult {
735
846
  const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
736
847
  if (!requirement) {
@@ -738,18 +849,18 @@ export function validateEvidenceForGate(
738
849
  // behavior from checkGateEvidenceMinimum when the gate had no entry).
739
850
  return { ok: true };
740
851
  }
741
- for (const combination of requirement.oneOf) {
852
+ for (const combination of gateCombinations(gate, profile)) {
742
853
  const satisfied = combination.every((kind) => atoms.some((a) => a.kind === kind));
743
854
  if (satisfied) return { ok: true };
744
855
  }
745
856
  return {
746
857
  ok: false,
747
- message: `Gate '${gate}' requires evidence: ${formatGateRequirement(gate)}`,
858
+ message: `Gate '${gate}' requires evidence: ${formatGateRequirement(gate, profile)}`,
748
859
  // T9949: rich, example-bearing remediation hint suitable for CLI `fix:`.
749
860
  // Always populated alongside `message` so consumers can choose between
750
861
  // the legacy single-line surface and the multi-line hint without re-
751
862
  // computing the spec.
752
- hint: formatGateRequirementHint(gate),
863
+ hint: formatGateRequirementHint(gate, profile),
753
864
  };
754
865
  }
755
866
 
@@ -835,6 +946,35 @@ function parsePrReference(
835
946
  : { prNumber: second, componentPrNumber: first, ...repo };
836
947
  }
837
948
 
949
+ /**
950
+ * Parse a `deploy:` payload, `<provider>/<deploymentId>@<sha>|<probe-url>`
951
+ * (T14083). The provider is lowercased, the SHA lowercased hex.
952
+ */
953
+ function parseDeployPayload(payload: string, chunk: string): z.infer<typeof deployAtomSchema> {
954
+ const fix =
955
+ 'Use format: deploy:<provider>/<deploymentId>@<sha>|<probe-url> e.g. ' +
956
+ 'deploy:railway/9f3c2a@1a2b3c4d|https://api.example.com/health';
957
+ const bar = payload.indexOf('|');
958
+ const match = /^([^/]+)\/(.+)@([0-9A-Fa-f]{7,40})$/.exec(
959
+ bar < 0 ? '' : payload.slice(0, bar).trim(),
960
+ );
961
+ const probeUrl = bar < 0 ? '' : payload.slice(bar + 1).trim();
962
+ const parsed = deployAtomSchema.safeParse({
963
+ kind: 'deploy',
964
+ provider: match?.[1]?.toLowerCase() ?? '',
965
+ deploymentId: match?.[2] ?? '',
966
+ sha: match?.[3]?.toLowerCase() ?? '',
967
+ probeUrl,
968
+ });
969
+ if (!match || !parsed.success) {
970
+ throw new EvidenceParseError(
971
+ `Malformed deploy atom: "${chunk}" (${!match || parsed.success ? 'expected <provider>/<deploymentId>@<sha>|<probe-url>' : parsed.error.issues[0]?.message})`,
972
+ fix,
973
+ );
974
+ }
975
+ return parsed.data;
976
+ }
977
+
838
978
  export function parseEvidenceString(raw: string): EvidenceAtom[] {
839
979
  if (!raw || typeof raw !== 'string') {
840
980
  throw new EvidenceParseError(
@@ -919,6 +1059,19 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
919
1059
  atoms.push(kind === 'pr' ? { kind: 'pr', ...ref } : { kind: 'ci', ...ref });
920
1060
  break;
921
1061
  }
1062
+ case 'ci-run': {
1063
+ if (!/^[1-9][0-9]*$/.test(payload)) {
1064
+ throw new EvidenceParseError(
1065
+ `ci-run atom requires a positive integer run id, got "${payload}" in "${chunk}"`,
1066
+ 'Use format: ci-run:<runId> e.g. ci-run:40812345678 (a repeat-run.yml run on main)',
1067
+ );
1068
+ }
1069
+ atoms.push({ kind: 'ci-run', runId: Number(payload) });
1070
+ break;
1071
+ }
1072
+ case 'deploy':
1073
+ atoms.push(parseDeployPayload(payload, chunk));
1074
+ break;
922
1075
  case 'loc-drop': {
923
1076
  const firstColon = payload.indexOf(':');
924
1077
  if (firstColon < 1 || firstColon === payload.length - 1) {
package/src/exit-codes.ts CHANGED
@@ -392,6 +392,47 @@ export const E_EVIDENCE_INSUFFICIENT = 'E_EVIDENCE_INSUFFICIENT' as const;
392
392
  export const E_PLAN_NOT_FOUND = 'E_PLAN_NOT_FOUND' as const;
393
393
  /** Generic release-plan validation failure (e.g. schema). Exit code 6. */
394
394
  export const E_RELEASE_PLAN_INVALID = 'E_RELEASE_PLAN_INVALID' as const;
395
+ /**
396
+ * `cleo release cut` found no recent default-branch commit whose required `CI`
397
+ * check run concluded `success` (or the pinned `--sha` is not green). Exit code 6.
398
+ * `error.details.candidates[]` lists each commit checked and its CI state.
399
+ *
400
+ * @task T14102
401
+ */
402
+ export const E_RELEASE_NO_GREEN_COMMIT = 'E_RELEASE_NO_GREEN_COMMIT' as const;
403
+ /**
404
+ * `cleo release cut` found a changeset in range that no merged pull request
405
+ * brought in (pushed straight to the default branch). Exit code 6.
406
+ * `error.details.changesets[]` lists each file and the commit that added it.
407
+ *
408
+ * @task T14102
409
+ */
410
+ export const E_RELEASE_UNMERGED_CHANGESET = 'E_RELEASE_UNMERGED_CHANGESET' as const;
411
+ /**
412
+ * `cleo release cut` could not create the tag (the tag object or its ref POST
413
+ * failed). Exit code 1. `error.details.tagObject` names a created but
414
+ * unreferenced tag object, which is harmless. The attempt is audited.
415
+ *
416
+ * @task T14102
417
+ */
418
+ export const E_RELEASE_TAG_FAILED = 'E_RELEASE_TAG_FAILED' as const;
419
+ /**
420
+ * `cleo release cut --yes` from a non-interactive caller (an agent) without a
421
+ * standing release authorization (owner decision D11212; T14141 stores it).
422
+ * Exit code 6. A human at a terminal confirms with `--yes` directly.
423
+ *
424
+ * @task T14102
425
+ */
426
+ export const E_RELEASE_NOT_AUTHORIZED = 'E_RELEASE_NOT_AUTHORIZED' as const;
427
+ /**
428
+ * `cleo release cut` found docs rot (stale, duplicate or orphaned docs, per
429
+ * `cleo docs rot`) and refused to tag. Exit code 6. `error.details.docsRot`
430
+ * carries the scan.
431
+ *
432
+ * @task T14102
433
+ * @task T14111
434
+ */
435
+ export const E_DOCS_ROT = 'E_DOCS_ROT' as const;
395
436
  /**
396
437
  * `cleo release plan` (or the lint script) encountered a `.changeset/*.md`
397
438
  * file whose YAML frontmatter cannot be parsed. Exit code 6 (VALIDATION_ERROR).
package/src/index.ts CHANGED
@@ -422,6 +422,32 @@ export { CANONICAL_DOMAINS } from './dispatch/identity.js';
422
422
  export type { OperationDef, Resolution } from './dispatch/operation-def.js';
423
423
  // === Dispatch OPERATIONS data + builder helpers (T10061 — T9833b / E-CLI-BOUNDARY / SG-ARCH-SOLID) ===
424
424
  export { defineDomain, defineOp, OPERATIONS } from './dispatch/operations-registry.js';
425
+ // === docs modernize plan + receipt (T14113) ===
426
+ export type {
427
+ DocsModernizeDisposition,
428
+ DocsModernizeJudgment,
429
+ DocsModernizeJudgmentQuestion,
430
+ DocsModernizeJudgmentSource,
431
+ DocsModernizeLinkFix,
432
+ DocsModernizeOpKind,
433
+ DocsModernizeOpPrior,
434
+ DocsModernizeOpState,
435
+ DocsModernizeOverride,
436
+ DocsModernizeOverrides,
437
+ DocsModernizePlan,
438
+ DocsModernizePlanRow,
439
+ DocsModernizeReceipt,
440
+ DocsModernizeReceiptOp,
441
+ DocsModernizeStoreRow,
442
+ } from './docs/modernize.js';
443
+ export {
444
+ DOCS_MODERNIZE_DISPOSITIONS,
445
+ DOCS_MODERNIZE_OPS,
446
+ docsModernizeJournalLineSchema,
447
+ docsModernizeOverridesSchema,
448
+ docsModernizePlanSchema,
449
+ docsModernizeReceiptSchema,
450
+ } from './docs/modernize.js';
425
451
  // === Docs Provenance Graph (T10166 / Saga T9855 / E12) ===
426
452
  export type {
427
453
  DocLifecycleStatus,
@@ -613,6 +639,7 @@ export type {
613
639
  EvidenceValidationFailure,
614
640
  EvidenceValidationResult,
615
641
  GateEvidenceRequirement,
642
+ GateProfileName,
616
643
  } from './evidence-atom-schema.js';
617
644
  export {
618
645
  AC_ALIAS_REGEX,
@@ -627,8 +654,10 @@ export {
627
654
  formatGateRequirement,
628
655
  formatGateRequirementHint,
629
656
  GATE_EVIDENCE_REQUIREMENTS,
657
+ GATE_PROFILE_NAMES,
630
658
  locDropAtomSchema,
631
659
  noteAtomSchema,
660
+ PROFILE_GATE_EVIDENCE,
632
661
  parseEvidenceString,
633
662
  prAtomSchema,
634
663
  qaRunAtomSchema,
@@ -1222,6 +1251,17 @@ export type {
1222
1251
  PatternRecord,
1223
1252
  SessionSummary,
1224
1253
  } from './memory.js';
1254
+ export type {
1255
+ MacosRegressionSync,
1256
+ MergeDirective,
1257
+ MergePrOutcome,
1258
+ MergeReceipt,
1259
+ MergeSweepHooks,
1260
+ MergeSweepOptions,
1261
+ MergeSweepReconcile,
1262
+ MergeSweepResult,
1263
+ MergeTaskOutcome,
1264
+ } from './merge-close.js';
1225
1265
  export type {
1226
1266
  MigrationEnumDrift,
1227
1267
  MigrationForeignKeyViolation,
@@ -2094,6 +2134,9 @@ export type {
2094
2134
  ListWorktreesOptions,
2095
2135
  ListWorktreesOpts,
2096
2136
  ListWorktreesResult,
2137
+ MergedCleanupItem,
2138
+ MergedCleanupOpts,
2139
+ MergedCleanupResult,
2097
2140
  PrunedWorktreeOutcome,
2098
2141
  PruneOrphanedWorktreesOpts,
2099
2142
  PruneOrphanedWorktreesResult,
@@ -2199,6 +2242,7 @@ export { PS_STABLE_ENV } from './process-probe.js';
2199
2242
  // === Project Context (ecosystem detection types) ===
2200
2243
  export type {
2201
2244
  EcosystemHint,
2245
+ EvidenceRepositoryDeclaration,
2202
2246
  FileNamingConvention,
2203
2247
  ImportStyle,
2204
2248
  ProjectContext,
@@ -2653,6 +2697,7 @@ export type {
2653
2697
  EvidenceValidationContext,
2654
2698
  FileMeta,
2655
2699
  GateEvidence,
2700
+ GateStandIn,
2656
2701
  Phase,
2657
2702
  PhaseStatus,
2658
2703
  PhaseTransition,
@@ -3275,6 +3320,16 @@ export {
3275
3320
  nexusStreamHeadSchema,
3276
3321
  nexusUserKeysSchema,
3277
3322
  } from './nexus-vault.js';
3323
+ // === Independent per-AC PR review (T14122) ===
3324
+ export type {
3325
+ ReviewAnswer,
3326
+ ReviewAuthors,
3327
+ ReviewGap,
3328
+ ReviewJudgement,
3329
+ ReviewPacket,
3330
+ ReviewPacketCriterion,
3331
+ ReviewVerdict,
3332
+ } from './review-verdict.js';
3278
3333
  // === Sync schema version, shared by the change journal and the cloud vault (T13034) ===
3279
3334
  export { SYNC_SCHEMA_VERSION } from './sync-schema.js';
3280
3335
  export { isStorableTaskId, isTaskId, TASK_ID_REGEX, type TaskId } from './task-id.js';
@@ -558,6 +558,22 @@ export const SYNC_WRITE_INVARIANTS: readonly SyncWriteInvariant[] = Object.freez
558
558
  reason:
559
559
  'two devices adopting one legacy doc must mint one uid: the shell takes the legacy birth and sentinel facts, so (uid, birth_fp) converge and receive places the peer copy as a duplicate; a lost local write is refused loudly at the writer',
560
560
  },
561
+ // T14118 — a slug is an alias, not an identity: renaming one moves the
562
+ // live slug on the registry row and the conduit identity row together and
563
+ // records the old slug in the append-only alias table.
564
+ {
565
+ id: 'docs.slug.rename-keeps-alias',
566
+ class: 'identity-layer',
567
+ tables: [
568
+ 'docs_attachments',
569
+ 'conduit_attachments',
570
+ 'docs_wikilinks',
571
+ 'conduit_doc_slug_aliases',
572
+ ],
573
+ sites: [],
574
+ reason:
575
+ 'a rename moves the live slug on docs_attachments and conduit_attachments in one transaction and records the old slug as an alias of the immutable doc uid, so every replica resolves the old and the new slug to one doc',
576
+ },
561
577
  // T13359 (D11165) — decisions-vs-ADRs link table; the natural composite key
562
578
  // makes identical links merge to one row, and the revoke is a one-way latch.
563
579
  // T13597: enforced at merge, not just by the local accessor — 'revoked' is an
@@ -74,6 +74,11 @@ export interface BrainCompactHit {
74
74
  _next?: Record<string, string>;
75
75
  /** Store the hit came from; absent means the project store (T13720). */
76
76
  scope?: MemoryScope;
77
+ /**
78
+ * Current docs linked to this decision (`docs link-decision`), each resolved
79
+ * past any supersession; never a superseded doc (T14110).
80
+ */
81
+ docs?: string[];
77
82
  }
78
83
 
79
84
  // ── SearchBrainCompactParams ────────────────────────────────────────
@@ -158,4 +163,9 @@ export interface SearchBrainCompactResult {
158
163
  total: number;
159
164
  /** Approximate token cost of the returned payload (~chars/4). */
160
165
  tokensEstimated: number;
166
+ /**
167
+ * For each task the query names (or a decision hit's task), the task's
168
+ * current docs; superseded docs are replaced by their current doc (T14110).
169
+ */
170
+ taskDocs?: Array<{ taskId: string; docs: string[] }>;
161
171
  }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Merge closes tasks (T14101, owner decision D11209): the contract of the
3
+ * post-merge sweep that records a merged PR's evidence on the tasks its body
4
+ * names and completes them, with no agent step.
5
+ *
6
+ * A PR body names tasks one per line:
7
+ *
8
+ * - `Closes: T#### AC1,AC2` binds those criteria and completes the task once
9
+ * the criteria every merged PR bound cover all of the task's criteria.
10
+ * - `Refs: T#### AC1` binds those criteria from a PR that delivers part of a
11
+ * task, and leaves it open.
12
+ *
13
+ * @task T14101
14
+ */
15
+
16
+ import type { MergedCleanupResult } from './operations/worktree.js';
17
+
18
+ /** One `Closes:` / `Refs:` line of a PR body. */
19
+ export interface MergeDirective {
20
+ /** `closes` completes the task; `refs` only binds criteria. */
21
+ kind: 'closes' | 'refs';
22
+ /** The task the line names. */
23
+ taskId: string;
24
+ /** Criterion aliases (`AC1`, …) the PR delivers, in the order written. */
25
+ acs: string[];
26
+ }
27
+
28
+ /**
29
+ * What a merged PR says, judged without a task store (the CI job's receipt):
30
+ * its lines, whether its required checks were green on its exact head, and
31
+ * why it closes nothing when it does not.
32
+ */
33
+ export interface MergeReceipt {
34
+ /** PR number. */
35
+ prNumber: number;
36
+ /** The merge commit, or null when GitHub reports none. */
37
+ mergeSha: string | null;
38
+ /** The PR's final head. */
39
+ headSha: string | null;
40
+ /** The parsed lines. */
41
+ directives: MergeDirective[];
42
+ /** Malformed `Closes:` / `Refs:` lines, each with the fix. */
43
+ errors: string[];
44
+ /** Whether every required check passed on the PR's head. */
45
+ ciGreen: boolean;
46
+ /** Why CI does not count as green, when it does not. */
47
+ ciReason?: string;
48
+ /** Why the PR closes nothing at all, when it closes nothing. */
49
+ closesNothing?: string;
50
+ }
51
+
52
+ /** What the sweep did, or would do under `dryRun`, for one task of one PR. */
53
+ export interface MergeTaskOutcome {
54
+ /** The task. */
55
+ taskId: string;
56
+ /**
57
+ * `closed`: evidence recorded and the task completed. `bound`: a `Refs:`
58
+ * line's criteria recorded, the task left open. `already`: an earlier sweep
59
+ * handled this PR for the task, or the task was already done. `refused`:
60
+ * nothing recorded; `reason` says why. `waiting`: the merge commit's own CI
61
+ * run was cancelled, skipped or has not finished and nothing stands in yet;
62
+ * the PR is re-judged every sweep.
63
+ */
64
+ action: 'closed' | 'bound' | 'already' | 'refused' | 'waiting';
65
+ /** The criteria the line named. */
66
+ acs: string[];
67
+ /** Why, for `refused` and `already`; for a dry run of an open PR, what would happen. */
68
+ reason?: string;
69
+ }
70
+
71
+ /** The sweep's result for one merged PR. */
72
+ export interface MergePrOutcome {
73
+ /** PR number. */
74
+ prNumber: number;
75
+ /** Its merge commit. */
76
+ mergeSha: string | null;
77
+ /** One entry per task the PR names. */
78
+ outcomes: MergeTaskOutcome[];
79
+ /** Why nothing was attempted for this PR (no lines, malformed lines, CI not green). */
80
+ closesNothing?: string;
81
+ }
82
+
83
+ /** Options of `sweepMergedClosures`. */
84
+ export interface MergeSweepOptions {
85
+ /** Report what would be recorded; write, complete and comment nothing. */
86
+ dryRun?: boolean;
87
+ /** Ignore the throttle (`cleo release` always passes it). */
88
+ force?: boolean;
89
+ /** Only PRs merged at or after this ISO time (default: the last sweep's high-water mark). */
90
+ since?: string;
91
+ /** Only these PRs (implies `force`). */
92
+ prNumbers?: number[];
93
+ }
94
+
95
+ /** What the sweep did with open `macos-regression` issues (T14104). */
96
+ export interface MacosRegressionSync {
97
+ /** Tasks created for newly seen issues (under `dryRun`: issue URLs that would get one). */
98
+ created: string[];
99
+ /** Issue URLs closed because their task is done. */
100
+ closedIssues: string[];
101
+ /** Why the issues were not read. */
102
+ skippedReason?: string;
103
+ }
104
+
105
+ /** The newest release's provenance reconcile, run by the sweep (T14102). */
106
+ export interface MergeSweepReconcile {
107
+ /** The release version, or null when there is none. */
108
+ version: string | null;
109
+ /** What happened. */
110
+ action: 'reconciled' | 'already' | 'unpublished' | 'none' | 'failed';
111
+ /** Why, when it did not reconcile. */
112
+ reason?: string;
113
+ }
114
+
115
+ /** The other upkeep one sweep runs after the closures, inside the same claim. */
116
+ export interface MergeSweepHooks {
117
+ /** Task ids flagged `docs-update` for docs a merged change left stale (T14111). */
118
+ docsRot?: string[];
119
+ /** `macos-regression` issues turned into tasks, and closed when their task is done (T14104). */
120
+ macosRegression?: MacosRegressionSync;
121
+ /** The newest release's reconcile (T14102); skipped under `dryRun`. */
122
+ reconcile?: MergeSweepReconcile;
123
+ /**
124
+ * Worktrees, branches and nested copies merged work left behind, removed
125
+ * per applied PR (T14142). Off unless `CLEO_MERGE_SWEEP_CLEANUP=1`.
126
+ */
127
+ cleanup?: MergedCleanupResult[];
128
+ }
129
+
130
+ /** Result of `sweepMergedClosures` / `cleo done --from-merge`. */
131
+ export interface MergeSweepResult {
132
+ /** False when the sweep did not run: throttled, `gh` unavailable, or offline. */
133
+ swept: boolean;
134
+ /** Why it did not run. */
135
+ skippedReason?: string;
136
+ /** Whether this was a dry run. */
137
+ dryRun: boolean;
138
+ /** Each merged PR considered, oldest merge first. */
139
+ prs: MergePrOutcome[];
140
+ /** Tasks completed (under `dryRun`: that would be). */
141
+ closed: string[];
142
+ /** Tasks with criteria bound by `Refs:`, left open. */
143
+ bound: string[];
144
+ /** Tasks nothing was recorded for, with the reason. */
145
+ refused: Array<{ taskId: string; prNumber: number; reason: string }>;
146
+ /** Task outcomes waiting on their merge commit's CI (re-judged every sweep). */
147
+ waiting: number;
148
+ /** Cancelled or skipped merge-commit CI runs this sweep reran (once per merge commit, ever). */
149
+ reran: number;
150
+ /**
151
+ * Set when more PRs merged than one sweep's page budget reads: `read` PRs
152
+ * were processed, and those merged in `[unreadSince, unreadBefore]` are read
153
+ * first by the next sweep (never skipped).
154
+ */
155
+ truncated?: { read: number; unreadSince: string; unreadBefore: string };
156
+ /**
157
+ * The pre-merge check of open PRs carrying Closes:/Refs: lines (T14122):
158
+ * the `cleo/merge-close` status each head got, and why when it failed.
159
+ */
160
+ openChecks?: Array<{
161
+ prNumber: number;
162
+ headSha: string;
163
+ state: 'success' | 'failure';
164
+ description: string;
165
+ }>;
166
+ /** The upkeep run in the same sweep, when any ran. */
167
+ hooks?: MergeSweepHooks;
168
+ }