@cleocode/contracts 2026.5.100 → 2026.5.102

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.
@@ -2395,6 +2395,39 @@ export const OPERATIONS: OperationDef[] = [
2395
2395
  },
2396
2396
  ] satisfies ParamDef[],
2397
2397
  },
2398
+ {
2399
+ gateway: 'mutate',
2400
+ domain: 'tasks',
2401
+ operation: 'saga.detach',
2402
+ description:
2403
+ 'tasks.saga.detach (mutate) — remove a Saga member relation (task_relations type=groups); idempotent; appends to .cleo/audit/saga-detach.jsonl (T10118)',
2404
+ tier: 0,
2405
+ idempotent: true,
2406
+ sessionRequired: false,
2407
+ requiredParams: ['sagaId', 'memberId'],
2408
+ params: [
2409
+ {
2410
+ name: 'sagaId',
2411
+ type: 'string',
2412
+ required: true,
2413
+ description: 'Saga task ID',
2414
+ cli: { positional: true },
2415
+ },
2416
+ {
2417
+ name: 'memberId',
2418
+ type: 'string',
2419
+ required: true,
2420
+ description: 'Member task ID to detach from the Saga',
2421
+ cli: { positional: true },
2422
+ },
2423
+ {
2424
+ name: 'reason',
2425
+ type: 'string',
2426
+ required: false,
2427
+ description: 'Human-readable reason recorded in the audit log entry',
2428
+ },
2429
+ ] satisfies ParamDef[],
2430
+ },
2398
2431
  {
2399
2432
  gateway: 'query',
2400
2433
  domain: 'tasks',
@@ -2445,6 +2478,28 @@ export const OPERATIONS: OperationDef[] = [
2445
2478
  },
2446
2479
  ] satisfies ParamDef[],
2447
2480
  },
