@cleocode/contracts 2026.9.23 → 2026.9.25

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 (85) hide show
  1. package/dist/branch-lock.d.ts +51 -0
  2. package/dist/branch-lock.d.ts.map +1 -1
  3. package/dist/branch-lock.js +25 -3
  4. package/dist/branch-lock.js.map +1 -1
  5. package/dist/cloud/api.d.ts +1 -1
  6. package/dist/data-accessor.d.ts +270 -16
  7. package/dist/data-accessor.d.ts.map +1 -1
  8. package/dist/data-accessor.js.map +1 -1
  9. package/dist/decide.d.ts +110 -1
  10. package/dist/decide.d.ts.map +1 -1
  11. package/dist/decide.js +23 -0
  12. package/dist/decide.js.map +1 -1
  13. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  14. package/dist/dispatch/operations-registry.js +137 -6
  15. package/dist/dispatch/operations-registry.js.map +1 -1
  16. package/dist/doctor.d.ts +9 -0
  17. package/dist/doctor.d.ts.map +1 -1
  18. package/dist/exit-codes.d.ts +7 -0
  19. package/dist/exit-codes.d.ts.map +1 -1
  20. package/dist/exit-codes.js +7 -0
  21. package/dist/exit-codes.js.map +1 -1
  22. package/dist/index.d.ts +10 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/knowledge-health.d.ts +21 -0
  27. package/dist/knowledge-health.d.ts.map +1 -1
  28. package/dist/nexus-account.d.ts +160 -0
  29. package/dist/nexus-account.d.ts.map +1 -0
  30. package/dist/nexus-account.js +68 -0
  31. package/dist/nexus-account.js.map +1 -0
  32. package/dist/operations/focus.d.ts +7 -3
  33. package/dist/operations/focus.d.ts.map +1 -1
  34. package/dist/operations/nexus-scope-map.d.ts +8 -0
  35. package/dist/operations/nexus-scope-map.d.ts.map +1 -1
  36. package/dist/operations/nexus-scope-map.js +8 -0
  37. package/dist/operations/nexus-scope-map.js.map +1 -1
  38. package/dist/operations/nexus.d.ts +265 -0
  39. package/dist/operations/nexus.d.ts.map +1 -1
  40. package/dist/operations/nexus.js +9 -1
  41. package/dist/operations/nexus.js.map +1 -1
  42. package/dist/operations/orchestrate.d.ts +6 -1
  43. package/dist/operations/orchestrate.d.ts.map +1 -1
  44. package/dist/operations/output-contracts-data.d.ts.map +1 -1
  45. package/dist/operations/output-contracts-data.js +115 -1
  46. package/dist/operations/output-contracts-data.js.map +1 -1
  47. package/dist/operations/tasks.d.ts +73 -9
  48. package/dist/operations/tasks.d.ts.map +1 -1
  49. package/dist/operations/tasks.js.map +1 -1
  50. package/dist/project-lifecycle.d.ts +6 -2
  51. package/dist/project-lifecycle.d.ts.map +1 -1
  52. package/dist/row-identity.d.ts +128 -0
  53. package/dist/row-identity.d.ts.map +1 -0
  54. package/dist/row-identity.js +20 -0
  55. package/dist/row-identity.js.map +1 -0
  56. package/dist/session.d.ts +19 -1
  57. package/dist/session.d.ts.map +1 -1
  58. package/dist/session.js +9 -2
  59. package/dist/session.js.map +1 -1
  60. package/dist/task.d.ts +56 -2
  61. package/dist/task.d.ts.map +1 -1
  62. package/dist/tools/score-task-priority.d.ts +1 -1
  63. package/dist/tools/score-task-priority.d.ts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  66. package/src/branch-lock.ts +80 -3
  67. package/src/data-accessor.ts +283 -17
  68. package/src/decide.ts +127 -1
  69. package/src/dispatch/operations-registry.ts +144 -6
  70. package/src/doctor.ts +9 -0
  71. package/src/exit-codes.ts +7 -0
  72. package/src/index.ts +56 -0
  73. package/src/knowledge-health.ts +22 -0
  74. package/src/nexus-account.ts +181 -0
  75. package/src/operations/focus.ts +7 -3
  76. package/src/operations/nexus-scope-map.ts +9 -0
  77. package/src/operations/nexus.ts +281 -0
  78. package/src/operations/orchestrate.ts +6 -2
  79. package/src/operations/output-contracts-data.ts +125 -1
  80. package/src/operations/tasks.ts +78 -12
  81. package/src/project-lifecycle.ts +6 -2
  82. package/src/row-identity.ts +132 -0
  83. package/src/session.ts +20 -2
  84. package/src/task.ts +58 -2
  85. package/src/tools/score-task-priority.ts +1 -1
