@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
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Docs modernize contract — the plan and receipt of `cleo docs modernize`.
3
+ *
4
+ * `cleo docs modernize <path>` turns a messy docs folder into clean, linked
5
+ * CleoDocs. A dry run produces a {@link DocsModernizePlan}: one
6
+ * {@link DocsModernizePlanRow} per markdown or text file, carrying the kind,
7
+ * the stable slug, the disposition and the reasons (including every System
8
+ * One judgment that was asked). `--apply` executes a plan and writes a
9
+ * {@link DocsModernizeReceipt}; every receipt op records the state it
10
+ * replaced, so `--rollback <runId>` can reverse the run.
11
+ *
12
+ * Types only — the planner and applier live in
13
+ * `packages/core/src/docs/modernize/`.
14
+ *
15
+ * @task T14113
16
+ * @epic T14106
17
+ */
18
+
19
+ import { z } from 'zod';
20
+
21
+ /** What modernize does with one file. */
22
+ export const DOCS_MODERNIZE_DISPOSITIONS = ['keep', 'rename', 'merge-into', 'delete'] as const;
23
+
24
+ /**
25
+ * One of {@link DOCS_MODERNIZE_DISPOSITIONS}.
26
+ *
27
+ * - `keep` — the file already sits at its canonical path; it is ingested as is.
28
+ * - `rename` — the file moves to `<kind folder>/<slug>.md` and is ingested there.
29
+ * - `merge-into` — the file duplicates another; its content folds into the
30
+ * survivor named by `target`, and the file is superseded and removed.
31
+ * - `delete` — the file carries nothing worth keeping (empty, or byte-identical
32
+ * to the survivor); it is superseded by `target` when one exists and removed.
33
+ */
34
+ export type DocsModernizeDisposition = (typeof DOCS_MODERNIZE_DISPOSITIONS)[number];
35
+
36
+ /** Where an acted-on answer came from. */
37
+ export type DocsModernizeJudgmentSource = 'provider' | 'cache' | 'fallback';
38
+
39
+ /** The two System One questions modernize asks. */
40
+ export type DocsModernizeJudgmentQuestion = 'classify' | 'merge';
41
+
42
+ /**
43
+ * One judgment taken for a plan row: the rule answer, the System One answer
44
+ * when one was asked, and which of the two acted.
45
+ */
46
+ export interface DocsModernizeJudgment {
47
+ /** Which question was judged. */
48
+ readonly question: DocsModernizeJudgmentQuestion;
49
+ /** Decision-site id the question was asked through (`docs.modernize.*`). */
50
+ readonly site: string;
51
+ /** Effective site mode (`off`, `shadow` or `on`). */
52
+ readonly mode: 'off' | 'shadow' | 'on';
53
+ /** The rule fallback's answer. */
54
+ readonly ruleAnswer: string;
55
+ /** The System One answer, or null when none came back (off, unreachable, invalid). */
56
+ readonly decidedAnswer: string | null;
57
+ /** Confidence of the System One answer, or null when none came back. */
58
+ readonly decidedConfidence: number | null;
59
+ /** Where the decided answer came from; `fallback` when the rule answered. */
60
+ readonly source: DocsModernizeJudgmentSource;
61
+ /** The answer the plan acts on. */
62
+ readonly acted: string;
63
+ }
64
+
65
+ /** One file's proposal. */
66
+ export interface DocsModernizePlanRow {
67
+ /** Path relative to the modernize root, POSIX separators. */
68
+ readonly path: string;
69
+ /** SHA-256 of the file bytes, hex. */
70
+ readonly sha256: string;
71
+ /** Taxonomy kind (a `DocKindRegistry` kind). */
72
+ readonly kind: string;
73
+ /** Stable slug: no task, session or date tokens. */
74
+ readonly slug: string;
75
+ /** Human title (first H1, else derived from the slug). */
76
+ readonly title: string;
77
+ /** What happens to the file. */
78
+ readonly disposition: DocsModernizeDisposition;
79
+ /**
80
+ * For `keep` / `rename`: the canonical path relative to the root.
81
+ * For `merge-into` / `delete`: the survivor's slug, or null when there is none.
82
+ */
83
+ readonly target: string | null;
84
+ /** Why, one line each. */
85
+ readonly reasons: readonly string[];
86
+ /** Every System One question asked for this row. */
87
+ readonly judgments: readonly DocsModernizeJudgment[];
88
+ /** Task (`T####`) and decision (`D####`) ids the old path named; kept as links, not in the slug. */
89
+ readonly ownerTasks: readonly string[];
90
+ /** The kind's folder the file belongs in (`''` for the root). */
91
+ readonly category: string;
92
+ /** Tracked files outside the root that cite the old path (listed, never rewritten). */
93
+ readonly refs: readonly string[];
94
+ /** True when the row came from an override file rather than the rules. */
95
+ readonly overridden: boolean;
96
+ /** Code paths this doc documents (from the override; written to front matter by the caller). */
97
+ readonly covers: readonly string[];
98
+ /** True when the old name broke the slug rule (task, session or date tokens) and the new one does not. */
99
+ readonly baselineRemove: boolean;
100
+ }
101
+
102
+ /** One reviewer decision for a path; it wins over the rules and System One. */
103
+ export interface DocsModernizeOverride {
104
+ /** What happens to the file. */
105
+ readonly disposition: DocsModernizeDisposition;
106
+ /** Canonical path (`keep`/`rename`) or survivor slug (`merge-into`/`delete`). */
107
+ readonly target?: string;
108
+ /** Why; recorded on the plan row. */
109
+ readonly reason: string;
110
+ /** Kind to use instead of the classified one. */
111
+ readonly kind?: string;
112
+ /** Slug to use instead of the derived one. */
113
+ readonly slug?: string;
114
+ /** Code paths this doc documents, copied onto the plan row. */
115
+ readonly covers?: readonly string[];
116
+ }
117
+
118
+ /** `--overrides <file>`: decisions keyed by path relative to the modernize root. */
119
+ export type DocsModernizeOverrides = Readonly<Record<string, DocsModernizeOverride>>;
120
+
121
+ /** A dry-run proposal for a whole tree. */
122
+ export interface DocsModernizePlan {
123
+ /** Absolute modernize root. */
124
+ readonly root: string;
125
+ /** ISO 8601 time the plan was made. */
126
+ readonly generatedAt: string;
127
+ /** One row per scanned file, sorted by path. */
128
+ readonly rows: readonly DocsModernizePlanRow[];
129
+ /** Files counted per disposition. */
130
+ readonly counts: Readonly<Record<DocsModernizeDisposition, number>>;
131
+ /**
132
+ * `tree` reorganises the folder; `store-only` ingests every file into the
133
+ * store (task-owned, slugless) and leaves the folder untouched; `store`
134
+ * re-kinds and re-slugs the rows already in the store (`storeRows`, T14118).
135
+ */
136
+ readonly mode: 'tree' | 'store-only' | 'store';
137
+ /** Store-mode rows (`mode: 'store'`); absent or empty otherwise. */
138
+ readonly storeRows?: readonly DocsModernizeStoreRow[];
139
+ }
140
+
141
+ /**
142
+ * One existing store doc in a `--store` plan (T14118): its kind and slug now,
143
+ * and what modernize proposes. An unchanged field repeats the current value.
144
+ */
145
+ export interface DocsModernizeStoreRow {
146
+ /** Attachment row id. */
147
+ readonly attachmentId: string;
148
+ /** Content hash. */
149
+ readonly sha256: string;
150
+ /** Current slug (null for a slugless row). */
151
+ readonly slug: string | null;
152
+ /** Proposed slug; the current one stays resolvable as an alias. */
153
+ readonly newSlug: string | null;
154
+ /** Current kind (null when untyped). */
155
+ readonly kind: string | null;
156
+ /** Proposed kind. */
157
+ readonly newKind: string;
158
+ /** Task ids the old slug named; linked to the doc as task refs. */
159
+ readonly ownerTasks: readonly string[];
160
+ /** Why, one line each. */
161
+ readonly reasons: readonly string[];
162
+ /** Every System One question asked for this row. */
163
+ readonly judgments: readonly DocsModernizeJudgment[];
164
+ }
165
+
166
+ /** The reversible steps an apply run takes. */
167
+ export const DOCS_MODERNIZE_OPS = [
168
+ 'ingest',
169
+ 'merge',
170
+ 'fold',
171
+ 'supersede',
172
+ 'write-file',
173
+ 'remove-file',
174
+ 'link',
175
+ 'rekind',
176
+ 'reslug',
177
+ 'link-task',
178
+ ] as const;
179
+
180
+ /** One of {@link DOCS_MODERNIZE_OPS}. */
181
+ export type DocsModernizeOpKind = (typeof DOCS_MODERNIZE_OPS)[number];
182
+
183
+ /**
184
+ * Write-ahead state of a receipt op: written `pending` before the step acts,
185
+ * `done` after it, `reversed` once a rollback has undone it.
186
+ */
187
+ export type DocsModernizeOpState = 'pending' | 'done' | 'reversed';
188
+
189
+ /** Store values a step replaced, so a rollback restores them exactly. */
190
+ export interface DocsModernizeOpPrior {
191
+ /** `supersede`: the older row's lifecycle status before the step. */
192
+ readonly lifecycleStatus?: string | null;
193
+ /** `supersede`: the older row's `superseded_by` before the step. */
194
+ readonly supersededBy?: string | null;
195
+ /** `supersede`: the newer row's `supersedes` before the step. */
196
+ readonly supersedes?: string | null;
197
+ /** `merge`: the row that held the slug before the update. */
198
+ readonly previousAttachmentId?: string | null;
199
+ /** `merge`: the row the update moved the slug to. */
200
+ readonly newAttachmentId?: string | null;
201
+ /** `link`: the slug's `links-to` targets before the step. */
202
+ readonly linkTargets?: readonly string[];
203
+ /** `rekind`: the row's kind before the step. */
204
+ readonly kind?: string | null;
205
+ }
206
+
207
+ /**
208
+ * One step with the state it replaced.
209
+ *
210
+ * - `ingest` — a doc stored under `slug` (or slugless) for `ownerId`; `attachmentId` is the row.
211
+ * - `merge` — the stored doc `slug` was updated with merged content; `prior` names both rows.
212
+ * - `fold` — the file `path` was folded into the survivor `target` (content went there).
213
+ * - `supersede` — `slug` was superseded by `target`; `prior` holds the replaced pointers.
214
+ * - `write-file` — `path` was written; `beforeSha256` is the old bytes, or null when it was new.
215
+ * - `remove-file` — `path` was removed; `beforeSha256` names the bytes kept for rollback.
216
+ * - `link` — the `links-to` edges leaving `slug` were set; `prior.linkTargets` holds the old set.
217
+ * - `rekind` — the row `attachmentId` changed kind; `prior.kind` holds the old one (T14118).
218
+ * - `reslug` — `slug` was renamed to `target`; `slug` stays an alias (T14118).
219
+ * - `link-task` — the row `attachmentId` gained a ref to task `ownerId` (T14118).
220
+ */
221
+ export interface DocsModernizeReceiptOp {
222
+ /** Step kind. */
223
+ readonly op: DocsModernizeOpKind;
224
+ /** Write-ahead state. */
225
+ readonly state: DocsModernizeOpState;
226
+ /** Doc slug the step touched, when any. */
227
+ readonly slug: string | null;
228
+ /** File path relative to the root, when any. */
229
+ readonly path: string | null;
230
+ /** The other end: survivor slug for `supersede` and `fold`. */
231
+ readonly target: string | null;
232
+ /** Attachment row created or touched by the step, when any. */
233
+ readonly attachmentId: string | null;
234
+ /** Owner the `ingest` ref was made for (`T####` or the project sentinel); rollback drops that ref. */
235
+ readonly ownerId: string | null;
236
+ /** Content the step replaced or removed, when any. */
237
+ readonly beforeSha256: string | null;
238
+ /** Content the step wrote, when any. */
239
+ readonly afterSha256: string | null;
240
+ /** Store values the step replaced. */
241
+ readonly prior?: DocsModernizeOpPrior;
242
+ }
243
+
244
+ /** The record of one apply run; the input to rollback. */
245
+ export interface DocsModernizeReceipt {
246
+ /** Run id (`dm-<timestamp>`); names the receipt file. */
247
+ readonly runId: string;
248
+ /** Absolute modernize root. */
249
+ readonly root: string;
250
+ /** ISO 8601 time the run started. */
251
+ readonly appliedAt: string;
252
+ /** SHA-256 of the canonical JSON of the plan that was applied. */
253
+ readonly planSha256: string;
254
+ /** Steps in the order they ran. */
255
+ readonly ops: readonly DocsModernizeReceiptOp[];
256
+ /** Wikilink edges in the graph after the run. */
257
+ readonly edgeCount: number;
258
+ /** Every scanned file whose bytes were hash-verified in the blob store, before any removal. */
259
+ readonly blobs: readonly { readonly path: string; readonly sha256: string }[];
260
+ /** Files moved, relative to the root (stage exactly these). */
261
+ readonly moves: readonly { readonly from: string; readonly to: string }[];
262
+ /** Files removed, relative to the root. */
263
+ readonly deleted: readonly string[];
264
+ /** ISO 8601 time the run was rolled back, or null. */
265
+ readonly rolledBackAt: string | null;
266
+ /** Wall time of the apply, in ms (store mode). */
267
+ readonly durationMs?: number;
268
+ }
269
+
270
+ // ─── Schemas (a plan or receipt read from disk is untrusted input) ───────────
271
+
272
+ const judgmentSchema = z.object({
273
+ question: z.enum(['classify', 'merge']),
274
+ site: z.string(),
275
+ mode: z.enum(['off', 'shadow', 'on']),
276
+ ruleAnswer: z.string(),
277
+ decidedAnswer: z.string().nullable(),
278
+ decidedConfidence: z.number().nullable(),
279
+ source: z.enum(['provider', 'cache', 'fallback']),
280
+ acted: z.string(),
281
+ });
282
+
283
+ const relPath = z.string().min(1).max(1024);
284
+
285
+ /** Shape check for a {@link DocsModernizePlan} read from a `--plan` file. */
286
+ export const docsModernizePlanSchema = z.object({
287
+ root: z.string().min(1),
288
+ generatedAt: z.string(),
289
+ mode: z.enum(['tree', 'store-only']),
290
+ counts: z.record(z.enum(DOCS_MODERNIZE_DISPOSITIONS), z.number()),
291
+ rows: z.array(
292
+ z.object({
293
+ path: relPath,
294
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
295
+ kind: z.string().min(1),
296
+ slug: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
297
+ title: z.string(),
298
+ disposition: z.enum(DOCS_MODERNIZE_DISPOSITIONS),
299
+ target: z.string().min(1).max(1024).nullable(),
300
+ reasons: z.array(z.string()),
301
+ judgments: z.array(judgmentSchema),
302
+ ownerTasks: z.array(z.string().regex(/^[TD]\d+$/)),
303
+ category: z.string(),
304
+ refs: z.array(z.string()),
305
+ overridden: z.boolean(),
306
+ covers: z.array(z.string()),
307
+ baselineRemove: z.boolean(),
308
+ }),
309
+ ),
310
+ });
311
+
312
+ /** Shape check for an `--overrides` file ({@link DocsModernizeOverrides}). */
313
+ export const docsModernizeOverridesSchema = z.record(
314
+ relPath,
315
+ z.object({
316
+ disposition: z.enum(DOCS_MODERNIZE_DISPOSITIONS),
317
+ target: z.string().min(1).max(1024).optional(),
318
+ reason: z.string().min(1),
319
+ kind: z.string().min(1).optional(),
320
+ slug: z
321
+ .string()
322
+ .regex(/^[a-z0-9][a-z0-9-]*$/)
323
+ .optional(),
324
+ covers: z.array(z.string()).optional(),
325
+ }),
326
+ );
327
+
328
+ /** Shape check for one receipt op. */
329
+ const receiptOpSchema = z.object({
330
+ op: z.enum(DOCS_MODERNIZE_OPS),
331
+ state: z.enum(['pending', 'done', 'reversed']),
332
+ slug: z.string().nullable(),
333
+ path: relPath.nullable(),
334
+ target: z.string().nullable(),
335
+ attachmentId: z.string().nullable(),
336
+ ownerId: z.string().nullable(),
337
+ beforeSha256: z
338
+ .string()
339
+ .regex(/^[0-9a-f]{64}$/)
340
+ .nullable(),
341
+ afterSha256: z
342
+ .string()
343
+ .regex(/^[0-9a-f]{64}$/)
344
+ .nullable(),
345
+ prior: z
346
+ .object({
347
+ lifecycleStatus: z.string().nullable().optional(),
348
+ supersededBy: z.string().nullable().optional(),
349
+ supersedes: z.string().nullable().optional(),
350
+ previousAttachmentId: z.string().nullable().optional(),
351
+ newAttachmentId: z.string().nullable().optional(),
352
+ linkTargets: z.array(z.string()).optional(),
353
+ kind: z.string().nullable().optional(),
354
+ })
355
+ .optional(),
356
+ });
357
+
358
+ /** Shape check for one line of a run's write-ahead journal (`<runId>.ops.jsonl`). */
359
+ export const docsModernizeJournalLineSchema = receiptOpSchema.extend({
360
+ i: z.number().int().min(0),
361
+ });
362
+
363
+ /** Shape check for a {@link DocsModernizeReceipt} read back for rollback. */
364
+ export const docsModernizeReceiptSchema = z.object({
365
+ runId: z.string().regex(/^dm-[\w-]+$/),
366
+ root: z.string().min(1),
367
+ appliedAt: z.string(),
368
+ planSha256: z.string(),
369
+ edgeCount: z.number(),
370
+ rolledBackAt: z.string().nullable(),
371
+ blobs: z.array(z.object({ path: relPath, sha256: z.string() })),
372
+ moves: z.array(z.object({ from: relPath, to: relPath })),
373
+ deleted: z.array(relPath),
374
+ ops: z.array(receiptOpSchema),
375
+ durationMs: z.number().optional(),
376
+ });
@@ -112,10 +112,16 @@ export interface DocKindMetadata {
112
112
  * @epic T10157
113
113
  */
114
114
  readonly requiredSections?: ReadonlyArray<string>;
115
+ /**
116
+ * When `true`, docs of this kind live only in the CLEO doc store and are
117
+ * never published to a repository mirror (T14107, D11210: session
118
+ * handoffs are exactly the rot published docs must not carry).
119
+ */
120
+ readonly storeOnly?: boolean;
115
121
  }