2481
+ {
2482
+ // T10117 — detach I5-violating parentId and re-attach via task_relations
2483
+ // type=groups (ADR-073 §1.2 invariant I5). Idempotent.
2484
+ gateway: 'mutate',
2485
+ domain: 'tasks',
2486
+ operation: 'saga.repair',
2487
+ description:
2488
+ 'tasks.saga.repair (mutate) — detach I5-violating parentId and re-attach via task_relations type=groups; idempotent',
2489
+ tier: 0,
2490
+ idempotent: true,
2491
+ sessionRequired: false,
2492
+ requiredParams: ['sagaId'],
2493
+ params: [
2494
+ {
2495
+ name: 'sagaId',
2496
+ type: 'string',
2497
+ required: true,
2498
+ description: 'Saga task ID (must have label=saga)',
2499
+ cli: { positional: true },
2500
+ },
2501
+ ] satisfies ParamDef[],
2502
+ },
2448
2503
  {
2449
2504
  gateway: 'mutate',
2450
2505
  domain: 'tasks',
package/src/doctor.ts CHANGED
@@ -202,6 +202,90 @@ export interface OrphanScanResult {
202
202
  softWarnMessage?: string;
203
203
  }
204
204
 
205
+ // ============================================================================
206
+ // Saga Hierarchy Audit (T10119 — ADR-073 §1.2 invariant + drift detection)
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Stable invariant identifiers surfaced by the saga hierarchy audit
211
+ * (ADR-073 §1.2). `I5` and `I7` mirror the runtime guards in
212
+ * `packages/core/src/sagas/enforcement.ts`. `depth` covers the I5/I7 depth
213
+ * ladder (saga → member-Epic → Task → Subtask = 3 hops max). `auto-close-drift`
214
+ * is a soft-drift detector (no invariant strictly broken — the saga is
215
+ * structurally valid, but every member is done while the saga still says
216
+ * pending; T10116 fixes the root cause).
217
+ */
218
+ export type SagaAuditViolationKind = 'I5' | 'I7' | 'depth' | 'auto-close-drift';
219
+
220
+ /**
221
+ * One violation surfaced by {@link auditSagaHierarchy}.
222
+ *
223
+ * Each violation is actionable: the `repairCommand` field names the
224
+ * canonical `cleo` invocation an operator should run to resolve it.
225
+ */
226
+ export interface SagaAuditViolation {
227
+ /** Stable violation identifier (`I5`, `I7`, `depth`, `auto-close-drift`). */
228
+ kind: SagaAuditViolationKind;
229
+ /** Saga task ID where the violation was detected. */
230
+ sagaId: string;
231
+ /**
232
+ * The offending member task ID. Equal to `sagaId` for I5 violations
233
+ * (the saga row itself is the offender). For I7 violations, the
234
+ * nested-saga candidate. For `auto-close-drift`, equal to `sagaId`.
235
+ */
236
+ offendingId: string;
237
+ /** Human-readable message naming both IDs and the repair command. */
238
+ message: string;
239
+ /** Canonical `cleo` command an operator should run to resolve. */
240
+ repairCommand: string;
241
+ }
242
+
243
+ /**
244
+ * Per-saga audit summary returned by {@link auditSagaHierarchy}.
245
+ *
246
+ * Carries enough context for the doctor CLI to render a one-line summary
247
+ * (`Saga T#### · status=… · 4/5 members done · 0 violations`) without a
248
+ * second database round-trip.
249
+ */
250
+ export interface SagaAuditEntry {
251
+ /** Saga task ID. */
252
+ sagaId: string;
253
+ /** Saga title (for human-readable rendering). */
254
+ title: string;
255
+ /** Saga status (`pending`, `active`, `done`, …). */
256
+ status: string;
257
+ /** Total member-Epic count. */
258
+ memberCount: number;
259
+ /** Member-Epics with `status='done'`. */
260
+ doneCount: number;
261
+ /**
262
+ * Violations attributable to this saga. Always returned (possibly
263
+ * empty) so callers can render a stable rows-then-violations layout.
264
+ */
265
+ violations: SagaAuditViolation[];
266
+ }
267
+
268
+ /**
269
+ * Aggregated result returned by {@link auditSagaHierarchy}.
270
+ *
271
+ * `count` is the total number of `I5`/`I7`/`depth` invariant violations
272
+ * across all sagas — the doctor CLI uses this to decide whether to set
273
+ * a non-zero exit code (`auto-close-drift` is a soft warning and does
274
+ * NOT alone trigger exit≠0; tests pass with drift but no I-invariant
275
+ * failure).
276
+ *
277
+ * `driftCount` is the number of `auto-close-drift` warnings (kept
278
+ * separate so CI gates can opt-in to treating drift as failure).
279
+ */
280
+ export interface SagaAuditResult {
281
+ /** All sagas audited, sorted by `sagaId` ascending. */
282
+ sagas: SagaAuditEntry[];
283
+ /** Total I5/I7/depth invariant violations across all sagas. */
284
+ count: number;
285
+ /** Total auto-close-drift warnings across all sagas. */
286
+ driftCount: number;
287
+ }
288
+
205
289
  /**
206
290
  * One line appended to `.cleo/audit/worktree-prune.jsonl` per prune
207
291
  * operation. Extends the existing audit-log schema (timestamp +
package/src/errors.ts CHANGED
@@ -233,6 +233,67 @@ export class LeadBypassDetectedError extends Error {
233
233
  }
234
234
  }
235
235
 
236
+ /**
237
+ * LAFS error code emitted when a saga-labeled epic carries forbidden parent or
238
+ * `depends` edges — ADR-073 §1.2 invariant **I3** violation.
239
+ *
240
+ * Sagas MUST link to member Epics exclusively via
241
+ * `task_relations.type='groups'`; a parent or depends edge re-introduces the
242
+ * depth-budget consumption the saga tier was created to avoid.
243
+ *
244
+ * Runtime guard: `assertSagaInvariantI3` (in `@cleocode/core` saga module).
245
+ *
246
+ * @task T10115
247
+ * @saga T10113
248
+ * @see ADR-073-above-epic-naming.md §1.2
249
+ */
250
+ export const E_SAGA_INVARIANT_VIOLATION_I3 = 'E_SAGA_INVARIANT_VIOLATION_I3' as const;
251
+
252
+ /**
253
+ * LAFS error code emitted when a saga-labeled row has a non-null `parentId` —
254
+ * ADR-073 §1.2 invariant **I5** violation.
255
+ *
256
+ * Sagas are top-level groupings; they do not nest under any task. A non-null
257
+ * `parentId` on a saga-labeled row indicates storage corruption or a faulty
258
+ * import path.
259
+ *
260
+ * Runtime guard: `assertSagaInvariantI5` (in `@cleocode/core` saga module).
261
+ *
262
+ * @task T10115
263
+ * @saga T10113
264
+ * @see ADR-073-above-epic-naming.md §1.2
265
+ */
266
+ export const E_SAGA_INVARIANT_VIOLATION_I5 = 'E_SAGA_INVARIANT_VIOLATION_I5' as const;
267
+
268
+ /**
269
+ * LAFS error code emitted when a saga-member candidate is itself a saga —
270
+ * ADR-073 §1.2 invariant **I7** violation (no nested sagas).
271
+ *
272
+ * Nested sagas would re-introduce the multi-release grouping depth the tier
273
+ * was created to flatten.
274
+ *
275
+ * Runtime guard: `assertSagaInvariantI7` (in `@cleocode/core` saga module).
276
+ *
277
+ * @task T10115
278
+ * @saga T10113
279
+ * @see ADR-073-above-epic-naming.md §1.2
280
+ */
281
+ export const E_SAGA_INVARIANT_VIOLATION_I7 = 'E_SAGA_INVARIANT_VIOLATION_I7' as const;
282
+
283
+ /**
284
+ * Union of saga-invariant LAFS error codes (I3 / I5 / I7).
285
+ *
286
+ * Useful for narrowing thrown error codes from
287
+ * `assertSagaInvariantI{3,5,7}` in dispatch handlers.
288
+ *
289
+ * @task T10115
290
+ * @saga T10113
291
+ */
292
+ export type SagaInvariantErrorCode =
293
+ | typeof E_SAGA_INVARIANT_VIOLATION_I3
294
+ | typeof E_SAGA_INVARIANT_VIOLATION_I5
295
+ | typeof E_SAGA_INVARIANT_VIOLATION_I7;
296
+
236
297
  /**
237
298
  * Normalize any thrown value into a standardized error object.
238
299
  *
package/src/index.ts CHANGED
@@ -128,6 +128,16 @@ export type {
128
128
  BackupMetadata,
129
129
  BackupScope,
130
130
  } from './backup-manifest.js';
131
+ // === Boundary Registry (SSoT for Rust/TS layering — ADR-078, Saga T10176) ===
132
+ export type {
133
+ BoundaryEntry,
134
+ CanonicalHome,
135
+ PerfBudget,
136
+ SafetyBudget,
137
+ ThroughputThreshold,
138
+ WorkloadIntent,
139
+ } from './boundary.js';
140
+ export { BOUNDARY_REGISTRY } from './boundary.js';
131
141
  // === Brain/Memory Types ===
132
142
  export type {
133
143
  BrainCognitiveType,
@@ -302,12 +312,17 @@ export {
302
312
  DocKindRegistry,
303
313
  } from './docs-taxonomy.js';
304
314
  // === Doctor: Worktree-Orphan Audit + Prune Types (T9790, T9808, T9962) ===
315
+ // === Doctor: Saga Hierarchy Audit (T10119 — ADR-073 §1.2) ===
305
316
  export type {
306
317
  ComprehensiveAuditResult,
307
318
  OrphanEntry,
308
319
  OrphanScanResult,
309
320
  PruneAuditEntry,
310
321
  PruneResult,
322
+ SagaAuditEntry,
323
+ SagaAuditResult,
324
+ SagaAuditViolation,
325
+ SagaAuditViolationKind,
311
326
  WorktreeAnomaly,
312
327
  WorktreeAnomalyKind,
313
328
  } from './doctor.js';
@@ -334,12 +349,16 @@ export {
334
349
  TASK_SEVERITIES,
335
350
  TASK_SIZES,
336
351
  } from './enums.js';
352
+ export type { SagaInvariantErrorCode } from './errors.js';
337
353
  // === Error Utilities ===
338
354
  export {
339
355
  ClassifierUnregisteredAgentError,
340
356
  createErrorResult,
341
357
  createSuccessResult,
342
358
  DecisionValidatorFailedError,
359
+ E_SAGA_INVARIANT_VIOLATION_I3,
360
+ E_SAGA_INVARIANT_VIOLATION_I5,
361
+ E_SAGA_INVARIANT_VIOLATION_I7,
343
362
  formatError,
344
363
  getErrorMessage,
345
364
  isErrorResult,
@@ -1156,6 +1175,8 @@ export type {
1156
1175
  TasksSagaAddResult,
1157
1176
  TasksSagaCreateParams,
1158
1177
  TasksSagaCreateResult,
1178
+ TasksSagaDetachParams,
1179
+ TasksSagaDetachResult,
1159
1180
  TasksSagaListParams,
1160
1181
  TasksSagaListResult,
1161
1182
  TasksSagaMembersParams,
@@ -1048,19 +1048,75 @@ export interface TasksSagaAddResult {
1048
1048
  added: boolean;
1049
1049
  }
1050
1050
 
1051
+ /**
1052
+ * Params for `tasks.saga.detach` — remove a Saga member relation
1053
+ * (`task_relations.type='groups'`). Idempotent.
1054
+ *
1055
+ * @task T10118
1056
+ * @see ADR-073-above-epic-naming.md §1.2 invariant I7
1057
+ */
1058
+ export interface TasksSagaDetachParams {
1059
+ /** Saga task ID (the `from` side of the groups relation). */
1060
+ sagaId: string;
1061
+ /** Member task ID (the `to` side of the groups relation). */
1062
+ memberId: string;
1063
+ /** Optional human-readable reason recorded in the audit log entry. */
1064
+ reason?: string;
1065
+ }
1066
+
1067
+ /**
1068
+ * Result of `tasks.saga.detach` — relation removal confirmation + audit
1069
+ * record (always appended even on idempotent no-op).
1070
+ *
1071
+ * @task T10118
1072
+ */
1073
+ export interface TasksSagaDetachResult {
1074
+ /** Saga task ID. */
1075
+ sagaId: string;
1076
+ /** Member task ID that was detached. */
1077
+ memberId: string;
1078
+ /** True when a row was actually removed; false on idempotent no-op. */
1079
+ removed: boolean;
1080
+ /** Reason recorded in the audit log entry. */
1081
+ reason: string;
1082
+ /** ISO 8601 timestamp recorded in the audit log entry. */
1083
+ timestamp: string;
1084
+ }
1085
+
1051
1086
  /** Params for `tasks.saga.list` — list all Sagas. */
1052
1087
  export type TasksSagaListParams = Record<string, never>;
1053
1088
 
1089
+ /**
1090
+ * A single I5-violation warning entry returned by `tasks.saga.list` when a
1091
+ * saga row carries a non-null `parentId` (ADR-073 §1.2 invariant I5).
1092
+ *
1093
+ * @task T10117
1094
+ */
1095
+ export interface TasksSagaInvariantI5Warning {
1096
+ /** Fixed warning code. */
1097
+ code: 'E_SAGA_INVARIANT_VIOLATION_I5';
1098
+ /** Saga task ID whose `parentId` violates invariant I5. */
1099
+ sagaId: string;
1100
+ /** The non-null `parentId` value found on the saga row. */
1101
+ offendingParentId: string;
1102
+ }
1103
+
1054
1104
  /**
1055
1105
  * Result of `tasks.saga.list` — all labeled top-level Epics.
1056
1106
  *
1057
1107
  * @task T9521
1108
+ * @task T10117 — added optional `warnings` array for I5 violations
1058
1109
  */
1059
1110
  export interface TasksSagaListResult {
1060
1111
  /** Array of Saga task records. */
1061
1112
  sagas: TaskRecord[];
1062
1113
  /** Total count. */
1063
1114
  total: number;
1115
+ /**
1116
+ * One entry per saga with a non-null `parentId`. Omitted entirely when no
1117
+ * violations were observed (pre-T10117 envelope shape preserved).
1118
+ */
1119
+ warnings?: TasksSagaInvariantI5Warning[];
1064
1120
  }
1065
1121
 
1066
1122
  /** Params for `tasks.saga.members` — list member Epics linked to a Saga. */
@@ -1083,6 +1139,43 @@ export interface TasksSagaMembersResult {
1083
1139
  total: number;
1084
1140
  }
1085
1141
 
1142
+ /**
1143
+ * Params for `tasks.saga.repair` — detach an I5-violating `parentId` from a
1144
+ * Saga and re-attach via `task_relations.type='groups'`.
1145
+ *
1146
+ * @task T10117
1147
+ * @see ADR-073-above-epic-naming.md §1.2 — invariant I5
1148
+ */
1149
+ export interface TasksSagaRepairParams {
1150
+ /** Saga task ID (must have `label='saga'`). */
1151
+ sagaId: string;
1152
+ }
1153
+
1154
+ /**
1155
+ * Result of `tasks.saga.repair` — idempotent detach + re-attach.
1156
+ *
1157
+ * @task T10117
1158
+ */
1159
+ export interface TasksSagaRepairResult {
1160
+ /** Saga task ID that was inspected. */
1161
+ sagaId: string;
1162
+ /** `true` when the call performed a state change; `false` for no-op. */
1163
+ repaired: boolean;
1164
+ /** The detached `parentId` value, or `null` when no detach was needed. */
1165
+ detachedParentId: string | null;
1166
+ /**
1167
+ * The `groups` edge that was written, or `null` for the idempotent no-op
1168
+ * and the missing-former-parent edge case.
1169
+ */
1170
+ attachedRelation: {
1171
+ from: string;
1172
+ to: string;
1173
+ type: 'groups';
1174
+ } | null;
1175
+ /** Free-form notes the CLI renderer can surface alongside the result. */
1176
+ note?: string;
1177
+ }
1178
+
1086
1179
  /** Params for `tasks.saga.rollup` — aggregate member Epic statuses. */
1087
1180
  export interface TasksSagaRollupParams {
1088
1181
  /** Saga task ID. */
@@ -1169,7 +1262,10 @@ export type TasksOps = {
1169
1262
  // Saga sub-domain ops (ADR-073)
1170
1263
  readonly 'saga.create': readonly [TasksSagaCreateParams, TasksSagaCreateResult];
1171
1264
  readonly 'saga.add': readonly [TasksSagaAddParams, TasksSagaAddResult];
1265
+ readonly 'saga.detach': readonly [TasksSagaDetachParams, TasksSagaDetachResult];
1172
1266
  readonly 'saga.list': readonly [TasksSagaListParams, TasksSagaListResult];
1173
1267
  readonly 'saga.members': readonly [TasksSagaMembersParams, TasksSagaMembersResult];
1174
1268
  readonly 'saga.rollup': readonly [TasksSagaRollupParams, TasksSagaRollupResult];
1269
+ /** T10117 — repair an I5-violating saga. */
1270
+ readonly 'saga.repair': readonly [TasksSagaRepairParams, TasksSagaRepairResult];
1175
1271
  };
@@ -449,8 +449,38 @@ export interface WorktreeInfo {
449
449
  owningAgent: string | null;
450
450
  /** ISO-8601 timestamp of last activity (newest commit OR mtime of working tree). */
451
451
  lastActivity: string;
452
+ /**
453
+ * ISO-8601 timestamp recording when this worktree was first created.
454
+ *
455
+ * Resolution order:
456
+ * 1. For git-native entries — mtime of the per-worktree admin file
457
+ * `<gitCommonDir>/worktrees/<basename(path)>/HEAD`. Git writes this once
458
+ * at `git worktree add` time and never rewrites it during normal usage,
459
+ * making it a faithful proxy for creation time.
460
+ * 2. For sentinel-only entries — the `adoptedAt` timestamp persisted in
461
+ * `.cleo/worktrees.json`.
462
+ * 3. Fallback — mtime of the worktree directory itself.
463
+ *
464
+ * Distinct from {@link lastActivity}, which moves with each new commit on
465
+ * the branch. The two values diverge for any worktree that has had at least
466
+ * one commit since creation.
467
+ *
468
+ * @task T9546
469
+ */
470
+ createdAt: string;
452
471
  /** Whether `git worktree list --porcelain` reports the worktree as locked. */
453
472
  isLocked: boolean;
473
+ /**
474
+ * Lock-state string mirroring {@link isLocked} as a discriminated literal.
475
+ *
476
+ * Provided for downstream consumers that want a single human-readable token
477
+ * rather than a boolean — `'locked'` when porcelain reports the worktree as
478
+ * locked, `'unlocked'` otherwise. Always equivalent to
479
+ * `isLocked ? 'locked' : 'unlocked'`.
480
+ *
481
+ * @task T9546
482
+ */
483
+ lockState: 'locked' | 'unlocked';
454
484
  /** Whether the worktree is stale: no activity > N days AND (task done/cancelled OR branch merged). */
455
485
  isStale: boolean;
456
486
  /** Whether the branch is reachable from `main` (already integrated). */