@cleocode/contracts 2026.5.114 → 2026.5.121

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 (49) hide show
  1. package/dist/__tests__/changesets.test.d.ts +8 -0
  2. package/dist/__tests__/changesets.test.d.ts.map +1 -0
  3. package/dist/__tests__/changesets.test.js +146 -0
  4. package/dist/__tests__/changesets.test.js.map +1 -0
  5. package/dist/__tests__/evidence-atom-schema.test.js +75 -1
  6. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  7. package/dist/__tests__/meta-tokens.test.d.ts +16 -0
  8. package/dist/__tests__/meta-tokens.test.d.ts.map +1 -0
  9. package/dist/__tests__/meta-tokens.test.js +99 -0
  10. package/dist/__tests__/meta-tokens.test.js.map +1 -0
  11. package/dist/boundary.js +3 -3
  12. package/dist/boundary.js.map +1 -1
  13. package/dist/changesets.d.ts +91 -0
  14. package/dist/changesets.d.ts.map +1 -1
  15. package/dist/changesets.js +72 -2
  16. package/dist/changesets.js.map +1 -1
  17. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  18. package/dist/dispatch/operations-registry.js +148 -0
  19. package/dist/dispatch/operations-registry.js.map +1 -1
  20. package/dist/evidence-atom-schema.d.ts +52 -8
  21. package/dist/evidence-atom-schema.d.ts.map +1 -1
  22. package/dist/evidence-atom-schema.js +96 -0
  23. package/dist/evidence-atom-schema.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/operations/docs.d.ts +166 -8
  29. package/dist/operations/docs.d.ts.map +1 -1
  30. package/dist/operations/docs.js +9 -7
  31. package/dist/operations/docs.js.map +1 -1
  32. package/dist/operations/tasks.d.ts +39 -0
  33. package/dist/operations/tasks.d.ts.map +1 -1
  34. package/dist/operations/tasks.js.map +1 -1
  35. package/dist/task-record.d.ts +7 -0
  36. package/dist/task-record.d.ts.map +1 -1
  37. package/package.json +2 -2
  38. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  39. package/src/__tests__/changesets.test.ts +176 -0
  40. package/src/__tests__/evidence-atom-schema.test.ts +88 -0
  41. package/src/__tests__/meta-tokens.test.ts +107 -0
  42. package/src/boundary.ts +3 -3
  43. package/src/changesets.ts +93 -2
  44. package/src/dispatch/operations-registry.ts +155 -0
  45. package/src/evidence-atom-schema.ts +131 -7
  46. package/src/index.ts +2 -0
  47. package/src/operations/docs.ts +187 -9
  48. package/src/operations/tasks.ts +39 -0
  49. package/src/task-record.ts +7 -0
@@ -2335,6 +2335,41 @@ export const OPERATIONS: OperationDef[] = [
2335
2335
  },
2336
2336
  ] satisfies ParamDef[],
2337
2337
  },
2338
+ {
2339
+ gateway: 'mutate',
2340
+ domain: 'tasks',
2341
+ operation: 'relates.remove',
2342
+ description:
2343
+ 'tasks.relates.remove (mutate) — delete a relation between two tasks. Pass `type` to scope deletion to a single relation kind; omit to remove any type.',
2344
+ tier: 1,
2345
+ idempotent: true,
2346
+ sessionRequired: false,
2347
+ requiredParams: ['taskId', 'relatedId'],
2348
+ params: [
2349
+ {
2350
+ name: 'taskId',
2351
+ type: 'string',
2352
+ required: true,
2353
+ description: 'Source task ID',
2354
+ cli: { positional: true },
2355
+ },
2356
+ {
2357
+ name: 'relatedId',
2358
+ type: 'string',
2359
+ required: true,
2360
+ description: 'Target task ID to remove the relation to',
2361
+ cli: { positional: true },
2362
+ },
2363
+ {
2364
+ name: 'type',
2365
+ type: 'string',
2366
+ required: false,
2367
+ description:
2368
+ 'Optional relation type (blocks|related|duplicates|absorbs|fixes|extends|supersedes). Omit to remove any type.',
2369
+ cli: { flag: 'type' },
2370
+ },
2371
+ ] satisfies ParamDef[],
2372
+ },
2338
2373
  // === saga sub-domain (ADR-073 — above-epic grouping tier) ===