116
122
 
117
123
  // ---------------------------------------------------------------------------
118
- // Built-in registry (10 canonical kinds)
124
+ // Built-in registry (14 canonical kinds)
119
125
  // ---------------------------------------------------------------------------
120
126
 
121
127
  /**
@@ -167,6 +173,7 @@ export const BUILTIN_DOC_KINDS: ReadonlyArray<DocKindMetadata> = [
167
173
  publishDir: 'docs/handoff',
168
174
  requiresEntityId: false,
169
175
  requiredSections: ['Context', 'State', 'Next-Steps'],
176
+ storeOnly: true,
170
177
  },
171
178
  {
172
179
  kind: 'note',
@@ -225,6 +232,42 @@ export const BUILTIN_DOC_KINDS: ReadonlyArray<DocKindMetadata> = [
225
232
  entityIdPattern: /^t\d+(-.+)?$/,
226
233
  requiredSections: ['Root-Cause', 'Action', 'Schedule', 'Detection'],
227
234
  },
235
+ {
236
+ kind: 'guide',
237
+ label: 'Guide',
238
+ description: 'How-to guide: the steps to get a task done (T14107)',
239
+ defaultOwnerKind: 'project',
240
+ publishDir: 'docs/guide',
241
+ requiresEntityId: false,
242
+ requiredSections: [],
243
+ },
244
+ {
245
+ kind: 'concept',
246
+ label: 'Concept',
247
+ description: 'Explanation of how and why the product works (T14107)',
248
+ defaultOwnerKind: 'project',
249
+ publishDir: 'docs/concept',
250
+ requiresEntityId: false,
251
+ requiredSections: [],
252
+ },
253
+ {
254
+ kind: 'reference',
255
+ label: 'Reference',
256
+ description: 'Reference: APIs, CLI, registries and formats (T14107)',
257
+ defaultOwnerKind: 'project',
258
+ publishDir: 'docs/reference',
259
+ requiresEntityId: false,
260
+ requiredSections: [],
261
+ },
262
+ {
263
+ kind: 'migration',
264
+ label: 'Migration',
265
+ description: 'Upgrade or data-migration guide (T14107)',
266
+ defaultOwnerKind: 'project',
267
+ publishDir: 'docs/migration',
268
+ requiresEntityId: false,
269
+ requiredSections: [],
270
+ },
228
271
  ];
229
272
 
230
273
  /**
package/src/done-plan.ts CHANGED
@@ -258,8 +258,21 @@ export interface DonePlanTypedGate {
258
258
  kind: string;
259
259
  /** Requirement id, when the gate declares one. */