@@ -124,6 +124,10 @@ export interface NexusProjectRecord {
124
124
  lastIndexed: string | null;
125
125
  /** Code intelligence stats from the last index run. */
126
126
  stats: NexusProjectStats;
127
+ /** ISO 8601 instant of the last health check, sync or git probe (T12512). */
128
+ lastProbedAt?: string | null;
129
+ /** ISO 8601 instant of the last real CLI use inside the project (T12512). */
130
+ lastOpenedAt?: string | null;
127
131
  }
128
132
 
129
133
  /** A single cross-project task reference tuple. */
@@ -1383,6 +1387,12 @@ export interface NexusProjectGitState {
1383
1387
  branch: string | null;
1384
1388
  /** HEAD commit; `null` for an unborn branch or when the probe failed. */
1385
1389
  headSha: string | null;
1390
+ /**
1391
+ * ISO 8601 UTC instant HEAD was committed (`git log -1 --format=%cI`,
1392
+ * normalized to `Z`); `null` for an unborn branch or when the probe failed
1393
+ * before reading it (T12721).
1394
+ */
1395
+ headCommittedAt: string | null;
1386
1396
  /** `true` when HEAD is detached. */
1387
1397
  detached: boolean;
1388
1398
  /** `true` for a shallow clone (ahead/behind may be truncated by the graft). */
@@ -1464,6 +1474,276 @@ export interface NexusProjectsStatusResult {
1464
1474
  };
1465
1475
  }
1466
1476
 