2339
2374
  {
2340
2375
  gateway: 'mutate',
@@ -6771,6 +6806,40 @@ export const OPERATIONS: OperationDef[] = [
6771
6806
  },
6772
6807
  ],
6773
6808
  },
6809
+ // ── docs.supersede (T10162) ──────────────────────────────────────────────
6810
+ {
6811
+ gateway: 'mutate',
6812
+ domain: 'docs',
6813
+ operation: 'supersede',
6814
+ description:
6815
+ 'docs.supersede (mutate) — atomically flip an older doc to `superseded` and link both rows via the supersedes/superseded_by self-FK pointers (T10162 · Saga T9855)',
6816
+ tier: 1,
6817
+ idempotent: true,
6818
+ sessionRequired: false,
6819
+ requiredParams: ['oldSlug', 'newSlug'],
6820
+ params: [
6821
+ {
6822
+ name: 'oldSlug',
6823
+ type: 'string' as const,
6824
+ required: true,
6825
+ description: 'Slug of the doc being replaced',
6826
+ cli: { positional: true },
6827
+ },
6828
+ {
6829
+ name: 'newSlug',
6830
+ type: 'string' as const,
6831
+ required: true,
6832
+ description: 'Slug of the doc that replaces oldSlug',
6833
+ cli: { positional: true },
6834
+ },
6835
+ {
6836
+ name: 'reason',
6837
+ type: 'string' as const,
6838
+ required: false,
6839
+ description: 'Optional human-readable reason carried back on the response',
6840
+ },
6841
+ ],
6842
+ },
6774
6843
  // ── docs.generate (T798) ─────────────────────────────────────────────────
6775
6844
  {
6776
6845
  gateway: 'query',
@@ -6797,6 +6866,61 @@ export const OPERATIONS: OperationDef[] = [
6797
6866
  },
6798
6867
  ],
6799
6868
  },
6869
+ // ── docs.update (T10161 — Epic T10157 / Saga T9855) ──────────────────────
6870
+ {
6871
+ gateway: 'mutate',
6872
+ domain: 'docs',
6873
+ operation: 'update',
6874
+ description:
6875
+ 'docs.update (mutate) — replace blob content for an existing slug while preserving the slug. ' +
6876
+ 'New sha256 + new attachment row; the old row stays reachable by ID for history. ' +
6877
+ 'NO supersession edge (use `cleo docs supersede` for that). ' +
6878
+ 'Audit log entry appended to .cleo/audit/docs-versioning.jsonl (squashed within a 5-min window).',
6879
+ tier: 1,
6880
+ idempotent: false,
6881
+ sessionRequired: false,
6882
+ requiredParams: ['slug'],
6883
+ params: [
6884
+ {
6885
+ name: 'slug',
6886
+ type: 'string' as const,
6887
+ required: true,
6888
+ description: 'Slug of the existing attachment to update',
6889
+ },
6890
+ {
6891
+ name: 'file',
6892
+ type: 'string' as const,
6893
+ required: false,
6894
+ description: 'Path to a local file containing the new content',
6895
+ },
6896
+ {
6897
+ name: 'content',
6898
+ type: 'string' as const,
6899
+ required: false,
6900
+ description: 'Inline UTF-8 content (mutually exclusive with file)',
6901
+ },
6902
+ {
6903
+ name: 'message',
6904
+ type: 'string' as const,
6905
+ required: false,
6906
+ description: 'One-line summary describing the change (recorded in the audit log)',
6907
+ },
6908
+ {
6909
+ name: 'status',
6910
+ type: 'string' as const,
6911
+ required: false,
6912
+ description:
6913
+ 'Override the new lifecycle status. Defaults to "draft" on every update. ' +
6914
+ 'Valid: draft|proposed|accepted|superseded|archived|deprecated.',
6915
+ },
6916
+ {
6917
+ name: 'attachedBy',
6918
+ type: 'string' as const,
6919
+ required: false,
6920
+ description: 'Agent identity that performed the update (defaults to "human")',
6921
+ },
6922
+ ],
6923
+ },
6800
6924
  // ── playbook.* HITL CLI surface (T935) ───────────────────────────────────