260
260
  req?: string;
261
- /** Latest stored result; `not-run` when none is stored, or when a stored pass would not stand. */
262
- status: 'pass' | 'fail' | 'not-run';
261
+ /**
262
+ * Latest stored result; `not-run` when none is stored, or when a stored pass
263
+ * would not stand. `cached` (T14138): nothing is stored, but the evidence
264
+ * cache holds a pass for this tree and environment (from `cleo verify
265
+ * --run`), which the next evidence write binds without running the gate.
266
+ */
267
+ status: 'pass' | 'fail' | 'not-run' | 'cached';
268
+ /** When the cached pass was recorded (`cached` only). */
269
+ cachedAt?: string;
270
+ /**
271
+ * Why a `not-run` gate's cache entry did not match: the key part that moved
272
+ * since the last cached pass (HEAD, dirty fingerprint, tree, inputs or
273
+ * environment), when a recent entry explains it (T14138).
274
+ */
275
+ cacheMiss?: string;
263
276
  /**
264
277
  * Why a stored pass is `not-run`: the refusal `cleo complete` would raise
265
278
  * from this tree and environment (T14063).
@@ -334,6 +347,12 @@ export interface DonePlan {
334
347
  commands: string[];
335
348
  /** True when no blocker remains and `commands` would succeed as planned. */
