@cleocode/contracts 2026.5.101 → 2026.5.103

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,58 @@ 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
+ },
2503
+ {
2504
+ // T10121 — idempotent cron-safe saga auto-close repair. Re-applies
2505
+ // T10116 logic for sagas whose members reached 100% terminal via paths
2506
+ // OTHER than completeTask (bulk SQL, crash recovery, manual edits).
2507
+ // Supersedes T10098 standalone scope.
2508
+ gateway: 'mutate',
2509
+ domain: 'tasks',
2510
+ operation: 'saga.reconcile',
2511
+ description:
2512
+ 'tasks.saga.reconcile (mutate) — idempotent cron-safe saga auto-close repair; per-saga advisory lock + audit log; supersedes T10098',
2513
+ tier: 0,
2514
+ idempotent: true,
2515
+ sessionRequired: false,
2516
+ requiredParams: [],
2517
+ params: [
2518
+ {
2519
+ name: 'sagaId',
2520
+ type: 'string',
2521
+ required: false,
2522
+ description: 'Optional saga task ID. Omit to walk every saga.',
2523
+ cli: { positional: true },
2524
+ },
2525
+ {
2526
+ name: 'dryRun',
2527
+ type: 'boolean',
2528
+ required: false,
2529
+ description: 'Report what would happen without mutating rows or writing audit log',
2530
+ },
2531
+ ] satisfies ParamDef[],
2532
+ },
2448
2533
  {
2449
2534
  gateway: 'mutate',
2450
2535
  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,124 @@ 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
+
1179
+ /**
1180
+ * Per-saga reconciliation action returned by `tasks.saga.reconcile`.
1181
+ *
1182
+ * @task T10121
1183
+ */
1184
+ export type TasksSagaReconcileAction = 'close' | 'no-op' | 'blocked' | 'error';
1185
+
1186
+ /**
1187
+ * Params for `tasks.saga.reconcile` — idempotent cron-safe saga auto-close
1188
+ * repair. Re-applies the T10116 auto-close logic for sagas whose members
1189
+ * reached 100% terminal status via paths OTHER than `completeTask` (bulk
1190
+ * SQL repair, crash recovery, manual state edits).
1191
+ *
1192
+ * @task T10121
1193
+ * @see ADR-073-above-epic-naming.md §1.3
1194
+ */
1195
+ export interface TasksSagaReconcileParams {
1196
+ /**
1197
+ * Optional single saga to reconcile. When omitted, the verb walks every
1198
+ * saga returned by `taskList({ type: 'epic', label: 'saga' })`.
1199
+ */
1200
+ sagaId?: string;
1201
+ /**
1202
+ * When `true`, run in report-only mode — log what would happen without
1203
+ * mutating any rows or writing to the audit log.
1204
+ */
1205
+ dryRun?: boolean;
1206
+ }
1207
+
1208
+ /**
1209
+ * Per-saga reconciliation outcome surfaced inside
1210
+ * {@link TasksSagaReconcileResult}.
1211
+ *
1212
+ * @task T10121
1213
+ */
1214
+ export interface TasksSagaReconcileEntry {
1215
+ /** Saga task ID this entry refers to. */
1216
+ sagaId: string;
1217
+ /** The action the reconciler took for this saga. */
1218
+ action: TasksSagaReconcileAction;
1219
+ /** Member task IDs considered by the closure check. */
1220
+ members: string[];
1221
+ /** Members that satisfied the terminal-status predicate. */
1222
+ terminalMembers: string[];
1223
+ /** Members that did NOT satisfy the terminal-status predicate. */
1224
+ pendingMembers: string[];
1225
+ /** Saga status BEFORE this run. */
1226
+ statusBefore: string;
1227
+ /** Saga status AFTER this run (== `statusBefore` for no-op/blocked/error). */
1228
+ statusAfter: string;
1229
+ /** Free-form human-readable reason recorded for the audit entry. */
1230
+ reason: string;
1231
+ /** ISO 8601 timestamp the decision was recorded. */
1232
+ timestamp: string;
1233
+ }
1234
+
1235
+ /**
1236
+ * Result of `tasks.saga.reconcile` — aggregate counters + per-saga entries
1237
+ * in stable id order.
1238
+ *
1239
+ * @task T10121
1240
+ */
1241
+ export interface TasksSagaReconcileResult {
1242
+ /** Total number of sagas inspected (== `entries.length`). */
1243
+ total: number;
1244
+ /** Number of sagas the run flipped to `status='done'`. */
1245
+ closed: number;
1246
+ /** Number of sagas already in the correct terminal state. */
1247
+ noOp: number;
1248
+ /** Number of sagas blocked behind a concurrent lock holder. */
1249
+ blocked: number;
1250
+ /** Number of sagas with pending non-terminal members (not closed). */
1251
+ pending: number;
1252
+ /** Number of sagas that errored out during reconciliation. */
1253
+ errors: number;
1254
+ /** Whether this run ran in dry-run mode. */
1255
+ dryRun: boolean;
1256
+ /** Detailed per-saga entries in stable id order. */
1257
+ entries: TasksSagaReconcileEntry[];
1258
+ }
1259
+
1086
1260
  /** Params for `tasks.saga.rollup` — aggregate member Epic statuses. */
1087
1261
  export interface TasksSagaRollupParams {
1088
1262
  /** Saga task ID. */
@@ -1169,7 +1343,12 @@ export type TasksOps = {
1169
1343
  // Saga sub-domain ops (ADR-073)
1170
1344
  readonly 'saga.create': readonly [TasksSagaCreateParams, TasksSagaCreateResult];
1171
1345
  readonly 'saga.add': readonly [TasksSagaAddParams, TasksSagaAddResult];
1346
+ readonly 'saga.detach': readonly [TasksSagaDetachParams, TasksSagaDetachResult];
1172
1347
  readonly 'saga.list': readonly [TasksSagaListParams, TasksSagaListResult];
1173
1348
  readonly 'saga.members': readonly [TasksSagaMembersParams, TasksSagaMembersResult];
1174
1349
  readonly 'saga.rollup': readonly [TasksSagaRollupParams, TasksSagaRollupResult];
1350
+ /** T10117 — repair an I5-violating saga. */
1351
+ readonly 'saga.repair': readonly [TasksSagaRepairParams, TasksSagaRepairResult];
1352
+ /** T10121 — idempotent cron-safe auto-close repair (supersedes T10098 scope). */
1353
+ readonly 'saga.reconcile': readonly [TasksSagaReconcileParams, TasksSagaReconcileResult];
1175
1354
  };
@@ -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). */