6801
6925
  {
6802
6926
  gateway: 'mutate',
@@ -7505,6 +7629,37 @@ export const OPERATIONS: OperationDef[] = [
7505
7629
  ] satisfies ParamDef[],
7506
7630
  },
7507
7631
 
7632
+ // release query: validate-changelog (T9937 / Saga T9862 — canonical CHANGELOG
7633
+ // header validator that replaces the brittle inline grep step in
7634
+ // .github/workflows/release.yml). Read-only — exposed only under the query
7635
+ // gateway because no DB or filesystem mutation occurs.
7636
+ {
7637
+ gateway: 'query',
7638
+ domain: 'release',
7639
+ operation: 'validate-changelog',
7640
+ description:
7641
+ 'release.validate-changelog (query) — validate that CHANGELOG.md contains the canonical `## [VERSION]` header per ADR-028 §2.5. Replaces the brittle inline grep in .github/workflows/release.yml (T9937 / Saga T9862).',
7642
+ tier: 1,
7643
+ idempotent: true,
7644
+ sessionRequired: false,
7645
+ requiredParams: ['version'],
7646
+ params: [
7647
+ {
7648
+ name: 'version',
7649
+ type: 'string',
7650
+ required: true,
7651
+ description: 'Release version (accepts v2026.5.94 or 2026.5.94)',
7652
+ cli: { positional: true },
7653
+ },
7654
+ {
7655
+ name: 'changelogPath',
7656
+ type: 'string',
7657
+ required: false,
7658
+ description: 'Override the CHANGELOG file path (default: <projectRoot>/CHANGELOG.md)',
7659
+ },
7660
+ ] satisfies ParamDef[],
7661
+ },
7662
+
7508
7663
  // ---------------------------------------------------------------------------
7509
7664
  // Provenance domain — `cleo provenance` CLI surface (T9528 · Phase 2 of T9493)
7510
7665
  // ---------------------------------------------------------------------------
@@ -332,14 +332,25 @@ export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
332
332
  * Result of {@link validateEvidenceForGate}.
333
333
  *
334
334
  * Success carries no extra payload (the caller already holds the atom list).