336
349
  ready: boolean;
350
+ /**
351
+ * Why the plan is not ready when no blocker says so (T14095): the task is
352
+ * already done, or there is no command to run. A plan with `ready: false`
353
+ * always carries a blocker or this reason.
354
+ */
355
+ readyReason?: string;
337
356
  /** The single next step: the first blocker's, or the first command. */
338
357
  next: DoneNextStep | null;
339
358
  /**
@@ -341,6 +360,12 @@ export interface DonePlan {
341
360
  * by the same function `cleo complete` runs, from local refs only.
342
361
  */
343
362
  integration?: WorktreeIntegrationPlan;
363
+ /**
364
+ * Untracked files the change's evidence leaves out (T14125): scope is the
365
+ * task's commits against the base plus uncommitted tracked edits (staged
366
+ * and `git add -N` files included), never a stray file in the checkout.
367
+ */
368
+ untrackedSetAside?: string[];
344
369
  }
345
370
 
346
371
  /** One tool `cleo done` ran (or reused from the ADR-061 cache) before recording. */
@@ -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,
@@ -605,6 +658,8 @@ const ATOM_EXAMPLES: Readonly<Record<EvidenceAtomKind, string>> = Object.freeze(
605
658
  decision: 'decision:D-arch-001',
606
659
  pr: 'pr:357',
607
660
  ci: 'ci:357',
661
+ 'ci-run': 'ci-run:<runId>',
662
+ deploy: 'deploy:<provider>/<deploymentId>@<sha>|https://<host>/health',
608
663
  'loc-drop': 'loc-drop:<fromLines>:<toLines>',
609
664
  'callsite-coverage': 'callsite-coverage:<symbolName>:<relativeSourcePath>',
610
665
  satisfies: 'satisfies:T1234#AC2',
@@ -835,6 +890,35 @@ function parsePrReference(
835
890
  : { prNumber: second, componentPrNumber: first, ...repo };
836
891
  }
837
892
 
893
+ /**
894
+ * Parse a `deploy:` payload, `<provider>/<deploymentId>@<sha>|<probe-url>`
895
+ * (T14083). The provider is lowercased, the SHA lowercased hex.
896
+ */
897
+ function parseDeployPayload(payload: string, chunk: string): z.infer<typeof deployAtomSchema> {
898
+ const fix =
899
+ 'Use format: deploy:<provider>/<deploymentId>@<sha>|<probe-url> e.g. ' +
900
+ 'deploy:railway/9f3c2a@1a2b3c4d|https://api.example.com/health';
901
+ const bar = payload.indexOf('|');
902
+ const match = /^([^/]+)\/(.+)@([0-9A-Fa-f]{7,40})$/.exec(
903
+ bar < 0 ? '' : payload.slice(0, bar).trim(),
904
+ );
905
+ const probeUrl = bar < 0 ? '' : payload.slice(bar + 1).trim();
906
+ const parsed = deployAtomSchema.safeParse({
907
+ kind: 'deploy',
908
+ provider: match?.[1]?.toLowerCase() ?? '',
909
+ deploymentId: match?.[2] ?? '',
910
+ sha: match?.[3]?.toLowerCase() ?? '',
911
+ probeUrl,
912
+ });
913
+ if (!match || !parsed.success) {
914
+ throw new EvidenceParseError(
915
+ `Malformed deploy atom: "${chunk}" (${!match || parsed.success ? 'expected <provider>/<deploymentId>@<sha>|<probe-url>' : parsed.error.issues[0]?.message})`,
916
+ fix,
917
+ );
918
+ }
919
+ return parsed.data;
920
+ }
921
+
838
922
  export function parseEvidenceString(raw: string): EvidenceAtom[] {
839
923
  if (!raw || typeof raw !== 'string') {
840
924
  throw new EvidenceParseError(
@@ -919,6 +1003,19 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
919
1003
  atoms.push(kind === 'pr' ? { kind: 'pr', ...ref } : { kind: 'ci', ...ref });
920
1004
  break;
921
1005
  }
1006
+ case 'ci-run': {
1007
+ if (!/^[1-9][0-9]*$/.test(payload)) {
1008
+ throw new EvidenceParseError(
1009
+ `ci-run atom requires a positive integer run id, got "${payload}" in "${chunk}"`,
1010
+ 'Use format: ci-run:<runId> e.g. ci-run:40812345678 (a repeat-run.yml run on main)',
1011
+ );
1012
+ }
1013
+ atoms.push({ kind: 'ci-run', runId: Number(payload) });
1014
+ break;
1015
+ }
1016
+ case 'deploy':
1017
+ atoms.push(parseDeployPayload(payload, chunk));
1018
+ break;
922
1019
  case 'loc-drop': {
923
1020
  const firstColon = payload.indexOf(':');
924
1021
  if (firstColon < 1 || firstColon === payload.length - 1) {