1477
+ /**
1478
+ * A condition on one project location in the fleet view (T12513).
1479
+ *
1480
+ * - `missing` — the location is recorded `missing`, or its last probe found no directory.
1481
+ * - `dirty` — the last probe saw tracked changes or untracked files.
1482
+ * - `behind` / `ahead` — the upstream had commits HEAD lacks / HEAD has commits
1483
+ * the upstream lacks, as of the LAST FETCH (see `remote.fetchedAt`).
1484
+ * - `stale` — never probed, probed longer ago than the staleness window, or its
1485
+ * remote was last fetched longer ago than the window (or never).
1486
+ * - `errored` — the last probe recorded a probe error.
1487
+ * - `unprobed` — no probe row exists for the location yet.
1488
+ */
1489
+ export type NexusFleetFlag =
1490
+ | 'missing'
1491
+ | 'dirty'
1492
+ | 'behind'
1493
+ | 'ahead'
1494
+ | 'stale'
1495
+ | 'errored'
1496
+ | 'unprobed';
1497
+
1498
+ /** Parameters for `nexus.projects.fleet` (T12513). Filters combine with AND. */
1499
+ export interface NexusProjectsFleetParams {
1500
+ /** Keep only locations on this device: a device id, a hostname, or `current`. */
1501
+ device?: string;
1502
+ /** Keep projects with a `missing` location. */
1503
+ missing?: boolean;
1504
+ /** Keep projects with a `dirty` location. */
1505
+ dirty?: boolean;
1506
+ /** Keep projects with a location `behind` its upstream (as of the last fetch). */
1507
+ behind?: boolean;
1508
+ /** Keep projects with a location `ahead` of its upstream (as of the last fetch). */
1509
+ ahead?: boolean;
1510
+ /** Keep projects with a `stale` location. */
1511
+ stale?: boolean;
1512
+ /** Keep projects with a location whose last probe errored. */
1513
+ errored?: boolean;
1514
+ /** Staleness window for probes, fetches and heartbeats, ms. Default 24h. */
1515
+ staleAfterMs?: number;
1516
+ /** Page size (projects). Default 50, max 500; `0` returns every match. */
1517
+ limit?: number;
1518
+ /** Projects skipped before the page. Default 0. */
1519
+ offset?: number;
1520
+ }
1521
+
1522
+ /** One device as the fleet view shows it (T12513). */
1523
+ export interface NexusFleetDevice {
1524
+ /** Stable device id (`<cleoHome>/device-id`). */
1525
+ deviceId: string;
1526
+ /**
1527
+ * Hostname at the last heartbeat; `null` when the device never sent one.
1528
+ * **Device-local, never mirrored** to the cloud.
1529
+ */
1530
+ hostname: string | null;
1531
+ /** Operating system at the last heartbeat. */
1532
+ os: string | null;
1533
+ /** CPU architecture at the last heartbeat. */
1534
+ arch: string | null;
1535
+ /** CLEO version at the last heartbeat. */
1536
+ cleoVersion: string | null;
1537
+ /** ISO 8601 instant of the last heartbeat; `null` if never. */
1538
+ lastHeartbeatAt: string | null;
1539
+ /** `true` when the last heartbeat is unknown or older than the staleness window. */
1540
+ heartbeatStale: boolean;
1541
+ /** `true` for the device running this command. */
1542
+ current: boolean;
1543
+ }
1544
+
1545
+ /** Per-device counts in the fleet view (T12513). */
1546
+ export interface NexusFleetDeviceSummary extends NexusFleetDevice {
1547
+ /** Visible (live or missing) locations on the device. */
1548
+ locations: number;
1549
+ /** Of those, locations flagged `missing`. */
1550
+ missing: number;
1551
+ /** Of those, locations flagged `dirty`. */
1552
+ dirty: number;
1553
+ /** Of those, locations flagged `behind`. */
1554
+ behind: number;
1555
+ /** Of those, locations flagged `stale`. */
1556
+ stale: number;
1557
+ }
1558
+
1559
+ /**
1560
+ * Version of the fleet/presence shape a {@link NexusProjectsFleetResult} and
1561
+ * the cloud `ReplicaPresence` built from it conform to (T12721). A positive
1562
+ * integer, sent as `ReplicaPresence.schemaVersion`; bump it when a field the
1563
+ * presence mapper reads changes meaning.
1564
+ *
1565
+ * - `1` — T12721: `headCommittedAt`, `replicaId`, device-local field markers.
1566
+ */
1567
+ export const NEXUS_FLEET_SCHEMA_VERSION = 1;
1568
+
1569
+ /**
1570
+ * Last recorded git state of one location (T12513, from T12511's probe rows).
1571
+ *
1572
+ * The cloud mirrors only a path-free projection of it (`ReplicaPresence`,
1573
+ * built by core's `toReplicaPresence`): dirty, ahead, behind, a remote-sync
1574
+ * enum and `headCommittedAt`. Every field marked **device-local, never
1575
+ * mirrored** stays on the device; `branch` leaves it only on explicit opt-in.
1576
+ */
1577
+ export interface NexusFleetGitSummary {
1578
+ /**
1579
+ * Checked-out branch; `null` when detached or unknown. **Mirrored only on
1580
+ * explicit opt-in** (branch names can be sensitive).
1581
+ */
1582
+ branch: string | null;
1583
+ /** HEAD commit. **Device-local, never mirrored.** */
1584
+ headSha: string | null;
1585
+ /**
1586
+ * ISO 8601 UTC instant HEAD was committed; `null` when unknown (unborn
1587
+ * branch, failed probe, or a row probed before T12721). Mirrored as
1588
+ * `ReplicaPresence.git.lastCommitAt`.
1589
+ */
1590
+ headCommittedAt: string | null;
1591
+ /** `true` when HEAD is detached. */
1592
+ detached: boolean;
1593
+ /** Tracked entries with changes. */
1594
+ dirtyCount: number | null;
1595
+ /** Untracked, not-ignored entries. */
1596
+ untrackedCount: number | null;
1597
+ /** Remote side, as of the last fetch — never "now". */
1598
+ remote: {
1599
+ /** Remote name. **Device-local, never mirrored.** */
1600
+ name: string | null;
1601
+ /** Remote URL, credentials removed. **Device-local, never mirrored.** */
1602
+ url: string | null;
1603
+ /** Upstream ref (e.g. `origin/main`). **Device-local, never mirrored.** */
1604
+ upstream: string | null;
1605
+ /** Upstream tracking-ref commit. **Device-local, never mirrored.** */
1606
+ headSha: string | null;
1607
+ /** Commits on HEAD not on the upstream. */
1608
+ ahead: number | null;
1609
+ /** Commits on the upstream not on HEAD. */
1610
+ behind: number | null;
1611
+ /** ISO 8601 instant of the last fetch; `null` if never. */
1612
+ fetchedAt: string | null;
1613
+ /** `true` when the last fetch is unknown or older than the staleness window. */
1614
+ stale: boolean;
1615
+ };
1616
+ /** ISO 8601 instant of the probe that recorded this state. */
1617
+ probedAt: string;
1618
+ /** `true` when `probedAt` is older than the staleness window. */
1619
+ probeStale: boolean;
1620
+ /** Probe error code, `null` on success. */
1621
+ probeErrorCode: NexusGitProbeErrorCode | null;
1622
+ /**
1623
+ * Probe error detail (git's stderr, an errno, a path). **Device-local, never
1624
+ * mirrored**: presence carries only `remote: 'unknown'` for an errored probe.
1625
+ */
1626
+ probeError: string | null;
1627
+ }
1628
+
1629
+ /** One place a project lives, on one device (T12513). */
1630
+ export interface NexusFleetLocation {
1631
+ /** Device the location is on. */
1632
+ deviceId: string;
1633
+ /**
1634
+ * Replica identity of this location in the cloud (the `:replicaId` of
1635
+ * `PUT /v1/projects/:projectId/replicas/:replicaId/presence`), or `null`.
1636
+ *
1637
+ * Always `null` today: the store has no stable per-replica id yet. T12675's
1638
+ * store-instance id becomes this `replicaId` (it must satisfy the cloud
1639
+ * `ReplicaId` shape, a UUIDv7). It is never derived from the path, the
1640
+ * device id or a hash of either — a derived id changes on a move and is not
1641
+ * stable. A caller must not send presence for a location whose `replicaId`
1642
+ * is `null`.
1643
+ */
1644
+ replicaId: string | null;
1645
+ /**
1646
+ * Hostname of that device, when it has sent a heartbeat. **Device-local,
1647
+ * never mirrored.**
1648
+ */
1649
+ hostname: string | null;
1650
+ /** `true` when the location is on the device running this command. */
1651
+ current: boolean;
1652
+ /** Checkout root on that device. **Device-local, never mirrored.** */
1653
+ path: string;
1654
+ /** Location lifecycle state. */
1655
+ state: 'live' | 'missing';
1656
+ /** ISO 8601 instant the location was last encountered. */
1657
+ lastSeen: string;
1658
+ /** Last recorded git state; `null` when never probed. */
1659
+ git: NexusFleetGitSummary | null;
1660
+ /** Conditions that hold for this location. */
1661
+ flags: NexusFleetFlag[];
1662
+ }
1663
+
1664
+ /** One project in the fleet view (T12513). */
1665
+ export interface NexusFleetProject {
1666
+ /** Immutable project id. */
1667
+ projectId: string;
1668
+ /** Registered name (not unique). */
1669
+ name: string;
1670
+ /** ISO 8601 instant of the last real CLI use; `null` if never recorded. */
1671
+ lastOpenedAt: string | null;
1672
+ /** ISO 8601 instant of the last health check, sync or git probe; `null` if never. */
1673
+ lastProbedAt: string | null;
1674
+ /** Number of distinct devices holding a visible location. */
1675
+ deviceCount: number;
1676
+ /** Union of the flags of its locations. */
1677
+ flags: NexusFleetFlag[];
1678
+ /** Every visible location (within the `device` filter), by device then path. */
1679
+ locations: NexusFleetLocation[];
1680
+ }
1681
+
1682
+ /**
1683
+ * Result of `nexus.projects.fleet` (T12513). Counts come first; `projects` is
1684
+ * one page. Read-only: it reports the probe rows recorded by
1685
+ * `nexus.projects.status` and never runs git.
1686
+ */
1687
+ export interface NexusProjectsFleetResult {
1688
+ /** Projects in the registry. */
1689
+ total: number;
1690
+ /** Projects matching the filters. */
1691
+ matched: number;
1692
+ /** Projects on this page. */
1693
+ returned: number;
1694
+ /** Offset of this page. */
1695
+ offset: number;
1696
+ /** Page size applied (`0` = unbounded). */
1697
+ limit: number;
1698
+ /** `true` when more matches follow this page. */
1699
+ hasMore: boolean;
1700
+ /** Staleness window applied, ms. */
1701
+ staleAfterMs: number;
1702
+ /** ISO 8601 instant staleness was evaluated at. */
1703
+ generatedAt: string;
1704
+ /** Device the command ran on. */
1705
+ currentDeviceId: string;
1706
+ /**
1707
+ * Fleet/presence shape version this result conforms to
1708
+ * ({@link NEXUS_FLEET_SCHEMA_VERSION}); sent as `ReplicaPresence.schemaVersion`.
1709
+ */
1710
+ schemaVersion: number;
1711
+ /**
1712
+ * Fleet-wide counts of PROJECTS with at least one location in each
1713
+ * condition. Scoped by `device` ONLY — the flag filters (`missing`, `dirty`,
1714
+ * `behind`, `ahead`, `stale`, `errored`) never narrow it, so it always shows
1715
+ * the whole fleet (or the whole device) while `matched` counts the filter.
1716
+ */
1717
+ summary: {
1718
+ /** Projects with a visible location. */
1719
+ located: number;
1720
+ /** Distinct visible locations. */
1721
+ locations: number;
1722
+ /** Projects flagged per condition. */
1723
+ missing: number;
1724
+ /** Projects with a dirty location. */
1725
+ dirty: number;
1726
+ /** Projects with a location behind its upstream. */
1727
+ behind: number;
1728
+ /** Projects with a location ahead of its upstream. */
1729
+ ahead: number;
1730
+ /** Projects with a stale location. */
1731
+ stale: number;
1732
+ /** Projects with an errored probe. */
1733
+ errored: number;
1734
+ /** Projects with a never-probed location. */
1735
+ unprobed: number;
1736
+ };
1737
+ /**
1738
+ * Every device holding a visible location or known by heartbeat, with
1739
+ * per-device location counts. Like `summary`, scoped by `device` only —
1740
+ * never by the flag filters.
1741
+ */
1742
+ devices: NexusFleetDeviceSummary[];
1743
+ /** This page of projects, by name then id. */
1744
+ projects: NexusFleetProject[];
1745
+ }
1746
+
1467
1747
  /** Parameters for `nexus.projects.register`. */