335
- * Failure carries a human-readable message in the legacy
336
- * `Gate '<gate>' requires evidence: [<combo>] OR [<combo>]` format so error
337
- * surfaces (E_EVIDENCE_INSUFFICIENT) read identically across the CLI and any
338
- * client-side validator built on top of this schema.
335
+ * Failure carries:
336
+ * - {@link EvidenceValidationFailure.message} — the legacy single-line
337
+ * `Gate '<gate>' requires evidence: [<combo>] OR [<combo>]` format,
338
+ * preserved byte-for-byte for backward-compat with surfaces and tests
339
+ * that match against it.
340
+ * - {@link EvidenceValidationFailure.hint} — a richer, multi-line,
341
+ * example-bearing remediation hint suitable for CLI `fix:` surfaces.
342
+ * Added by T9949 so `E_EVIDENCE_INSUFFICIENT` errors point clearly at
343
+ * the alternative atom the caller needs (e.g. "this gate requires
344
+ * real proof — note: alone is not accepted; use commit+files,
345
+ * commit+note, or pr instead").
339
346
  */
340
- export type EvidenceValidationResult =
341
- | { readonly ok: true }
342
- | { readonly ok: false; readonly message: string };
347
+ export interface EvidenceValidationFailure {
348
+ readonly ok: false;
349
+ readonly message: string;
350
+ readonly hint: string;
351
+ }
352
+
353
+ export type EvidenceValidationResult = { readonly ok: true } | EvidenceValidationFailure;
343
354
 
344
355
  /**
345
356
  * Format the satisfying-combinations spec for a gate as the legacy
@@ -354,6 +365,114 @@ export function formatGateRequirement(gate: VerificationGate): string {
354
365
  return requirement.oneOf.map((set) => `[${set.join(' AND ')}]`).join(' OR ');
355
366
  }
356
367
 
368
+ /**
369
+ * Example `--evidence` strings for each atom kind. Used by
370
+ * {@link formatGateRequirementHint} to render copy-pasteable remediation
371
+ * commands in CLI error surfaces.
372
+ *
373
+ * @task T9949
374
+ */
375
+ const ATOM_EXAMPLES: Readonly<Record<EvidenceAtomKind, string>> = Object.freeze({
376
+ commit: 'commit:<sha>',
377
+ files: 'files:path/a.ts,path/b.ts',
378
+ 'test-run': 'test-run:/tmp/vitest-out.json',
379
+ tool: 'tool:test',
380
+ url: 'url:https://example.com/docs',
381
+ note: 'note:<short description>',
382
+ decision: 'decision:D-arch-001',
383
+ pr: 'pr:357',
384
+ 'loc-drop': 'loc-drop:<fromLines>:<toLines>',
385
+ 'callsite-coverage': 'callsite-coverage:<symbolName>:<relativeSourcePath>',
386
+ });
387
+
388
+ /**
389
+ * Render a satisfying atom combination as a copy-pasteable `--evidence`
390
+ * string fragment, e.g. `['commit', 'files']` → `commit:<sha>;files:path/a.ts,path/b.ts`.
391
+ *
392
+ * @internal
393
+ */
394
+ function renderCombinationAsEvidence(combination: ReadonlyArray<EvidenceAtomKind>): string {
395
+ return combination.map((kind) => ATOM_EXAMPLES[kind]).join(';');
396
+ }
397
+
398
+ /**
399
+ * Produce a multi-line, example-bearing remediation hint for a gate.
400
+ *
401
+ * Designed for CLI `fix:` surfaces so `E_EVIDENCE_INSUFFICIENT` errors point
402
+ * the caller at the specific alternative atom shape they need. The hint lists
403
+ * every satisfying combination as a copy-pasteable `cleo verify` invocation
404
+ * plus a note-specific clarification when the gate does NOT accept `note:`
405
+ * alone.
406
+ *
407
+ * The motivating bug (T9949) was that note-only deliverables (decision-only
408
+ * tasks, deletion-only refactors) hit `E_EVIDENCE_INSUFFICIENT` with the
409
+ * legacy single-line error and had to consult ADR-051 to figure out which
410
+ * additional atom kind the gate required. The richer hint inlines the answer.
411
+ *
412
+ * @param gate - Verification gate the hint is for.
413
+ * @returns Multi-line remediation hint suitable for an `engineError({fix:})`
414
+ * surface. Always ends in a clarifying sentence about whether `note:` alone
415
+ * is accepted for the gate.
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * formatGateRequirementHint('implemented');
420
+ * // "Gate 'implemented' requires programmatic evidence. Use ONE of:
421
+ * // - cleo verify T#### --gate implemented --evidence 'commit:<sha>;files:path/a.ts,path/b.ts'
422
+ * // - cleo verify T#### --gate implemented --evidence 'commit:<sha>;note:<short description>'
423
+ * // - cleo verify T#### --gate implemented --evidence 'decision:D-arch-001;files:path/a.ts,path/b.ts'
424
+ * // - cleo verify T#### --gate implemented --evidence 'decision:D-arch-001;note:<short description>'
425
+ * // - cleo verify T#### --gate implemented --evidence 'pr:357'
426
+ * // Note: 'note:' alone is NOT accepted for this gate — pair it with 'commit:' or 'decision:'."
427
+ * ```
428
+ *
429
+ * @task T9949
430
+ * @adr ADR-051 §2.3
431
+ */
432
+ export function formatGateRequirementHint(gate: VerificationGate): string {
433
+ const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
434
+ if (!requirement) {
435
+ // Unknown gate — match validateEvidenceForGate's defensive default.
436
+ return `Gate '${gate}' has no documented evidence requirements.`;
437
+ }
438
+
439
+ const lines: string[] = [
440
+ `Gate '${gate}' requires programmatic evidence. Use ONE of:`,
441
+ ...requirement.oneOf.map(
442
+ (combo) =>
443
+ ` - cleo verify T#### --gate ${gate} --evidence '${renderCombinationAsEvidence(combo)}'`,
444
+ ),
445
+ ];
446
+
447
+ // Note-specific clarification — T9949's headline pain point.
448
+ // The gate accepts note: alone IFF some single-element combination is ['note'].
449
+ const noteAloneAccepted = requirement.oneOf.some(
450
+ (combo) => combo.length === 1 && combo[0] === 'note',
451
+ );
452
+ if (noteAloneAccepted) {
453
+ lines.push(`Note: 'note:' alone IS accepted for this gate (waiver-style evidence).`);
454
+ } else {
455
+ // Surface every alternative pairing involving note for at-a-glance routing.
456
+ const notePairings = requirement.oneOf.filter(
457
+ (combo) => combo.length > 1 && combo.includes('note'),
458
+ );
459
+ if (notePairings.length > 0) {
460
+ const partners = Array.from(
461
+ new Set(notePairings.flatMap((combo) => combo.filter((k) => k !== 'note'))),
462
+ ).map((k) => `'${k}:'`);
463
+ lines.push(
464
+ `Note: 'note:' alone is NOT accepted for this gate — pair it with ${partners.join(' or ')}.`,
465
+ );
466
+ } else {
467
+ lines.push(
468
+ `Note: 'note:' is NOT accepted for this gate (even when paired). See ADR-051 for the full grammar.`,
469
+ );
470
+ }
471
+ }
472
+
473
+ return lines.join('\n');
474
+ }
475
+
357
476
  /**
358
477
  * Check whether a set of parsed atoms satisfies the requirement spec for
359
478
  * `gate`.
@@ -401,6 +520,11 @@ export function validateEvidenceForGate(
401
520
  return {
402
521
  ok: false,
403
522
  message: `Gate '${gate}' requires evidence: ${formatGateRequirement(gate)}`,
523
+ // T9949: rich, example-bearing remediation hint suitable for CLI `fix:`.
524
+ // Always populated alongside `message` so consumers can choose between
525
+ // the legacy single-line surface and the multi-line hint without re-
526
+ // computing the spec.
527
+ hint: formatGateRequirementHint(gate),
404
528
  };
405
529
  }
406
530
 
package/src/index.ts CHANGED
@@ -475,6 +475,7 @@ export {
475
475
  export type {
476
476
  EvidenceAtom as EvidenceAtomInput,
477
477
  EvidenceAtomKind,
478
+ EvidenceValidationFailure,
478
479
  EvidenceValidationResult,
479
480
  GateEvidenceRequirement,
480
481
  } from './evidence-atom-schema.js';
@@ -486,6 +487,7 @@ export {
486
487
  EvidenceParseError,
487
488
  filesAtomSchema,
488
489
  formatGateRequirement,
490
+ formatGateRequirementHint,
489
491
  GATE_EVIDENCE_REQUIREMENTS,
490
492
  locDropAtomSchema,
491
493
  noteAtomSchema,
@@ -1,15 +1,17 @@
1
1
  /**
2
- * Docs Domain Operations Contract (5 operations)
2
+ * Docs Domain Operations Contract (7 operations)
3
3
  *
4
4
  * Query operations: 3
5
- * Mutate operations: 2
5
+ * Mutate operations: 4
6
6
  *
7
7
  * Docs domain handles attachment management and document generation via `cleo docs`:
8
- * - add — attach a local file or URL to a CLEO owner entity (task, session, observation)
9
- * - list — list attachments for an owner entity
10
- * - generate — generate llms.txt summary for a task/epic, optionally attach as blob
11
- * - fetch — retrieve attachment bytes and metadata by ID or SHA-256
12
- * - remove — remove an attachment ref; purges blob when refCount hits zero
8
+ * - add — attach a local file or URL to a CLEO owner entity (task, session, observation)
9
+ * - list — list attachments for an owner entity
10
+ * - generate — generate llms.txt summary for a task/epic, optionally attach as blob
11
+ * - fetch — retrieve attachment bytes and metadata by ID or SHA-256
12
+ * - remove — remove an attachment ref; purges blob when refCount hits zero
13
+ * - update — UPDATE-in-place via slug (T10161)
14
+ * - supersede — atomically flip an older doc to `superseded` and link it to its successor (T10162)
13
15
  *
14
16
  * Owner type is auto-detected from the owner ID prefix:
15
17
  * T### → 'task'
@@ -398,6 +400,15 @@ export interface DocsAddParams {
398
400
  * @task T9636 (T-DOCS-SLUG-1)
399
401
  */
400
402
  slug?: string;
403
+ /**
404
+ * Optional human-readable title — used to derive the kebab-slug tail when
405
+ * auto-allocating an ADR slug. REQUIRED when `type === 'adr'` AND `slug`
406
+ * is omitted; ignored otherwise. The allocator slugifies the title via
407
+ * the shared kebabize helper.
408
+ *
409
+ * @task T10360 (closes T10153)
410
+ */
411
+ title?: string;
401
412
  /**
402
413
  * Optional taxonomy classification. Must match one of
403
414
  * {@link DOCS_TYPE_VALUES}; an invalid value returns `E_INVALID_TYPE`.
@@ -447,6 +458,13 @@ export interface DocsAddResult {
447
458
  slug?: string;
448
459
  /** Type classification recorded for this attachment, when provided (T9637). */
449
460
  type?: DocsType;
461
+ /**
462
+ * Auto-allocated ADR number, present iff the slug was minted by
463
+ * {@link allocateAdrSlug} (i.e. `--type adr` without `--slug`).
464
+ *
465
+ * @task T10360
466
+ */
467
+ adrNumber?: number;
450
468
  }
