@cleocode/contracts 2026.10.27 → 2026.11.1-canary.2

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 (102) hide show
  1. package/dist/.source-stamp.json +4 -0
  2. package/dist/__tests__/acceptance-gate.test.js +15 -3
  3. package/dist/__tests__/acceptance-gate.test.js.map +1 -1
  4. package/dist/__tests__/evidence-atom-schema.test.js +3 -1
  5. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  6. package/dist/__tests__/memory-wire-shapes.test.js.map +1 -1
  7. package/dist/acceptance-gate-schema.d.ts +25 -0
  8. package/dist/acceptance-gate-schema.d.ts.map +1 -1
  9. package/dist/acceptance-gate-schema.js +5 -0
  10. package/dist/acceptance-gate-schema.js.map +1 -1
  11. package/dist/acceptance-gate.d.ts +8 -0
  12. package/dist/acceptance-gate.d.ts.map +1 -1
  13. package/dist/data-accessor.d.ts +7 -0
  14. package/dist/data-accessor.d.ts.map +1 -1
  15. package/dist/data-accessor.js.map +1 -1
  16. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  17. package/dist/dispatch/operations-registry.js +198 -0
  18. package/dist/dispatch/operations-registry.js.map +1 -1
  19. package/dist/docs/modernize.d.ts +409 -0
  20. package/dist/docs/modernize.d.ts.map +1 -0
  21. package/dist/docs/modernize.js +129 -0
  22. package/dist/docs/modernize.js.map +1 -0
  23. package/dist/docs-taxonomy.d.ts +6 -0
  24. package/dist/docs-taxonomy.d.ts.map +1 -1
  25. package/dist/docs-taxonomy.js +38 -1
  26. package/dist/docs-taxonomy.js.map +1 -1
  27. package/dist/done-plan.d.ts +27 -2
  28. package/dist/done-plan.d.ts.map +1 -1
  29. package/dist/evidence-atom-schema.d.ts +47 -1
  30. package/dist/evidence-atom-schema.d.ts.map +1 -1
  31. package/dist/evidence-atom-schema.js +83 -0
  32. package/dist/evidence-atom-schema.js.map +1 -1
  33. package/dist/exit-codes.d.ts +41 -0
  34. package/dist/exit-codes.d.ts.map +1 -1
  35. package/dist/exit-codes.js +41 -0
  36. package/dist/exit-codes.js.map +1 -1
  37. package/dist/index.d.ts +7 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/invariants/sync-write-invariants.d.ts.map +1 -1
  42. package/dist/invariants/sync-write-invariants.js +15 -0
  43. package/dist/invariants/sync-write-invariants.js.map +1 -1
  44. package/dist/memory/search.d.ts +13 -0
  45. package/dist/memory/search.d.ts.map +1 -1
  46. package/dist/merge-close.d.ts +167 -0
  47. package/dist/merge-close.d.ts.map +1 -0
  48. package/dist/merge-close.js +16 -0
  49. package/dist/merge-close.js.map +1 -0
  50. package/dist/operations/admin.d.ts +6 -0
  51. package/dist/operations/admin.d.ts.map +1 -1
  52. package/dist/operations/docs.d.ts +174 -3
  53. package/dist/operations/docs.d.ts.map +1 -1
  54. package/dist/operations/docs.js.map +1 -1
  55. package/dist/operations/worktree.d.ts +63 -0
  56. package/dist/operations/worktree.d.ts.map +1 -1
  57. package/dist/project-context.d.ts +31 -0
  58. package/dist/project-context.d.ts.map +1 -1
  59. package/dist/release/authorization.d.ts +46 -0
  60. package/dist/release/authorization.d.ts.map +1 -0
  61. package/dist/release/authorization.js +10 -0
  62. package/dist/release/authorization.js.map +1 -0
  63. package/dist/review-verdict.d.ts +125 -0
  64. package/dist/review-verdict.d.ts.map +1 -0
  65. package/dist/review-verdict.js +14 -0
  66. package/dist/review-verdict.js.map +1 -0
  67. package/dist/session-journal.d.ts.map +1 -1
  68. package/dist/session-journal.js +7 -1
  69. package/dist/session-journal.js.map +1 -1
  70. package/dist/task-record.d.ts +6 -1
  71. package/dist/task-record.d.ts.map +1 -1
  72. package/dist/task.d.ts +130 -3
  73. package/dist/task.d.ts.map +1 -1
  74. package/package.json +2 -2
  75. package/schemas/acceptance-gate.schema.json +24 -0
  76. package/schemas/task.schema.json +24 -0
  77. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  78. package/src/__tests__/acceptance-gate.test.ts +16 -3
  79. package/src/__tests__/evidence-atom-schema.test.ts +3 -1
  80. package/src/__tests__/memory-wire-shapes.test.ts +2 -0
  81. package/src/acceptance-gate-schema.ts +5 -0
  82. package/src/acceptance-gate.ts +9 -0
  83. package/src/data-accessor.ts +8 -0
  84. package/src/dispatch/operations-registry.ts +207 -0
  85. package/src/docs/modernize.ts +376 -0
  86. package/src/docs-taxonomy.ts +44 -1
  87. package/src/done-plan.ts +27 -2
  88. package/src/evidence-atom-schema.ts +97 -0
  89. package/src/exit-codes.ts +41 -0
  90. package/src/index.ts +51 -0
  91. package/src/invariants/sync-write-invariants.ts +16 -0
  92. package/src/memory/search.ts +10 -0
  93. package/src/merge-close.ts +168 -0
  94. package/src/operations/admin.ts +6 -0
  95. package/src/operations/docs.ts +180 -4
  96. package/src/operations/worktree.ts +66 -0
  97. package/src/project-context.ts +33 -0
  98. package/src/release/authorization.ts +48 -0
  99. package/src/review-verdict.ts +122 -0
  100. package/src/session-journal.ts +7 -1
  101. package/src/task-record.ts +6 -1
  102. package/src/task.ts +124 -3
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,31 @@ 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
+ DocsModernizeOpKind,
432
+ DocsModernizeOpPrior,
433
+ DocsModernizeOpState,
434
+ DocsModernizeOverride,
435
+ DocsModernizeOverrides,
436
+ DocsModernizePlan,
437
+ DocsModernizePlanRow,
438
+ DocsModernizeReceipt,
439
+ DocsModernizeReceiptOp,
440
+ DocsModernizeStoreRow,
441
+ } from './docs/modernize.js';
442
+ export {
443
+ DOCS_MODERNIZE_DISPOSITIONS,
444
+ DOCS_MODERNIZE_OPS,
445
+ docsModernizeJournalLineSchema,
446
+ docsModernizeOverridesSchema,
447
+ docsModernizePlanSchema,
448
+ docsModernizeReceiptSchema,
449
+ } from './docs/modernize.js';
425
450
  // === Docs Provenance Graph (T10166 / Saga T9855 / E12) ===
