@cleocode/contracts 2026.10.6 → 2026.10.7-rc.ci.674

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 (65) hide show
  1. package/dist/__tests__/evidence-atom-schema.test.js +4 -2
  2. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  3. package/dist/acceptance-gate.d.ts +2 -1
  4. package/dist/acceptance-gate.d.ts.map +1 -1
  5. package/dist/ask-enforce-hook.d.ts +59 -0
  6. package/dist/ask-enforce-hook.d.ts.map +1 -0
  7. package/dist/ask-enforce-hook.js +18 -0
  8. package/dist/ask-enforce-hook.js.map +1 -0
  9. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  10. package/dist/dispatch/operations-registry.js +114 -0
  11. package/dist/dispatch/operations-registry.js.map +1 -1
  12. package/dist/done-plan.d.ts +1 -1
  13. package/dist/done-plan.d.ts.map +1 -1
  14. package/dist/evidence-atom-schema.d.ts +20 -1
  15. package/dist/evidence-atom-schema.d.ts.map +1 -1
  16. package/dist/evidence-atom-schema.js +23 -1
  17. package/dist/evidence-atom-schema.js.map +1 -1
  18. package/dist/git-hooks.d.ts +50 -0
  19. package/dist/git-hooks.d.ts.map +1 -0
  20. package/dist/git-hooks.js +20 -0
  21. package/dist/git-hooks.js.map +1 -0
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/nexus-account.d.ts +1 -1
  27. package/dist/nexus-account.d.ts.map +1 -1
  28. package/dist/nexus-account.js +10 -0
  29. package/dist/nexus-account.js.map +1 -1
  30. package/dist/nexus-cloud.d.ts +15 -0
  31. package/dist/nexus-cloud.d.ts.map +1 -1
  32. package/dist/nexus-cloud.js.map +1 -1
  33. package/dist/nexus-vault.d.ts +68 -1
  34. package/dist/nexus-vault.d.ts.map +1 -1
  35. package/dist/operations/docs.d.ts +75 -1
  36. package/dist/operations/docs.d.ts.map +1 -1
  37. package/dist/operations/orchestrate.d.ts +15 -1
  38. package/dist/operations/orchestrate.d.ts.map +1 -1
  39. package/dist/operations/validate.d.ts +5 -0
  40. package/dist/operations/validate.d.ts.map +1 -1
  41. package/dist/release/evidence-atoms.d.ts +2 -0
  42. package/dist/release/evidence-atoms.d.ts.map +1 -1
  43. package/dist/release/evidence-atoms.js +4 -0
  44. package/dist/release/evidence-atoms.js.map +1 -1
  45. package/dist/release/plan.d.ts +3 -3
  46. package/dist/task.d.ts +36 -0
  47. package/dist/task.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  50. package/src/__tests__/evidence-atom-schema.test.ts +4 -2
  51. package/src/acceptance-gate.ts +2 -1
  52. package/src/ask-enforce-hook.ts +69 -0
  53. package/src/dispatch/operations-registry.ts +118 -0
  54. package/src/done-plan.ts +1 -0
  55. package/src/evidence-atom-schema.ts +24 -1
  56. package/src/git-hooks.ts +43 -0
  57. package/src/index.ts +11 -0
  58. package/src/nexus-account.ts +10 -0
  59. package/src/nexus-cloud.ts +16 -0
  60. package/src/nexus-vault.ts +71 -1
  61. package/src/operations/docs.ts +88 -2
  62. package/src/operations/orchestrate.ts +15 -1
  63. package/src/operations/validate.ts +5 -0
  64. package/src/release/evidence-atoms.ts +4 -0
  65. package/src/task.ts +37 -0