451
469
 
452
470
  // --------------------------------------------------------------------------
@@ -481,6 +499,157 @@ export interface DocsRemoveResult {
481
499
  attachmentBackend?: AttachmentBackend;
482
500
  }
483
501
 
502
+ // --------------------------------------------------------------------------
503
+ // docs.update — UPDATE-in-place via slug (T10161)
504
+ // --------------------------------------------------------------------------
505
+
506
+ /**
507
+ * Allowed `lifecycle_status` values mirrored from the attachments-table enum
508
+ * (`ATTACHMENT_LIFECYCLE_STATUSES` in
509
+ * `packages/core/src/store/schema/attachments.ts`). Kept inline here so the
510
+ * contract surface stays self-contained — the dispatch handler narrows raw
511
+ * `--status` input against this set before touching the store.
512
+ *
513
+ * @task T10161 (Epic T10157 / Saga T9855)
514
+ */
515
+ export type DocsLifecycleStatus =
516
+ | 'draft'
517
+ | 'proposed'
518
+ | 'accepted'
519
+ | 'superseded'
520
+ | 'archived'
521
+ | 'deprecated';
522
+
523
+ /**
524
+ * Parameters for `docs.update`.
525
+ *
526
+ * Replaces the blob content for an existing slug — the slug is transferred
527
+ * from the old `attachments` row to a new row carrying the new sha256, and
528
+ * the old row remains reachable by attachment-id for history. Unlike
529
+ * {@link DocsSupersedeParams}-style flows, NO supersession edge is created
530
+ * (callers wanting an explicit lineage edge should use `cleo docs supersede`).
531
+ *
532
+ * Exactly one of `file` or `content` MUST be provided.
533
+ *
534
+ * @task T10161 (Epic T10157 / Saga T9855 — E12.C4)
535
+ */
536
+ export interface DocsUpdateParams {
537
+ /** Slug of the existing attachment to update (required). */
538
+ slug: string;
539
+ /** Path to a local file containing the new content. */
540
+ file?: string;
541
+ /** Inline UTF-8 content (mutually exclusive with `file`). */
542
+ content?: string;
543
+ /** Optional one-line summary describing the change (recorded in the audit log). */
544
+ message?: string;
545
+ /**
546
+ * Override the new lifecycle status. Defaults to `'draft'` on every update
547
+ * so an explicit `accepted` doc gets back-pressured to draft on edit. Pass
548
+ * `--status accepted` (or any other valid status) to override.
549
+ */
550
+ status?: DocsLifecycleStatus;
551
+ /** Agent identity that performed the update (default: `'human'`). */
552
+ attachedBy?: string;
553
+ }
554
+
555
+ /**
556
+ * Result of `docs.update`.
557
+ *
558
+ * Identifies the new row (active) and the row it replaced. When the supplied
559
+ * content is byte-identical to the current content the operation is a noop —
560
+ * `previousSha256` will equal `sha256` and `changed` will be `false`.
561
+ *
562
+ * @task T10161
563
+ */
564
+ export interface DocsUpdateResult {
565
+ /** Slug that now points at the new content. */
566
+ slug: string;
567
+ /** Type/kind of the row, preserved from the prior version. */
568
+ type?: DocsType;
569
+ /** New attachment ID now bearing the slug. */
570
+ attachmentId: string;
571
+ /** Previous attachment ID; retains its row + bytes for history. */
572
+ previousAttachmentId: string;
573
+ /** SHA-256 hex of the new content. */
574
+ sha256: string;
575
+ /** SHA-256 hex of the prior content (equals `sha256` on a noop). */
576
+ previousSha256: string;
577
+ /** Whether the bytes actually changed (false ⇒ noop). */
578
+ changed: boolean;
579
+ /** Lifecycle status now stored on the new row. */
580
+ lifecycleStatus: DocsLifecycleStatus;
581
+ /** ISO 8601 timestamp the new row was registered. */
582
+ updatedAt: string;
583
+ /**
584
+ * Best-effort 1-indexed version number for the slug (count of historical
585
+ * rows that ever carried this slug, including the current one). When the
586
+ * underlying store cannot enumerate history this falls back to `2` for the
587
+ * first update and increments by 1 thereafter.
588
+ */
589
+ version: number;
590
+ /**
591
+ * True when this update was squashed into an existing audit entry within
592
+ * the 5-minute squash window (no new audit line was written).
593
+ */
594
+ squashed: boolean;
595
+ }
596
+
597
+ // --------------------------------------------------------------------------
598
+ // docs.supersede — flip an old doc to `superseded` and point at its successor
599
+ // --------------------------------------------------------------------------
600
+
601
+ /**
602
+ * Parameters for `docs.supersede`.
603
+ *
604
+ * Both `oldSlug` and `newSlug` reference rows on `attachments.slug`. The
605
+ * operation is atomic: either both rows update (and the lifecycle flip lands)
606
+ * or neither does.
607
+ *
608
+ * @task T10162 (Saga T9855 · Epic T10157 · ADR-078)
609
+ */
610
+ export interface DocsSupersedeParams {
611
+ /** Slug of the doc being replaced. */
612
+ oldSlug: string;
613
+ /** Slug of the doc that replaces {@link oldSlug}. */
614
+ newSlug: string;
615
+ /**
616
+ * Optional human-readable reason surfaced on the response envelope and
617
+ * preserved by future provenance-graph reads (T10166 `summary` field on
618
+ * the `supersedes` edge). Not persisted to a dedicated column today.
619
+ */
620
+ reason?: string;
621
+ }
622
+
623
+ /**
624
+ * Result of `docs.supersede`.
625
+ *
626
+ * @task T10162 (Saga T9855 · Epic T10157 · ADR-078)
627
+ */
628
+ export interface DocsSupersedeResult {
629
+ /** Slug of the doc that was just superseded. */
630
+ oldSlug: string;
631
+ /** Slug of the new doc that supersedes {@link oldSlug}. */
632
+ newSlug: string;
633
+ /** Attachment ID resolved from {@link oldSlug}. */
634
+ oldAttachmentId: string;
635
+ /** Attachment ID resolved from {@link newSlug}. */
636
+ newAttachmentId: string;
637
+ /** ISO-8601 timestamp the supersession was recorded. */
638
+ supersededAt: string;
639
+ /**
640
+ * Stable identifier for the lineage edge minted by this call.
641
+ *
642
+ * The edge itself is not stored in a dedicated table — it is reconstructed
643
+ * at read time from `attachments.supersedes` / `attachments.superseded_by`
644
+ * by `cleo docs provenance` (T10166). The ID is deterministic
645
+ * (`supersedes:<newAttachmentId>->&<oldAttachmentId>`) so concurrent callers
646
+ * that win the transaction race observe a stable value.
647
+ */
648
+ edgeId: string;
649
+ /** Optional reason carried through from the request. */
650
+ reason?: string;
651
+ }
652
+
484
653
  // ============================================================================