426
451
  export type {
427
452
  DocLifecycleStatus,
@@ -1222,6 +1247,17 @@ export type {
1222
1247
  PatternRecord,
1223
1248
  SessionSummary,
1224
1249
  } from './memory.js';
1250
+ export type {
1251
+ MacosRegressionSync,
1252
+ MergeDirective,
1253
+ MergePrOutcome,
1254
+ MergeReceipt,
1255
+ MergeSweepHooks,
1256
+ MergeSweepOptions,
1257
+ MergeSweepReconcile,
1258
+ MergeSweepResult,
1259
+ MergeTaskOutcome,
1260
+ } from './merge-close.js';
1225
1261
  export type {
1226
1262
  MigrationEnumDrift,
1227
1263
  MigrationForeignKeyViolation,
@@ -2094,6 +2130,9 @@ export type {
2094
2130
  ListWorktreesOptions,
2095
2131
  ListWorktreesOpts,
2096
2132
  ListWorktreesResult,
2133
+ MergedCleanupItem,
2134
+ MergedCleanupOpts,
2135
+ MergedCleanupResult,
2097
2136
  PrunedWorktreeOutcome,
2098
2137
  PruneOrphanedWorktreesOpts,
2099
2138
  PruneOrphanedWorktreesResult,
@@ -2199,6 +2238,7 @@ export { PS_STABLE_ENV } from './process-probe.js';
2199
2238
  // === Project Context (ecosystem detection types) ===
2200
2239
  export type {
2201
2240
  EcosystemHint,
2241
+ EvidenceRepositoryDeclaration,
2202
2242
  FileNamingConvention,
2203
2243
  ImportStyle,
2204
2244
  ProjectContext,
@@ -2653,6 +2693,7 @@ export type {
2653
2693
  EvidenceValidationContext,
2654
2694
  FileMeta,
2655
2695
  GateEvidence,
2696
+ GateStandIn,
2656
2697
  Phase,
2657
2698
  PhaseStatus,
2658
2699
  PhaseTransition,
@@ -3275,6 +3316,16 @@ export {
3275
3316
  nexusStreamHeadSchema,
3276
3317
  nexusUserKeysSchema,
3277
3318
  } from './nexus-vault.js';
3319
+ // === Independent per-AC PR review (T14122) ===
3320
+ export type {
3321
+ ReviewAnswer,
3322
+ ReviewAuthors,
3323
+ ReviewGap,
3324
+ ReviewJudgement,
3325
+ ReviewPacket,
3326
+ ReviewPacketCriterion,
3327
+ ReviewVerdict,
3328
+ } from './review-verdict.js';
3278
3329
  // === Sync schema version, shared by the change journal and the cloud vault (T13034) ===
3279
3330
  export { SYNC_SCHEMA_VERSION } from './sync-schema.js';
3280
3331
  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
+ }
@@ -1662,6 +1662,12 @@ export interface AdminBackupMutateParams {
1662
1662
  confirmOwnerStore?: boolean;
1663
1663
  /** Create: also back up the global store (`<CLEO_HOME>/cleo.db`, T13245). */
1664
1664
  includeGlobal?: boolean;
1665
+ /**
1666
+ * Create: write the backup into this directory (the global copy under
1667
+ * `<destination>/global`) instead of the rotating backup dirs; it never
1668
+ * rotates or is rotated (T13209).
1669
+ */
1670
+ destination?: string;
1665
1671
  /** Restore by id: `global` restores a backup of the global store (T13245). */
1666
1672
  scope?: 'project' | 'global';
1667
1673
  }
@@ -32,6 +32,8 @@
32
32
 
33
33
  import { z } from 'zod';
34
34
  import type { AttachmentKind } from '../attachment.js';
35
+ import type { TaskPopulation } from '../data-accessor.js';
36
+ import type { DocsModernizePlan, DocsModernizeReceipt } from '../docs/modernize.js';
35
37
  import { BUILTIN_DOC_KIND_VALUES, type BuiltinDocKind } from '../docs-taxonomy.js';
36
38
  import type {
37
39
  BackgroundJobStatus,
@@ -316,6 +318,12 @@ export interface DocsListResult {
316
318
  * @task T9792
317
319
  */
318
320
  totalCount?: number;
321
+ /**
322
+ * How many attachments matched, how many this response returns, whether it
323
+ * left any out, and the window applied (`null`: every match, `--limit 0`).
324
+ * The same shape `cleo list` reports (T14115).
325
+ */
326
+ population: Pick<TaskPopulation, 'matched' | 'returned' | 'truncated' | 'limit'>;
319
327
  /**
320
328
  * Effective limit applied to this response. Mirrored from the request
321
329
  * (or {@link DOCS_LIST_DEFAULT_LIMIT} when the request did not set one)
@@ -458,6 +466,37 @@ export interface DocsFetchResult {
458
466
  * ratifies.
459
467
  */
460
468
  linkedDecisions?: string[];
469
+ /**
470
+ * Where this doc sits in its supersession chain and what it serves
471
+ * (T14110). Present for slugged docs.
472
+ */
473
+ lineage?: DocsLineage;
474
+ }
475
+
476
+ /**
477
+ * A doc's supersession chain and the work it serves (T14110). Built from the
478
+ * existing `supersedes`/`superseded_by` pointers, task refs and
479
+ * `related_tasks`; nothing here is stored separately.
480
+ */
481
+ export interface DocsLineage {
482
+ /** The doc's lifecycle status (`superseded` when replaced). */
483
+ readonly lifecycleStatus: string;
484
+ /** Slug of the doc this one replaced, when any. */
485
+ readonly supersedes: string | null;
486
+ /** Slug of the doc that replaced this one, when any. */
487
+ readonly supersededBy: string | null;
488
+ /**
489
+ * Slug of the current doc at the end of the `superseded_by` chain: this
490
+ * doc's own slug when it is current. Read this one instead.
491
+ */
492
+ readonly currentSlug: string;
493
+ /** Task ids the doc serves: its task owners and the tasks its body names. */
494
+ readonly servesTasks: readonly string[];
495
+ /**
496
+ * Decisions this doc supersedes: the `supersedes` of each decision it links
497
+ * (`docs link-decision`). Absent when it links none that supersede another.
498
+ */
499
+ readonly supersedesDecisions?: readonly string[];
461
500
  }
462
501
 
463
502
  // --------------------------------------------------------------------------
@@ -570,6 +609,12 @@ export interface DocsAddParams {
570
609
  * @saga T9855
571
610
  */
572
611
  strict?: boolean;
612
+ /**
613
+ * Why this doc is new although it resembles an existing one (T14109). A
614
+ * likely duplicate is refused unless this states the reason (at least 15
615
+ * characters); the reason is recorded in `.cleo/audit/docs-new-because.jsonl`.
616
+ */
617
+ newBecause?: string;
573
618
  }
574
619
 
575
620
  /** Canonical attachment and owner whose optional projections may be prepared. */
@@ -1121,7 +1166,8 @@ export interface DocsDoctorDiagnostic {
1121
1166
  | 'legacy-store-present'
1122
1167
  | 'legacy-blob-coverage'
1123
1168
  | 'stale-drafts'
1124
- | 'conduit-history-gaps';
1169
+ | 'conduit-history-gaps'
1170
+ | 'publication-ledger-drift';
1125
1171
  /** `error` should be fixed promptly; `info` is observational. */
1126
1172
  severity: 'info' | 'warn' | 'error';
1127
1173
  /** Total offenders found (may exceed `items.length`). */
@@ -1164,7 +1210,7 @@ export interface DocsDoctorResult {
1164
1210
  olderThanDays: number;
1165
1211
  /** Backup receipt that authorised this run (null on dry runs). */
1166
1212
  backupReceiptPath: string | null;
1167
- /** All eight diagnostics, in declaration order. */
1213
+ /** All nine diagnostics, in declaration order. */
1168
1214
  diagnostics: readonly DocsDoctorDiagnostic[];
1169
1215
  /** Repair plan (dry run) or executed repairs (apply). */
1170
1216
  repairs: readonly DocsDoctorRepairAction[];
@@ -1242,6 +1288,87 @@ export interface DocsStatsResult {
1242
1288
  readonly summary: string;
1243
1289
  }
1244
1290
 
1291
+ /**
1292
+ * Parameters for `docs.rot` — the docs-rot report (T14111).
1293
+ *
1294
+ * Read-only and git-only: it reads tracked markdown and git history, never
1295
+ * the docs store, so it runs the same in CI and locally.
1296
+ *
1297
+ * @task T14111 (Epic T14106)
1298
+ */
1299
+ export interface DocsRotParams {
1300
+ /**
1301
+ * Git revision range `<base>..<head>` of a merged change. When set, the
1302
+ * report also lists the docs whose `covers:` globs match a file the range
1303
+ * changed (and that the range did not update itself).
1304
+ */
1305
+ readonly changed?: string;
1306
+ }
1307
+
1308
+ /**
1309
+ * A doc whose declared `covers:` code changed without the doc changing.
1310
+ *
1311
+ * @task T14111
1312
+ */
1313
+ export interface DocsRotCoverageHit {
1314
+ /** Repo-relative path of the doc. */
1315
+ readonly doc: string;
1316
+ /** Covered files that changed (sorted, repo-relative). */
1317
+ readonly files: readonly string[];
1318
+ }
1319
+
1320
+ /**
1321
+ * A group of docs that duplicate each other: identical bodies, or titles
1322
+ * close enough that one is a fork of the other.
1323
+ *
1324
+ * @task T14111
1325
+ */
1326
+ export interface DocsRotDuplicate {
1327
+ /** Why the docs were grouped: `content` (same body) or `title`. */
1328
+ readonly reason: 'content' | 'title';
1329
+ /** Repo-relative paths of the duplicates (sorted). */
1330
+ readonly docs: readonly string[];
1331
+ }
1332
+
1333
+ /**
1334
+ * A doc nothing ties to the repo: a `covers:` glob that matches no tracked
1335
+ * file, or a `docs/` file no other tracked file references.
1336
+ *
1337
+ * @task T14111
1338
+ */
1339
+ export interface DocsRotOrphan {
1340
+ /** Repo-relative path of the doc. */
1341
+ readonly doc: string;
1342
+ /** `dead-covers` (globs listed in `globs`) or `unreferenced`. */
1343
+ readonly reason: 'dead-covers' | 'unreferenced';
1344
+ /** The covers globs that match nothing (only for `dead-covers`). */
1345
+ readonly globs?: readonly string[];
1346
+ }
1347
+
1348
+ /**
1349
+ * Result of `docs.rot` — stale, duplicate and orphaned docs (T14111).
1350
+ *
1351
+ * @task T14111
1352
+ */
1353
+ export interface DocsRotResult {
1354
+ /** Tracked markdown docs scanned. */
1355
+ readonly scanned: number;
1356
+ /** Docs that declare `covers:`. */
1357
+ readonly covering: number;
1358
+ /** Docs whose covered code changed after the doc last changed on main. */
1359
+ readonly stale: readonly DocsRotCoverageHit[];
1360
+ /** Duplicate doc groups. */
1361
+ readonly duplicates: readonly DocsRotDuplicate[];
1362
+ /** Orphaned docs. */
1363
+ readonly orphans: readonly DocsRotOrphan[];
1364
+ /** Docs the `changed` range flags for update (empty without `changed`). */
1365
+ readonly flagged: readonly DocsRotCoverageHit[];
1366
+ /** Wall time of the scan in milliseconds. */
1367
+ readonly durationMs: number;
1368
+ /** One-line human summary. */
1369
+ readonly summary: string;
1370
+ }
1371
+
1245
1372
  // ============================================================================
1246
1373
  // Discriminated Union (DocsOps)
1247
1374
  // ============================================================================
@@ -1269,7 +1396,54 @@ export type DocsOps =
1269
1396
  | { op: 'docs.unlink-decision'; params: DocsDecisionLinkParams; result: DocsDecisionLinkResult }
1270
1397
  | { op: 'docs.doctor'; params: DocsDoctorParams; result: DocsDoctorResult }
1271
1398
  | { op: 'docs.migrate'; params: DocsMigrateParams; result: DocsMigrateResult }
1272
- | { op: 'docs.stats'; params: DocsStatsParams; result: DocsStatsResult };
1399
+ | { op: 'docs.modernize'; params: DocsModernizeParams; result: DocsModernizeResult }
1400
+ | { op: 'docs.stats'; params: DocsStatsParams; result: DocsStatsResult }
1401
+ | { op: 'docs.rot'; params: DocsRotParams; result: DocsRotResult };
1402
+
1403
+ /**
1404
+ * Parameters for `docs.modernize` (T14113) — dry run by default.
1405
+ */
1406
+ export interface DocsModernizeParams {
1407
+ /** Folder to modernize, relative to the project root or absolute. Required unless `rollback`. */
1408
+ readonly path?: string;
1409
+ /** Execute the plan; without it the run is a dry run that writes nothing. */
1410
+ readonly apply?: boolean;
1411
+ /** Apply this saved plan JSON verbatim instead of re-planning. */
1412
+ readonly planFile?: string;
1413
+ /** Save the plan JSON here. */
1414
+ readonly out?: string;
1415
+ /** Roll back the apply run with this id. */
1416
+ readonly rollback?: string;
1417
+ /** Extra folder names to skip, comma-separated. */
1418
+ readonly exclude?: string;
1419
+ /** Path of a `DocsModernizeOverrides` JSON file keyed by root-relative path. */
1420
+ readonly overrides?: string;
1421
+ /** Use this kind for every file instead of classifying. */
1422
+ readonly kind?: string;
1423
+ /** Ingest every file into the store (task-owned, slugless) and leave the folder untouched. */
1424
+ readonly storeOnly?: boolean;
1425
+ /** Apply even when the root is not a clean git checkout. */
1426
+ readonly allowDirty?: boolean;
1427
+ /** Modernize the docs already in the store (re-kind, re-slug with aliases, ADR numbers). */
1428
+ readonly store?: boolean;
1429
+ }
1430
+
1431
+ /**
1432
+ * Result of `docs.modernize`: the plan (dry run, apply) and the receipt
1433
+ * (apply, rollback).
1434
+ */
1435
+ export interface DocsModernizeResult {
1436
+ /** What ran. */
1437
+ readonly mode: 'dry-run' | 'apply' | 'rollback';
1438
+ /** The plan, or null for a rollback. */
1439
+ readonly plan: DocsModernizePlan | null;
1440
+ /** Where the plan was saved, when `out` was given. */
1441
+ readonly planPath: string | null;
1442
+ /** The receipt, or null for a dry run. */
1443
+ readonly receipt: DocsModernizeReceipt | null;
1444
+ /** One-line human summary. */
1445
+ readonly summary: string;
1446
+ }
1273
1447
 
1274
1448
  /**
1275
1449
  * Parameters for `docs.migrate` — the T13360 legacy docs migration (spec §5).
@@ -1336,4 +1510,6 @@ export type DocsOp =
1336
1510
  | 'docs.unlink-decision'
1337
1511
  | 'docs.doctor'
1338
1512
  | 'docs.migrate'
1339
- | 'docs.stats';
1513
+ | 'docs.modernize'
1514
+ | 'docs.stats'
1515
+ | 'docs.rot';
@@ -1098,3 +1098,69 @@ export interface AdoptWorktreeResult {
1098
1098
  */
1099
1099
  projectInfo: 'seeded' | 'present' | 'skipped';
1100
1100
  }
1101
+
1102
+ /**
1103
+ * Options for `cleanupMergedWork` (T14142): remove what merged work left
1104
+ * behind, after proving each item redundant.
1105
+ *
1106
+ * @task T14142
1107
+ */
1108
+ export interface MergedCleanupOpts {
1109
+ /** Project root (the main checkout; its hash names the canonical worktree root). */
1110
+ projectRoot: string;
1111
+ /**
1112
+ * Tasks whose work merged (the sweep's merged PR, or a release's tasks).
1113
+ * Either way a task is in scope only while stored `done` or `cancelled`:
1114
+ * an open task (more PRs to come) keeps its worktree and branches.
1115
+ */
1116
+ taskIds?: readonly string[];
1117
+ /** List everything without deleting. Default `true`: deleting is opt-in. */
1118
+ dryRun?: boolean;
1119
+ /** Also delete merged task branches on `origin`. Default `true`. */
1120
+ remote?: boolean;
1121
+ }
1122
+
1123
+ /**
1124
+ * One item the cleanup considered: removed (or, on a dry run, removable), or
1125
+ * kept with the reason.
1126
+ *
1127
+ * @task T14142
1128
+ */
1129
+ export interface MergedCleanupItem {
1130
+ /** Worktree or nested-copy path, or branch name. */
1131
+ target: string;
1132
+ /** Owning task, when the target names one. */
1133
+ taskId: string | null;
1134
+ /** Branch tip (branches only): what a restore pushes back. */
1135
+ sha?: string;
1136
+ /** True when removed (dry run: when it would be removed). */
1137
+ removed: boolean;
1138
+ /** Why it was kept, or what proved it redundant. */
1139
+ reason: string;
1140
+ /** Bytes on disk (worktrees and nested copies; 0 for branches). */
1141
+ bytes: number;
1142
+ }
1143
+
1144
+ /**
1145
+ * Result of `cleanupMergedWork` (T14142).
1146
+ *
1147
+ * @task T14142
1148
+ */
1149
+ export interface MergedCleanupResult {
1150
+ /** Whether nothing was deleted. */
1151
+ dryRun: boolean;
1152
+ /** The default branch merges were proven against (`origin/<name>`). */
1153
+ base: string;
1154
+ /** Canonical task worktrees. */
1155
+ worktrees: MergedCleanupItem[];
1156
+ /** Local `task/*` branches. */
1157
+ localBranches: MergedCleanupItem[];
1158
+ /** `origin` `task/*` branches. */
1159
+ remoteBranches: MergedCleanupItem[];
1160
+ /** Repository copies nested inside worktrees. */
1161
+ nestedCopies: MergedCleanupItem[];
1162
+ /** Bytes removed (dry run: that would be removed). */
1163
+ bytesReclaimed: number;
1164
+ /** Why nothing was considered (the fetch failed, no base branch); absent when it ran. */
1165
+ skippedReason?: string;
1166
+ }