1468
1748
  export interface NexusProjectsRegisterParams {
1469
1749
  /** Path to the project directory (required). */
@@ -2115,6 +2395,7 @@ export type NexusOps = {
2115
2395
  readonly 'projects.scan': readonly [NexusProjectsScanParams, NexusProjectsScanResult];
2116
2396
  readonly 'projects.clean': readonly [NexusProjectsCleanParams, NexusProjectsCleanResult];
2117
2397
  readonly 'projects.status': readonly [NexusProjectsStatusParams, NexusProjectsStatusResult];
2398
+ readonly 'projects.fleet': readonly [NexusProjectsFleetParams, NexusProjectsFleetResult];
2118
2399
  readonly 'refresh-bridge': readonly [NexusRefreshBridgeParams, NexusRefreshBridgeResult];
2119
2400
  readonly diff: readonly [NexusDiffParams, NexusDiffResult];
2120
2401
  readonly 'query-cte': readonly [NexusQueryCteParams, NexusQueryCteResult];
@@ -432,8 +432,12 @@ export interface OrchestrateBootstrapParams {
432
432
  speed?: 'fast' | 'full' | 'complete';
433
433
  }
434
434
  export interface BrainState {
435
- /** Currently active session summary. @task T963 */
436
- session?: { id: string; name: string; status: string; startedAt: string };
435
+ /**
436
+ * Session summary: the caller's bound session, else the newest active row.
437
+ * `unbound: true` marks the latter, which may be another agent's (T12500).
438
+ * @task T963
439
+ */
440
+ session?: { id: string; name: string; status: string; startedAt: string; unbound?: boolean };
437
441
  /** Current task context. @task T963 */
438
442
  currentTask?: { id: string; title: string; status: string };
439
443
  /** Suggested next task with score. @task T963 */
@@ -63,6 +63,26 @@ import {
63
63
  * exact shape whose absence produced `cleo show --field /data/title` →
64
64
  * `E_FIELD_NOT_FOUND`.
65
65
  */
66
+ /**
67
+ * JSON schema of a {@link TaskClaim} lease (T12502), shared by `tasks.show`,
68
+ * `tasks.start` and `tasks.claim`.
69
+ */
70
+ const TASK_CLAIM_SCHEMA = {
71
+ type: 'object',
72
+ description:
73
+ 'Agent claim lease (T12502): the holder session, its agent, and when the lease lapses unless renewed. Absent when unclaimed.',
74
+ required: ['sessionId', 'agentId', 'claimedAt', 'leaseExpiresAt'],
75
+ properties: {
76
+ sessionId: { type: 'string', description: 'Session holding the lease.' },
77
+ agentId: { type: ['string', 'null'], description: 'Agent identity of the holder.' },
78
+ claimedAt: { type: 'string', description: 'When the lease was taken (ISO-8601 UTC).' },
79
+ leaseExpiresAt: {
80
+ type: 'string',
81
+ description: 'When the lease lapses unless renewed (ISO-8601 UTC).',
82
+ },
83
+ },
84
+ } as const;
85
+
66
86
  const tasksShowOutputContract: OperationOutputContract = {
67
87
  operation: 'tasks.show',
68
88
  shapeNote:
@@ -70,6 +90,8 @@ const tasksShowOutputContract: OperationOutputContract = {
70
90
  'Mutation envelopes are FLAT by contrast (/data/created/0, /data/updated/0), which is ' +
71
91
  'the asymmetry that produced GH #1225/#1239/#1231. ' +
72
92
  '`view` may be null. `acRows` and `relations` are conditional. ' +
93
+ 'The default (MVI) projection withholds each `acRows` UUID and names it once in ' +
94
+ '`/data/_withheld` as `acRows/*/id` (T12523); cite a criterion by `alias` (AC1..n). ' +
73
95
  '`description`, `acceptance` and `verification` are withheld by the default (MVI) ' +
74
96
  'projection but `--field` resolves them transparently (T12108) — no `--full` needed. ' +
75
97
  'Evidence is NOT a task field: it lives at /data/task/verification/evidence ' +
@@ -92,6 +114,18 @@ const tasksShowOutputContract: OperationOutputContract = {
92
114
  priority: { type: 'string' },
93
115
  type: { type: 'string' },
94
116
  parentId: { type: ['string', 'null'] },
117
+ // T12503: the optimistic-concurrency version agents pass to --if-match.
118
+ updatedAt: {
119
+ type: ['string', 'null'],
120
+ description:
121
+ 'Task version for optimistic concurrency: pass it to `cleo update|complete --if-match`.',
122
+ },
123
+ // T12502: the human assignee and the agent claim lease are separate.
124
+ assignee: {
125
+ type: ['string', 'null'],
126
+ description: 'Human / owner assignee. Separate from `claim`.',
127
+ },
128
+ claim: TASK_CLAIM_SCHEMA,
95
129
  },
96
130
  },
97
131
  view: {
@@ -105,7 +139,13 @@ const tasksShowOutputContract: OperationOutputContract = {
105
139
  },
106
140
  acRows: {
107
141
  type: 'array',
108
- description: 'Acceptance-criterion rows (id, alias AC<n>, ordinal, text). Optional.',
142
+ description:
143
+ 'Acceptance-criterion rows (alias AC<n>, ordinal, text; the UUID id only with --full). Optional.',
144
+ },
145
+ _withheld: {
146
+ type: 'object',
147
+ description:
148
+ 'Fields the default projection withheld from list elements, e.g. `acRows/*/id` → UTF-8 byte total. Absent with --full.',
109
149
  },
110
150
  relations: {
111
151
  type: 'object',
@@ -120,6 +160,14 @@ const tasksShowOutputContract: OperationOutputContract = {
120
160
  '/data/task/priority',
121
161
  '/data/task/type',
122
162
  '/data/task/parentId',
163
+ // T12503 — the optimistic-concurrency version for `--if-match`; withheld
164
+ // by the MVI projection, resolved from the full record by `--field`.
165
+ '/data/task/updatedAt',
166
+ // T12502 — the agent claim lease (withheld by MVI; resolved by --field).
167
+ '/data/task/assignee',
168
+ '/data/task/claim',
169
+ '/data/task/claim/sessionId',
170
+ '/data/task/claim/leaseExpiresAt',
123
171
  // T12127 (GH #1231) — withheld by the MVI projection but resolvable
124
172
  // through `--field` since T12108. They were absent from this list, so the
125
173
  // remediation an agent is shown on a failed pointer never mentioned the
@@ -494,6 +542,78 @@ const tasksAssigneeOutputContract: OperationOutputContract = {
494
542
  fieldPointers: ['/data/taskId', '/data/assignee', '/data/assigned'],
495
543
  };
496
544
 
545
+ /**
546
+ * OUTPUT contract for `tasks.start` (T12502).
547
+ *
548
+ * Grounded in `TasksStartQueryResult`: `{ taskId, previousTask, claim }`.
549
+ */
550
+ const tasksStartOutputContract: OperationOutputContract = {
551
+ operation: 'tasks.start',
552
+ shapeNote:
553
+ 'The claim lease this start holds is at /data/claim (null when the caller is not bound to a session). ' +
554
+ 'Another session holding the task fails with E_TASK_CLAIMED (exit 35); error.details.holder names it.',
555
+ dataSchema: {
556
+ type: 'object',
557
+ required: ['taskId', 'previousTask'],
558
+ additionalProperties: true,
559
+ properties: {
560
+ taskId: { type: 'string', description: 'The task now active.' },
561
+ previousTask: { type: ['string', 'null'], description: 'The task active before, if any.' },
562
+ claim: { ...TASK_CLAIM_SCHEMA, type: ['object', 'null'] },
563
+ },
564
+ },
565
+ fieldPointers: [
566
+ '/data/taskId',
567
+ '/data/previousTask',
568
+ '/data/claim',
569
+ '/data/claim/sessionId',
570
+ '/data/claim/leaseExpiresAt',
571
+ ],
572
+ };
573
+
574
+ /**
575
+ * OUTPUT contract for `tasks.claim` (T12502).
576
+ *
577
+ * Grounded in `TasksClaimResult`: `{ taskId, agentId, claim }`.
578
+ */
579
+ const tasksClaimOutputContract: OperationOutputContract = {
580
+ operation: 'tasks.claim',
581
+ shapeNote:
582
+ 'The lease now held by your session is at /data/claim; renew it with --renew. ' +
583
+ 'Another holder fails with E_TASK_CLAIMED (exit 35): --take-over (expired) or --force-claim (live).',
584
+ dataSchema: {
585
+ type: 'object',
586
+ required: ['taskId', 'agentId', 'claim'],
587
+ additionalProperties: true,
588
+ properties: {
589
+ taskId: { type: 'string', description: 'The claimed task.' },
590
+ agentId: { type: ['string', 'null'], description: 'Agent recorded with the lease.' },
591
+ claim: TASK_CLAIM_SCHEMA,
592
+ },
593
+ },
594
+ fieldPointers: ['/data/taskId', '/data/agentId', '/data/claim', '/data/claim/leaseExpiresAt'],
595
+ };
596
+
597
+ /**
598
+ * OUTPUT contract for `tasks.unclaim` (T12502).
599
+ *
600
+ * Grounded in `TasksUnclaimResult`: `{ taskId, released }`.
601
+ */
602
+ const tasksUnclaimOutputContract: OperationOutputContract = {
603
+ operation: 'tasks.unclaim',
604
+ shapeNote: '/data/released is true when a lease was cleared, false when the task was unclaimed.',
605
+ dataSchema: {
606
+ type: 'object',
607
+ required: ['taskId', 'released'],
608
+ additionalProperties: true,
609
+ properties: {
610
+ taskId: { type: 'string', description: 'The released task.' },
611
+ released: { type: 'boolean', description: 'Whether a lease was cleared.' },
612
+ },
613
+ },
614
+ fieldPointers: ['/data/taskId', '/data/released'],
615
+ };
616
+
497
617
  // ---------------------------------------------------------------------------
498
618
  // admin.config.* — config-as-domain (T11917 · M5/AC3 · ConfigManifest cascade)
499
619
  // ---------------------------------------------------------------------------
@@ -649,6 +769,10 @@ export const OUTPUT_CONTRACTS: OperationOutputContractRegistry = {
649
769
  'tasks.reorder-rank': tasksReorderRankOutputContract,
650
770
  'tasks.bulk-move': tasksBulkMoveOutputContract,
651
771
  'tasks.assignee': tasksAssigneeOutputContract,
772
+ // T12502 — leased task claims.
773
+ 'tasks.start': tasksStartOutputContract,
774
+ 'tasks.claim': tasksClaimOutputContract,
775
+ 'tasks.unclaim': tasksUnclaimOutputContract,
652
776
  'admin.config.get': adminConfigGetOutputContract,
653
777
  'admin.config.list': adminConfigListOutputContract,
654
778
  'admin.config.validate': adminConfigValidateOutputContract,
@@ -25,8 +25,13 @@ import type { TaskAnalysisResult, TaskRef } from '../results.js';
25
25
  * Common task types (API contract — matches CLI src/types/task.ts)
26
26
  */
27
27
  import type { TaskStatus } from '../status-registry.js';
28
- import type { TaskKind, TaskPriority, TaskSeverity, TaskType } from '../task.js';
29
- import type { MinimalTaskRecord, TaskMatch, TaskRecord } from '../task-record.js';
28
+ import type { TaskClaim, TaskKind, TaskPriority, TaskSeverity, TaskType } from '../task.js';
29
+ import type {
30
+ MinimalTaskRecord,
31
+ RecordProjectionDisclosure,
32
+ TaskMatch,
33
+ TaskRecord,
34
+ } from '../task-record.js';
30
35
  import type { ExternalTask, ExternalTaskLink, ReconcileResult } from '../task-sync.js';
31
36
  import type {
32
37
  CompletionEvaluateParams,
@@ -285,10 +290,16 @@ export interface TaskShowRelationsEntry {
285
290
  * Result of `tasks.show` — the full task record plus its canonical view
286
291
  * projection. `view` is null when the task has no lifecycle pipeline.
287
292
  *
293
+ * The inherited `_withheld` marker is set by the default (MVI) projection
294
+ * when it withholds a field from every `acRows` element: the key
295
+ * `acRows/*\/id` names the row UUIDs and its value is their UTF-8 byte total
296
+ * (T12523). `--full` returns the rows unprojected, with no marker.
297
+ *
288
298
  * @task T1703
289
299
  * @task T9966 — attachments[] always present (empty array when none)
300
+ * @task T12523 — `_withheld` for the projected `acRows`
290
301
  */
291
- export interface TasksShowResult {
302
+ export interface TasksShowResult extends RecordProjectionDisclosure {
292
303
  /** Full task record (string-widened for dispatch layer serialization). */
293
304
  task: TaskRecord;
294
305
  /** Canonical task view projection produced by `computeTaskView`. Null when unavailable. */
@@ -307,6 +318,8 @@ export interface TasksShowResult {
307
318
  * Acceptance-criterion rows hydrated from the `task_acceptance_criteria`
308
319
  * table (T10502). Each entry carries the stable UUID `id`, the
309
320
  * `AC<ordinal>` alias, the ordinal itself, and the canonical AC text.
321
+ * The default (MVI) projection withholds `id` and says so in this result's
322
+ * `_withheld` (`acRows/*\/id`); `--full` returns it (T12523).
310
323
  *
311
324
  * Optional — undefined when the task has no rows in the table (e.g.
312
325
  * legacy tasks not yet backfilled by T10505). Consumers should fall
@@ -329,8 +342,14 @@ export interface TasksShowResult {
329
342
  * @task T10508
330
343
  */
331
344
  export interface TaskShowAcRowEntry {
332
- /** UUIDv4 stable identifier, immutable for the AC's lifetime. */
333
- id: string;
345
+ /**
346
+ * UUIDv4 stable identifier, immutable for the AC's lifetime.
347
+ *
348
+ * Absent under the default (MVI) projection, which cites a criterion by its
349
+ * `alias` and lists `acRows/*\/id` in the result's `_withheld`; present with
350
+ * `--full` or a `--field /data/acRows/<i>/id` pointer (T12523).
351
+ */
352
+ id?: string;
334
353
  /** Display alias derived from ordinal — `AC1`, `AC2`, etc. */
335
354
  alias: string;
336
355
  /** 1-based ordinal — never reused per task (gaps remain on shrink). */
@@ -1680,6 +1699,13 @@ export interface TasksCompleteQueryParams {
1680
1699
  * @task T11954 (DHQ-071)
1681
1700
  */
1682
1701
  waiveDependsReason?: string;
1702
+ /**
1703
+ * Optimistic-concurrency guard (`--if-match`): the task `updatedAt` the
1704
+ * caller read. When the stored version differs, completion fails with
1705
+ * `E_CONFLICT` carrying the current version and the changed fields.
1706
+ * @task T12503
1707
+ */
1708
+ expectedUpdatedAt?: string;
1683
1709
  }
1684
1710
  /**
1685
1711
  * Result of `tasks.complete` — completion confirmation with unblocked tasks.
@@ -1824,51 +1850,91 @@ export interface TasksArchiveQueryResult {
1824
1850
  archivedTasks: Array<{ id: string }>;
1825
1851
  }
1826
1852
 
1827
- // tasks.claim
1853
+ /**
1854
+ * Params for `tasks.claim` — take, renew or override the caller session's
1855
+ * leased claim on a task (T12502). The human `assignee` is never written.
1856
+ *
1857
+ * @task T1703
1858
+ * @task T12502
1859
+ */
1828
1860
  export interface TasksClaimParams {
1861
+ /** Task to claim. */
1829
1862
  taskId: string;
1830
- agentId: string;
1863
+ /** Agent identity recorded with the lease; defaults to the session's agent / `CLEO_AGENT_ID`. */
1864
+ agentId?: string;
1865
+ /** Renew the caller's own lease instead of taking one (`--renew`). */
1866
+ renew?: boolean;
1867
+ /** Take over an EXPIRED lease held by another session (`--take-over`, audited). */
1868
+ takeOver?: boolean;
1869
+ /** Take over a LIVE lease held by another session (`--force-claim`, audited). */
1870
+ forceClaim?: boolean;
1831
1871
  }
1832
1872
  /**
1833
- * Result of `tasks.claim` — agent claim confirmation.
1873
+ * Result of `tasks.claim` — the lease now held.
1834
1874
  *
1835
1875
  * @task T1703
1876
+ * @task T12502
1836
1877
  */
1837
1878
  export interface TasksClaimResult {
1838
1879
  /** The task ID that was claimed. */
1839
1880
  taskId: string;
1840
- /** The agent ID that now holds the claim. */
1841
- agentId: string;
1881
+ /** The agent ID recorded with the lease, or `null`. */
1882
+ agentId: string | null;
1883
+ /** The lease held by the caller's session. */
1884
+ claim: TaskClaim;
1842
1885
  }
1843
1886
 
1844
- // tasks.unclaim
1887
+ /**
1888
+ * Params for `tasks.unclaim` — release the caller session's lease (T12502).
1889
+ *
1890
+ * @task T12502
1891
+ */
1845
1892
  export interface TasksUnclaimParams {
1893
+ /** Task to release. */
1846
1894
  taskId: string;
1895
+ /** Release a lease held by ANOTHER session (`--force-claim`, audited). */
1896
+ forceClaim?: boolean;
1847
1897
  }
1848
1898
  /**
1849
1899
  * Result of `tasks.unclaim` — agent release confirmation.
1850
1900
  *
1851
1901
  * @task T1703
1902
+ * @task T12502
1852
1903
  */
1853
1904
  export interface TasksUnclaimResult {
1854
1905
  /** The task ID whose claim was released. */
1855
1906
  taskId: string;
1907
+ /** `true` when a lease was cleared; `false` when the task was unclaimed. */
1908
+ released: boolean;
1856
1909
  }
1857
1910
 
1858
- // tasks.start (dispatch-level)
1911
+ /**
1912
+ * Params for `tasks.start`. T12502: the start takes the caller session's
1913
+ * claim lease; the flags are the explicit, audited overrides.
1914
+ *
1915
+ * @task T12502
1916
+ */
1859
1917
  export interface TasksStartQueryParams {
1918
+ /** Task to start. */
1860
1919
  taskId: string;
1920
+ /** Take over an EXPIRED lease held by another session (`--take-over`). */
1921
+ takeOver?: boolean;
1922
+ /** Take over a LIVE lease held by another session (`--force-claim`). */
1923
+ forceClaim?: boolean;
1861
1924
  }
1862
1925
  /**
1863
1926
  * Result of `tasks.start` — work-start confirmation.
1864
1927
  *
1865
1928
  * @task T1703
1929
+ * @task T12502
1866
1930
  */
1867
1931
  export interface TasksStartQueryResult {
1868
1932
  /** The task ID that is now active. */
1869
1933
  taskId: string;
1870
1934
  /** The task ID that was previously active (auto-stopped), or null. */
1871
1935
  previousTask: string | null;
1936
+ /** The claim lease this start holds, or `null` for an unbound caller (no lease). */
1937
+ claim?: TaskClaim | null;
1872
1938
  }
1873
1939
 
1874
1940
  // tasks.stop (dispatch-level)
@@ -124,8 +124,12 @@ export interface RerootProjectResult {
124
124
  checkpointPath: string;
125
125
  /** Top-level entries renamed from the old root into the new one. */
126
126
  renamed: string[];
127
- /** Whether `.cleo/project-id` was already present or had to be written. */
128
- projectIdFile: 'present' | 'written';
127
+ /**
128
+ * Whether the tracked identity was already present (`.cleo/project.json`),
129
+ * present only as the legacy `.cleo/project-id` (`legacy`, T12716), or had
130
+ * to be written.
131
+ */
132
+ projectIdFile: 'present' | 'written' | 'legacy';
129
133
  /** Absolute path of the tombstone left at the old root. */
130
134
  tombstone: string;
131
135
  /** Registry status after the rebind. */