485
654
  // Discriminated Union (DocsOps)
486
655
  // ============================================================================
@@ -500,7 +669,9 @@ export type DocsOps =
500
669
  | { op: 'docs.fetch'; params: DocsFetchParams; result: DocsFetchResult }
501
670
  | { op: 'docs.generate'; params: DocsGenerateParams; result: DocsGenerateResult }
502
671
  | { op: 'docs.add'; params: DocsAddParams; result: DocsAddResult }
503
- | { op: 'docs.remove'; params: DocsRemoveParams; result: DocsRemoveResult };
672
+ | { op: 'docs.remove'; params: DocsRemoveParams; result: DocsRemoveResult }
673
+ | { op: 'docs.update'; params: DocsUpdateParams; result: DocsUpdateResult }
674
+ | { op: 'docs.supersede'; params: DocsSupersedeParams; result: DocsSupersedeResult };
504
675
 
505
676
  /**
506
677
  * Enumeration of all docs domain operation names.
@@ -509,4 +680,11 @@ export type DocsOps =
509
680
  * Useful for dynamic operation dispatch, type narrowing, or documentation.
510
681
  * Kept in sync with the `DocsOps` discriminated union above.
511
682
  */
512
- export type DocsOp = 'docs.list' | 'docs.fetch' | 'docs.generate' | 'docs.add' | 'docs.remove';
683
+ export type DocsOp =
684
+ | 'docs.list'
685
+ | 'docs.fetch'
686
+ | 'docs.generate'
687
+ | 'docs.add'
688
+ | 'docs.remove'
689
+ | 'docs.update'
690
+ | 'docs.supersede';
@@ -125,6 +125,45 @@ export interface TasksFindParams {
125
125
  * @task T9072
126
126
  */
127
127
  kind?: string;
128
+ /**
129
+ * Unified urgency surface (T9905).
130
+ *
131
+ * When `true`, the predicate is
132
+ *
133
+ * `priority IN ('critical','high') OR severity IN ('P0','P1')`
134
+ *
135
+ * combining the two orthogonal urgency axes (priority + severity) into a
136
+ * single filter. Composes with other filters via AND.
137
+ *
138
+ * @task T9905
139
+ */
140
+ urgent?: boolean;
141
+ /**
142
+ * Filter by label — selects tasks whose `labels` array contains this
143
+ * value. Composes with other filters via AND.
144
+ *
145
+ * Closes GH#393 — gives `cleo find --label <name>` parity with the
146
+ * positional `cleo labels <name>` surface.
147
+ *
148
+ * @task T9904
149
+ */
150
+ label?: string;
151
+ /**
152
+ * Filter by parent task ID — restricts the result set to tasks whose
153
+ * `parentId` equals this value. Mirrors the `--parent` axis on
154
+ * `cleo list`. When the parent task is a Saga (Epic with
155
+ * `label='saga'`), routing goes through `task_relations.type='groups'`
156
+ * member IDs instead of the `parentId` column (ADR-073 §1).
157
+ *
158
+ * Composes with other filters via AND. Closes T10108 — pre-fix,
159
+ * `cleo find "" --parent <id>` returned every task in the project
160
+ * because the empty-string query bypassed all filters via
161
+ * `fuzzyScore('', '<title>')===80`.
162
+ *
163
+ * @task T10108
164
+ * @saga T9862
165
+ */
166
+ parent?: string;
128
167
  }
129
168
  export type TasksFindResult = MinimalTask[];
130
169
 
@@ -103,4 +103,11 @@ export interface MinimalTaskRecord {
103
103
  type?: string;
104
104
  /** Scope size estimate — helps agents decide if decomposition is needed. @task T091 */
105
105
  size?: string;
106
+ /**
107
+ * Bug severity axis — P0|P1|P2|P3 (string-widened). Surfaced so the unified
108
+ * urgency surface (`cleo find --urgent`) can identify P0/P1 rows without a
109
+ * follow-up `cleo show` per row.
110
+ * @task T9905
111
+ */
112
+ severity?: string | null;
106
113
  }