@@ -30,12 +30,14 @@ import type { VerificationGate } from '../task.js';
30
30
  describe('EvidenceAtomSchema (T10337)', () => {
31
31
  it('EVIDENCE_ATOM_KINDS covers every discriminated union member (T12030)', () => {
32
32
  // Verify the shared constant is in sync with the zod discriminated union.
33
- // The discriminated union keys are 'commit','files','test-run','tool','url',
34
- // 'note','decision','pr','ci','loc-drop','callsite-coverage','satisfies'.
33
+ // The discriminated union keys are 'commit','files','test-run','qa-run',
34
+ // 'tool','url','note','decision','pr','ci','loc-drop','callsite-coverage',
35
+ // 'satisfies' ('qa-run': T13427).
35
36
  const expected = [
36
37
  'commit',
37
38
  'files',
38
39
  'test-run',
40
+ 'qa-run',
39
41
  'tool',
40
42
  'url',
41
43
  'note',
@@ -124,7 +124,8 @@ export interface TestGate extends GateBase {
124
124
  /** Arguments split explicitly (avoids shell-injection). */
125
125
  args?: string[];
126
126
  /**
127
- * - `"pass"`: exit code 0 AND stdout contains no `FAIL|failing|Error:` pattern.
127
+ * - `"pass"`: exit code 0 AND stdout contains no `FAIL|failing|Error:` pattern, ignoring
128
+ * summaries that report zero failures (`ℹ fail 0`, `# fail 0`, `0 failed`; T13511).
128
129
  * - `"exit0"`: exit code 0 only (permissive mode).
129
130
  */
130
131
  expect: 'pass' | 'exit0';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ask-enforce Stop hook — shared types (T13420).
3
+ *
4
+ * The owner rule (CLEO-INJECTION.md universal protocol step 7) routes every
5
+ * owner question or decision through the harness ask tool. The ask-enforce
6
+ * hook runs when an agent's turn ends: if the final reply asks the owner
7
+ * something in prose and the turn made no ask-tool call, the hook blocks the
8
+ * stop and tells the agent to re-ask with its ask tool. The classifier lives
9
+ * in `@cleocode/core` (`harness/ask-enforce`); the provider wire formats and
10
+ * the entry point (`cleo hook ask-enforce`) in `@cleocode/cleo`.
11
+ *
12
+ * Design: `cleo docs fetch t13420-ask-enforce-stop-hook-design`.
13
+ *
14
+ * @task T13420
15
+ * @epic T13418
16
+ */
17
+
18
+ /**
19
+ * How the hook treats a turn that ends in a prose owner question.
20
+ *
21
+ * - `block` — DEFAULT. Block the stop once per turn with the re-ask message.
22
+ * - `warn` — never block; surface the message as context only.
23
+ * - `off` — do nothing.
24
+ */
25
+ export type AskEnforceMode = 'block' | 'warn' | 'off';
26
+
27
+ /** What the classifier sees for one ended turn. */
28
+ export interface AskEnforceInput {
29
+ /** The final assistant message of the turn (empty or absent: fail open). */
30
+ readonly lastAssistantText: string | null | undefined;
31
+ /** Tool names called since the last user message. */
32
+ readonly turnToolCalls: readonly string[];
33
+ /** Every tool name that counts as an ask-tool call (provider names, CAAMP union). */
34
+ readonly askToolNames: readonly string[];
35
+ /** The harness already re-prompted once from a stop hook in this turn. */
36
+ readonly stopHookActive?: boolean;
37
+ /** Blocks this hook already issued in this turn (loop guard for harnesses without `stopHookActive`). */
38
+ readonly blocksThisTurn?: number;
39
+ }
40
+
41
+ /**
42
+ * Which rule decided the verdict.
43
+ *
44
+ * - `no-text` — nothing to classify (fail open).
45
+ * - `asked` — the turn already called an ask tool.
46
+ * - `loop-guard` — the stop was already blocked once this turn.
47
+ * - `hitl-request` — the reply carries a `hitl.request` envelope (no-ask-tool fallback).
48
+ * - `clean` — no owner question in the reply's tail.
49
+ * - `prose-question` — a reader-directed question ends the reply.
50
+ * - `written-decision` — a request for an owner choice or approval without `?`.
51
+ */
52
+ export type AskEnforceSignal =
53
+ | 'no-text'
54
+ | 'asked'
55
+ | 'loop-guard'
56
+ | 'hitl-request'
57
+ | 'clean'
58
+ | 'prose-question'
59
+ | 'written-decision';
60
+
61
+ /** The classifier's answer for one ended turn. */
62
+ export interface AskEnforceVerdict {
63
+ /** `block` only for `prose-question` / `written-decision`. */
64
+ readonly verdict: 'allow' | 'block';
65
+ /** The rule that decided. */
66
+ readonly signal: AskEnforceSignal;
67
+ /** The offending sentence, at most 160 characters (block only). */
68
+ readonly excerpt: string | null;
69
+ }
@@ -100,6 +100,66 @@ export const OPERATIONS: OperationDef[] = [
100
100
  ],
101
101
  },
102
102
  },
103
+ {
104
+ gateway: 'mutate',
105
+ domain: 'tasks',
106
+ operation: 'req.replace',
107
+ description:
108
+ "Replace a typed requirement gate's definition in place, keeping its AC row and REQ-ID; the superseded gate is kept in AC history (T12988)",
109
+ tier: 1,
110
+ idempotent: true,
111
+ sessionRequired: true,
112
+ requiredParams: ['taskId', 'req', 'gate'],
113
+ params: [
114
+ requirementTaskParam,
115
+ {
116
+ name: 'req',
117
+ type: 'string',
118
+ required: true,
119
+ description: 'REQ-ID of the gate to replace',
120
+ cli: { positional: true },
121
+ },
122
+ {
123
+ name: 'gate',
124
+ type: 'string',
125
+ required: true,
126
+ description: 'Replacement AcceptanceGate JSON; its req must be absent or equal the REQ-ID',
127
+ cli: { flag: 'gate' },
128
+ },
129
+ {
130
+ name: 'reason',
131
+ type: 'string',
132
+ required: false,
133
+ description: 'Why the gate changes; required once the task is in a locked pipeline stage',
134
+ cli: { flag: 'reason' },
135
+ },
136
+ ],
137
+ inputSchema: {
138
+ operation: 'tasks.req.replace',
139
+ schema: {
140
+ type: 'object',
141
+ required: ['taskId', 'req', 'gate'],
142
+ additionalProperties: false,
143
+ properties: {
144
+ taskId: { type: 'string', minLength: 1 },
145
+ req: { type: 'string', minLength: 1 },
146
+ gate: { type: 'string', minLength: 1 },
147
+ reason: { type: 'string', minLength: 1 },
148
+ },
149
+ },
150
+ examples: [
151
+ {
152
+ name: 'repo-relative test gate',
153
+ value: {
154
+ taskId: 'T121',
155
+ req: 'PARTNER-121',
156
+ gate: '{"kind":"test","command":"pnpm","args":["--filter","app","exec","vitest","run","src/a.test.ts"],"expect":"pass","description":"Task tests pass","req":"PARTNER-121"}',
157
+ reason: 'gate pointed at a removed worktree',
158
+ },
159
+ },
160
+ ],
161
+ },
162
+ },
103
163
  {
104
164
  gateway: 'query',
105
165
  domain: 'tasks',
@@ -1849,6 +1909,14 @@ export const OPERATIONS: OperationDef[] = [
1849
1909
  required: false,
1850
1910
  description: 'Actor attribution recorded on each gate result',
1851
1911
  },
1912
+ {
1913
+ name: 'req',
1914
+ type: 'string',
1915
+ required: false,
1916
+ description:
1917
+ 'Comma-separated REQ-IDs: run and cache only these typed gates; the others are not run (T13486)',
1918
+ cli: { flag: 'req' },
1919
+ },
1852
1920
  ] satisfies ParamDef[],
1853
1921
  },
1854
1922
  // check.archive.stats incoming from admin (T5615)
@@ -8510,6 +8578,56 @@ export const OPERATIONS: OperationDef[] = [
8510
8578
  },
8511
8579
  ],
8512
8580
  },
8581
+ // ── docs.doctor (T13447 — Epic T13340 / Saga T13339) ──────────────────────
8582
+ {
8583
+ gateway: 'query',
8584
+ domain: 'docs',
8585
+ operation: 'doctor',
8586
+ description:
8587
+ 'docs.doctor (query) — docs store health diagnostics (dry-run): dangling local-file ' +
8588
+ 'pointers, docVersion skew vs the audit log, empty topics/related_tasks provenance, ' +
8589
+ 'docs_wikilinks drift, legacy store surfaces, stale drafts. Reports a repair plan.',
8590
+ tier: 1,
8591
+ idempotent: true,
8592
+ sessionRequired: false,
8593
+ requiredParams: [],
8594
+ params: [
8595
+ {
8596
+ name: 'olderThanDays',
8597
+ type: 'number' as const,
8598
+ required: false,
8599
+ description: 'Age threshold in days for the stale-drafts check (default 30)',
8600
+ },
8601
+ ],
8602
+ },
8603
+ {
8604
+ gateway: 'mutate',
8605
+ domain: 'docs',
8606
+ operation: 'doctor',
8607
+ description:
8608
+ 'docs.doctor (mutate) — apply the docs doctor repairs: set docVersion from the audit ' +
8609
+ 'log, backfill topics/related_tasks from content, archive dangling local-file rows, ' +
8610
+ 'rebuild docs_wikilinks. The dispatch layer creates a fresh backup (cleo backup add ' +
8611
+ 'path) and requires its receipt before any write.',
8612
+ tier: 1,
8613
+ idempotent: false,
8614
+ sessionRequired: false,
8615
+ requiredParams: [],
8616
+ params: [
8617
+ {
8618
+ name: 'apply',
8619
+ type: 'boolean' as const,
8620
+ required: false,
8621
+ description: 'Execute repairs (default false — dry-run plan only)',
8622
+ },
8623
+ {
8624
+ name: 'olderThanDays',
8625
+ type: 'number' as const,
8626
+ required: false,
8627
+ description: 'Age threshold in days for the stale-drafts check (default 30)',
8628
+ },
8629
+ ],
8630
+ },
8513
8631
  // ── docs.generate (T798) ─────────────────────────────────────────────────
8514
8632
  {
8515
8633
  gateway: 'query',
package/src/done-plan.ts CHANGED
@@ -67,6 +67,7 @@ export type DonePlanBlockerCode =
67
67
  | 'tool-failed'
68
68
  | 'typed-gate-failed'
69
69
  | 'test-run-needed'
70
+ | 'qa-run-needed'
70
71
  | 'ac-mapping-needed'
71
72
  | 'manual-gate'
72
73
  | 'epic-rollup'
@@ -57,6 +57,7 @@ export const EVIDENCE_ATOM_KINDS = [
57
57
  'commit',
58
58
  'files',
59
59
  'test-run',
60
+ 'qa-run',
60
61
  'tool',
61
62
  'url',
62
63
  'note',
@@ -126,6 +127,22 @@ export const testRunAtomSchema = z.object({
126
127
  path: z.string().min(1, 'test-run atom requires a non-empty path'),
127
128
  });
128
129
 
130
+ /**
131
+ * `qa-run:<path>` atom — path to a JSON receipt of a native typecheck or lint
132
+ * run (T13427): `{kind, command, exitCode, diagnostics: {errors}, roots,
133
+ * startTime?, tool?: {name, version}}`.
134
+ *
135
+ * Format: `qa-run:<absolute or project-relative path>`. The runtime validator
136
+ * requires exit 0 and zero errors, binds the receipt to the change (fresh,
137
+ * covering every changed code path) and pins the tree hash.
138
+ *
139
+ * @task T13427
140
+ */
141
+ export const qaRunAtomSchema = z.object({
142
+ kind: z.literal('qa-run'),
143
+ path: z.string().min(1, 'qa-run atom requires a non-empty path'),
144
+ });
145
+
129
146
  /**
130
147
  * `tool:<name>` atom — runs a project-resolved canonical tool and accepts
131
148
  * exit-code 0.
@@ -391,6 +408,7 @@ export const satisfiesAtomSchema = z.object({
391
408
  * - `commit` — git commit SHA (T832)
392
409
  * - `files` — touched file paths (T832)
393
410
  * - `test-run` — structured test runner JSON (T832)
411
+ * - `qa-run` — native typecheck/lint receipt JSON (T13427)
394
412
  * - `tool` — project-resolved tool exit code (T832 / T1534)
395
413
  * - `url` — soft external pointer (T832)
396
414
  * - `note` — free-form note (T832)
@@ -408,6 +426,7 @@ export const EvidenceAtomSchema = z.discriminatedUnion('kind', [
408
426
  commitAtomSchema,
409
427
  filesAtomSchema,
410
428
  testRunAtomSchema,
429
+ qaRunAtomSchema,
411
430
  toolAtomSchema,
412
431
  urlAtomSchema,
413
432
  noteAtomSchema,
@@ -506,7 +525,7 @@ export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
506
525
  ],
507
526
  },
508
527
  testsPassed: { oneOf: [['test-run'], ['tool'], ['ci']] },
509
- qaPassed: { oneOf: [['tool'], ['ci']] },
528
+ qaPassed: { oneOf: [['tool'], ['ci'], ['qa-run']] },
510
529
  documented: { oneOf: [['files'], ['url']] },
511
530
  securityPassed: { oneOf: [['tool'], ['note']] },
512
531
  cleanupDone: { oneOf: [['note']] },
@@ -565,6 +584,7 @@ const ATOM_EXAMPLES: Readonly<Record<EvidenceAtomKind, string>> = Object.freeze(
565
584
  commit: 'commit:<sha>',
566
585
  files: 'files:path/a.ts,path/b.ts',
567
586
  'test-run': 'test-run:/tmp/vitest-out.json',
587
+ 'qa-run': 'qa-run:/tmp/typecheck-receipt.json',
568
588
  tool: 'tool:test',
569
589
  url: 'url:https://example.com/docs',
570
590
  note: 'note:<short description>',
@@ -847,6 +867,9 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
847
867
  case 'test-run':
848
868
  atoms.push({ kind: 'test-run', path: payload });
849
869
  break;
870
+ case 'qa-run':
871
+ atoms.push({ kind: 'qa-run', path: payload });
872
+ break;
850
873
  case 'tool':
851
874
  atoms.push({ kind: 'tool', tool: payload });
852
875
  break;
@@ -0,0 +1,43 @@
1
+ /** Hash-guarded Git hook installation and recovery contracts. */
2
+ import { z } from 'zod';
3
+
4
+ /** Canonical names of the three shipped Git checks. */
5
+ export const CLEO_GIT_HOOK_NAMES = ['commit-msg', 'pre-commit', 'pre-push'] as const;
6
+
7
+ /** One recoverable managed Git hook change. */
8
+ export const GitHookChangeSchema = z.object({
9
+ name: z.enum(CLEO_GIT_HOOK_NAMES),
10
+ before: z.string().nullable(),
11
+ beforeMode: z.number().int().min(0).max(0o777).nullable(),
12
+ afterHash: z.string().regex(/^[a-f0-9]{64}$/),
13
+ });
14
+ /** A receipt scoped to the effective Git hook directory. */
15
+ export const GitHookInstallReceiptSchema = z.object({
16
+ schemaVersion: z.literal(1),
17
+ hooksDir: z.string(),
18
+ changes: z.array(GitHookChangeSchema),
19
+ });
20
+ /** One managed change with its recovery image. */
21
+ export type GitHookChange = z.infer<typeof GitHookChangeSchema>;
22
+ /** Installation image used only after matching the current content hash. */
23
+ export type GitHookInstallReceipt = z.infer<typeof GitHookInstallReceiptSchema>;
24
+
25
+ /** Exact hashes of historical shipped templates; marker text is insufficient. */
26
+ export const GitHookLegacyHashesSchema = z.record(
27
+ z.string(),
28
+ z.array(z.string().regex(/^[a-f0-9]{64}$/)),
29
+ );
30
+
31
+ /** Explicit installer controls shared by SDK wrappers and CLI delivery. */
32
+ export interface GitHookInstallOptions {
33
+ /** Override shipped template location. */
34
+ templatesDir?: string;
35
+ /** Deprecated compatibility flag; never bypasses ownership or tracked-path safety. */
36
+ force?: boolean;
37
+ /** Preview without filesystem mutations. */
38
+ dryRun?: boolean;
39
+ /** Restore matching local installation images. */
40
+ rollbackReceipt?: GitHookInstallReceipt;
41
+ /** Explicitly permit writes into an effective hooksPath inside the working tree. */
42
+ allowTrackedHooksPath?: boolean;
43
+ }
package/src/index.ts CHANGED
@@ -85,6 +85,13 @@ export type {
85
85
  CycleTimeDistribution,
86
86
  CycleTimePercentiles,
87
87
  } from './archive.js';
88
+ // === Ask-enforce Stop hook (T13420 / Epic T13418) ===
89
+ export type {
90
+ AskEnforceInput,
91
+ AskEnforceMode,
92
+ AskEnforceSignal,
93
+ AskEnforceVerdict,
94
+ } from './ask-enforce-hook.js';
88
95
  // === Attachment Types ===
89
96
  export type {
90
97
  Attachment,
@@ -613,6 +620,7 @@ export {
613
620
  noteAtomSchema,
614
621
  parseEvidenceString,
615
622
  prAtomSchema,
623
+ qaRunAtomSchema,
616
624
  SATISFIES_TASK_ID_REGEX,
617
625
  SATISFIES_VERSION_PIN_REGEX,
618
626
  satisfiesAtomSchema,
@@ -3158,6 +3166,9 @@ export {
3158
3166
  type CloudLeaseReleaseResult,
3159
3167
  type CloudPushResult,
3160
3168
  type CloudRestoreResult,
3169
+ type CloudSyncPushEnableResult,
3170
+ type CloudSyncResult,
3171
+ type CloudSyncStreamResult,
3161
3172
  type CloudVaultLease,
3162
3173
  type CloudVaultScope,
3163
3174
  type CloudVaultSnapshot,
@@ -101,6 +101,16 @@ export const NEXUS_ACCOUNT_ERROR_CODES = [
101
101
  'E_NEXUS_VAULT_STORE_BUSY',
102
102
  /** A project restore target already holds a different project. */
103
103
  'E_NEXUS_VAULT_TARGET_OCCUPIED',
104
+ // Change-journal push (`cleo sync enable push`, T12343 S4-1b).
105
+ /** The store is not fit to reach genesis (a precondition failed); nothing was cut. */
106
+ 'E_NEXUS_SYNC_REFUSED',
107
+ /**
108
+ * Another device already started this stream's change journal: this store restores its journal
109
+ * checkpoint, then joins the stream (T12999), never writing a second genesis.
110
+ */
111
+ 'E_NEXUS_SYNC_STREAM_JOURNALED',
112
+ /** `cleo cloud sync` found no stream with `sync.push` or `sync.pull` on (`cleo sync enable`). */
113
+ 'E_SYNC_DISABLED',
104
114
  // Projects by name (`cleo cloud restore <name>`, T13102).
105
115
  /** No project of the account has that name, label or id. */
106
116
  'E_NEXUS_PROJECT_NOT_FOUND',
@@ -614,6 +614,20 @@ export interface CloudStatusUndoBudget {
614
614
  readonly exceededAt: string | null;
615
615
  }
616
616
 
617
+ /**
618
+ * The pull's seen-transaction ledger (`_sync_seen_txn`, T13317). The cloud
619
+ * pull prunes each origin's rows below its staged `local_seq` floor (T13318);
620
+ * a pruned transaction delivered again is refused, never applied twice.
621
+ */
622
+ export interface CloudStatusSeenTxns {
623
+ /** Rows held, every stream. */
624
+ readonly rows: number;
625
+ /** Estimated bytes: the stored text and integer payload, without SQLite page overhead. */
626
+ readonly bytes: number;
627
+ /** Rows per stream. */
628
+ readonly byStream: Readonly<Record<string, number>>;
629
+ }
630
+
617
631
  export interface CloudStatusSyncStream {
618
632
  /** Which store. */
619
633
  readonly scope: 'project' | 'global';
@@ -639,6 +653,8 @@ export interface CloudStatusSyncStream {
639
653
  readonly held: CloudStatusHeld;
640
654
  /** Undo held against its budget (§3.5 Rule 2, D5). */
641
655
  readonly undo: CloudStatusUndoBudget;
656
+ /** The seen-transaction ledger's size; it grows until pruning has a proven floor (T13317). */
657
+ readonly seenTxns: CloudStatusSeenTxns;
642
658
  /** Sealed ops not yet handed to the outbox for sending. */
643
659
  readonly unsentOps: CloudSyncFact<number>;
644
660
  /** The last sequence this store pushed to the server. */
@@ -211,6 +211,76 @@ export interface CloudPushResult {
211
211
  warnings: CloudWarning[];
212
212
  }
213
213
 
214
+ /** `cleo sync enable push`: the stream's genesis cut and its genesis checkpoint (T12343 S4-1b). */
215
+ export interface CloudSyncPushEnableResult {
216
+ apiUrl: string;
217
+ scope: CloudVaultScope;
218
+ streamId: string;
219
+ /**
220
+ * `enabled`: the cut was recorded and its genesis checkpoint stored; `resumed`: a cut recorded
221
+ * by an earlier run whose checkpoint push failed was pushed now; `already`: push was already on;
222
+ * `joined`: this store holds the stream's journal checkpoint another device cut, and now pushes its
223
+ * own writes and pulls the stream's after it, with no genesis of its own (T13312).
224
+ */
225
+ status: 'enabled' | 'resumed' | 'already' | 'joined';
226
+ /** The genesis cut: the highest capture seq the checkpoint carries. */
227
+ cut: number;
228
+ /** Transactions sealed before the cut, folded into the checkpoint, and rows given genesis row meta. */
229
+ sealed: number;
230
+ folded: number;
231
+ baselined: Record<string, number>;
232
+ /** The genesis checkpoint (null when `already`). */
233
+ snapshot: CloudVaultSnapshot | null;
234
+ /** The delta segment appended so the counts reconcile with the parent, if one was needed. */
235
+ deltaSegmentSeq: number | null;
236
+ /** The replicaSeq this replica's first journal segment follows (null: it starts at 0). */
237
+ replicaSeqFloor: number | null;
238
+ warnings: CloudWarning[];
239
+ }
240
+
241
+ /** One stream of `cleo cloud sync` (T12996). */
242
+ export interface CloudSyncStreamResult {
243
+ scope: CloudVaultScope;
244
+ streamId: string | null;
245
+ /**
246
+ * `synced`: pushed and pulled what was enabled; `disabled`: neither `sync.push` nor `sync.pull` is on;
247
+ * `paused`: push paused for a device clock ahead of the server's; `not-attached`: this machine has no
248
+ * store on that stream; `refused`: a precondition or the server refused (see `refused`); `failed`: an
249
+ * error stopped this stream (see `refused`); the other streams still synced.
250
+ */
251
+ status: 'synced' | 'disabled' | 'paused' | 'not-attached' | 'refused' | 'failed';
252
+ /** Why this stream did not sync fully, or null. */
253
+ refused: string | null;
254
+ /**
255
+ * Legs with nothing to do, not failures: a flag off, or push before this store has its own genesis
256
+ * (a store that only pulls).
257
+ */
258
+ skipped: string[];
259
+ /** Transactions sealed, segments persisted, segments the server stored (`duplicates` of them retries). */
260
+ sealed: number;
261
+ built: number;
262
+ sent: number;
263
+ duplicates: number;
264
+ /** Segments received, transactions staged, re-deliveries skipped. */
265
+ received: number;
266
+ staged: number;
267
+ redelivered: number;
268
+ /** Transactions applied; still held (pending, clock skew, or a newer schema); in conflict. */
269
+ applied: number;
270
+ held: number;
271
+ conflicts: number;
272
+ /** The stream position this store has staged up to, and the server's head (null when not pulled). */
273
+ after: number | null;
274
+ head: number | null;
275
+ }
276
+
277
+ /** `cleo cloud sync` (T12996): seal, push, pull and apply each attached stream. */
278
+ export interface CloudSyncResult {
279
+ apiUrl: string;
280
+ streams: CloudSyncStreamResult[];
281
+ warnings: CloudWarning[];
282
+ }
283
+
214
284
  /** `cleo cloud pull` / `cleo cloud restore`. */
215
285
  export interface CloudRestoreResult {
216
286
  apiUrl: string;
@@ -372,7 +442,7 @@ export interface CloudConflict {
372
442
  columns: string[];
373
443
  /** The typed rule, guard message or invariant, when there is one. */
374
444
  rule: string | null;
375
- /** What happened to the incoming op: `incoming-applied`, `incoming-dropped`, `row-deleted`, `op-voided`. */
445
+ /** What happened to the incoming op: `incoming-applied`, `incoming-dropped`, `row-deleted`, `op-voided`, `op-held`. */
376
446
  resolution: string;
377
447
  opHlc: string;
378
448
  localHlc: string | null;
@@ -899,6 +899,90 @@ export interface DocsSupersedeResult {
899
899
  reason?: string;
900
900
  }
901
901
 
902
+ // --------------------------------------------------------------------------
903
+ // docs.doctor — store health diagnostics + safe repairs (T13447)
904
+ // --------------------------------------------------------------------------
905
+
906
+ /**
907
+ * Parameters for `docs.doctor`.
908
+ *
909
+ * Dry-run by default; `apply: true` executes the repairs (the dispatch layer
910
+ * creates a fresh backup first and hands the receipt to core — callers never
911
+ * pass one).
912
+ *
913
+ * @task T13447 (Epic T13340 / Saga T13339)
914
+ */
915
+ export interface DocsDoctorParams {
916
+ /** Execute repairs instead of producing a dry-run plan. Default false. */
917
+ apply?: boolean;
918
+ /** Age threshold (days) for the `stale-drafts` diagnostic. Default 30. */
919
+ olderThanDays?: number;
920
+ }
921
+
922
+ /**
923
+ * One `docs.doctor` diagnostic class in the report.
924
+ *
925
+ * @task T13447
926
+ */
927
+ export interface DocsDoctorDiagnostic {
928
+ /** Which check produced this entry. */
929
+ kind:
930
+ | 'dangling-pointers'
931
+ | 'doc-version-skew'
932
+ | 'empty-provenance'
933
+ | 'wikilinks-drift'
934
+ | 'legacy-store-present'
935
+ | 'stale-drafts';
936
+ /** `error` should be fixed promptly; `info` is observational. */
937
+ severity: 'info' | 'warn' | 'error';
938
+ /** Total offenders found (may exceed `items.length`). */
939
+ count: number;
940
+ /** Capped per-item detail. */
941
+ items: ReadonlyArray<Record<string, unknown>>;
942
+ /** True when `count` exceeded the detail cap. */
943
+ truncated: boolean;
944
+ }
945
+
946
+ /**
947
+ * One planned or executed `docs.doctor` repair step.
948
+ *
949
+ * @task T13447
950
+ */
951
+ export interface DocsDoctorRepairAction {
952
+ /** Diagnostic class this repair addresses. */
953
+ kind: DocsDoctorDiagnostic['kind'];
954
+ /** Machine-readable action verb (e.g. `set-doc-version`). */
955
+ action: string;
956
+ /** False on a dry run (the action is a plan, not a write). */
957
+ applied: boolean;
958
+ /** Doc slug the action touched, when row-scoped. */
959
+ slug?: string;
960
+ /** Attachment id the action touched, when row-scoped. */
961
+ attachmentId?: string;
962
+ /** Human-readable detail (old→new values, derived counts, …). */
963
+ detail: string;
964
+ }
965
+
966
+ /**
967
+ * Result of `docs.doctor`.
968
+ *
969
+ * @task T13447
970
+ */
971
+ export interface DocsDoctorResult {
972
+ /** True when repairs were written (`apply: true`). */
973
+ applied: boolean;
974
+ /** Age threshold used for the `stale-drafts` check. */
975
+ olderThanDays: number;
976
+ /** Backup receipt that authorised this run (null on dry runs). */
977
+ backupReceiptPath: string | null;
978
+ /** All six diagnostics, in declaration order. */
979
+ diagnostics: readonly DocsDoctorDiagnostic[];
980
+ /** Repair plan (dry run) or executed repairs (apply). */
981
+ repairs: readonly DocsDoctorRepairAction[];
982
+ /** One-line human summary. */
983
+ summary: string;
984
+ }
985
+
902
986
  // ============================================================================
903
987
  // Discriminated Union (DocsOps)
904
988
  // ============================================================================
@@ -921,7 +1005,8 @@ export type DocsOps =
921
1005
  | { op: 'docs.add'; params: DocsAddParams; result: DocsAddResult }
922
1006
  | { op: 'docs.remove'; params: DocsRemoveParams; result: DocsRemoveResult }
923
1007
  | { op: 'docs.update'; params: DocsUpdateParams; result: DocsUpdateResult }
924
- | { op: 'docs.supersede'; params: DocsSupersedeParams; result: DocsSupersedeResult };
1008
+ | { op: 'docs.supersede'; params: DocsSupersedeParams; result: DocsSupersedeResult }
1009
+ | { op: 'docs.doctor'; params: DocsDoctorParams; result: DocsDoctorResult };
925
1010
 
926
1011
  /**
927
1012
  * Enumeration of all docs domain operation names.
@@ -938,4 +1023,5 @@ export type DocsOp =
938
1023
  | 'docs.add'
939
1024
  | 'docs.remove'
940
1025
  | 'docs.update'
941
- | 'docs.supersede';
1026
+ | 'docs.supersede'
1027
+ | 'docs.doctor';
@@ -561,8 +561,22 @@ export interface OrchestrateSpawnResult {
561
561
  * @task T963
562
562
  */
563
563
  prompt: string;
564
- /** Agent id the spawn is routed to. @task T963 */
564
+ /**
565
+ * The spawned agent's identity: the same id as `worktreeEnv.CLEO_AGENT_ID`,
566
+ * its per-agent session's handle (`agent-<task>`) and the worktree lock
567
+ * holder. Before T13494 this field carried the classifier's persona.
568
+ * @task T963
569
+ * @task T13494
570
+ */
565
571
  agentId: string;
572
+ /**
573
+ * The agent profile the classifier routed the task to (e.g.
574
+ * `project-security-worker`, `cleo-subagent`); mirrors
575
+ * `meta.classify.agentId`. It names what the prompt is written for, not who
576
+ * runs it.
577
+ * @task T13494
578
+ */
579
+ persona: string;
566
580
  /** Role the agent will execute as (`orchestrator` | `lead` | `worker`). @task T963 */
567
581
  role: string;
568
582
  /** Tier of the rendered prompt (0/1/2). @task T963 */
@@ -228,6 +228,11 @@ export interface ValidateGateParams {
228
228
  * none; a gate without a fresh cached pass refuses with `E_GATE_NOT_CACHED`.
229
229
  */
230
230
  noRun?: boolean;
231
+ /**
232
+ * `cleo verify --run --req A,B` (T13486): run and cache only the typed gates
233
+ * with these REQ-IDs (comma-separated); the others are not run.
234
+ */
235
+ req?: string;
231
236
  }
232
237
  export interface ValidateGateResult {
233
238
  taskId: string;