@cleocode/contracts 2026.9.23 → 2026.9.24

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 (80) hide show
  1. package/dist/branch-lock.d.ts +9 -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 +205 -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 +9 -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/session.d.ts +19 -1
  53. package/dist/session.d.ts.map +1 -1
  54. package/dist/session.js +9 -2
  55. package/dist/session.js.map +1 -1
  56. package/dist/task.d.ts +35 -1
  57. package/dist/task.d.ts.map +1 -1
  58. package/dist/tools/score-task-priority.d.ts +1 -1
  59. package/dist/tools/score-task-priority.d.ts.map +1 -1
  60. package/package.json +2 -2
  61. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  62. package/src/branch-lock.ts +38 -3
  63. package/src/data-accessor.ts +211 -16
  64. package/src/decide.ts +127 -1
  65. package/src/dispatch/operations-registry.ts +144 -6
  66. package/src/doctor.ts +9 -0
  67. package/src/exit-codes.ts +7 -0
  68. package/src/index.ts +48 -0
  69. package/src/knowledge-health.ts +22 -0
  70. package/src/nexus-account.ts +181 -0
  71. package/src/operations/focus.ts +7 -3
  72. package/src/operations/nexus-scope-map.ts +9 -0
  73. package/src/operations/nexus.ts +281 -0
  74. package/src/operations/orchestrate.ts +6 -2
  75. package/src/operations/output-contracts-data.ts +125 -1
  76. package/src/operations/tasks.ts +78 -12
  77. package/src/project-lifecycle.ts +6 -2
  78. package/src/session.ts +20 -2
  79. package/src/task.ts +37 -1
  80. package/src/tools/score-task-priority.ts +1 -1
@@ -366,6 +366,19 @@ export const ISOLATION_ENV_KEYS = [
366
366
  /** Union of all isolation env key strings. */
367
367
  export type IsolationEnvKey = (typeof ISOLATION_ENV_KEYS)[number];
368
368
 
369
+ /**
370
+ * The identity keys re-exported on every shell call of a spawned agent
371
+ * (T12502) — see {@link IsolationResult.perCallLine}. `CLEO_AGENT_ROLE` is
372
+ * among them because the git shim reads it on every `git` call to enforce
373
+ * the worker's restricted-role policy; a shell that lost it would run git
374
+ * unrestricted.
375
+ */
376
+ const IDENTITY_ENV_KEYS = [
377
+ 'CLEO_SESSION_ID',
378
+ 'CLEO_AGENT_ID',
379
+ 'CLEO_AGENT_ROLE',
380
+ ] as const satisfies readonly IsolationEnvKey[];
381
+
369
382
  /**
370
383
  * Absolute-path validation rules enforced for Edit/Write SDK tool operations.
371
384
  *
@@ -515,6 +528,15 @@ export interface IsolationResult {
515
528
  * The trailing newline is included so callers can concatenate directly.
516
529
  */
517
530
  preamble: string;
531
+ /**
532
+ * The one line an agent must run at the start of EVERY shell call (T12502):
533
+ * `cd <worktree> && export CLEO_SESSION_ID=… CLEO_AGENT_ID=… CLEO_AGENT_ROLE=… || exit 1`
534
+ * (identity keys only when set; every value shell-quoted). Harness shells
535
+ * keep neither cwd nor env between calls, so this line re-enters the
536
+ * worktree AND re-binds the agent's own session and the role the git shim
537
+ * enforces. It is also Step 1 of {@link IsolationResult.preamble}.
538
+ */
539
+ perCallLine: string;
518
540
  /**
519
541
  * Boundary contract for downstream enforcement layers (git-shim, tests).
520
542
  *
@@ -596,11 +618,24 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
596
618
  // T12520 — every path is emitted through `shellQuote`, never interpolated
597
619
  // bare or inside double quotes, so the snippet parses for any worktree path.
598
620
  const quotedPath = shellQuote(worktreePath);
621
+ // T12502 — env does not persist between an agent harness's Bash calls, so
622
+ // the per-call line re-binds the agent's own identity (and the role the git
623
+ // shim enforces) along with the cwd.
624
+ // Without it a worker's `cleo start` resolves to the ORCHESTRATOR's session
625
+ // and is refused on its own (spawn-claimed) task with E_TASK_CLAIMED.
626
+ const identityExports = IDENTITY_ENV_KEYS.filter((k) => env[k] !== '').map(
627
+ (k) => `${k}=${shellQuote(env[k])}`,
628
+ );
629
+ const perCallLine =
630
+ identityExports.length > 0
631
+ ? `cd ${quotedPath} && export ${identityExports.join(' ')} || exit 1`
632
+ : `cd ${quotedPath} || exit 1`;
599
633
  const preamble = [
600
634
  '## Worktree Isolation (REQUIRED — do not skip)',
601
635
  '',
602
- '# Step 1: Enter the worktree (exits immediately if path is missing)',
603
- `cd ${quotedPath} || exit 1`,
636
+ '# Step 1: Enter the worktree and bind your session (exits if the path is missing).',
637
+ '# Repeat this line at the start of EVERY Bash call: cwd and env do not persist.',
638
+ perCallLine,
604
639
  '',
605
640
  '# Step 2: Verify working directory (guards against shell/symlink quirks)',
606
641
  'case "$PWD" in',
@@ -630,7 +665,7 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
630
665
  absolutePathRules,
631
666
  };
632
667
 
633
- return { cwd, env, preamble, boundaryContract };
668
+ return { cwd, env, preamble, perCallLine, boundaryContract };
634
669
  }
635
670
 
636
671
  // ---------------------------------------------------------------------------
@@ -17,6 +17,7 @@ import type { ArchivedTask } from './archive.js';
17
17
  import type { Session } from './session.js';
18
18
  import type {
19
19
  Task,
20
+ TaskClaim,
20
21
  TaskKind,
21
22
  TaskPriority,
22
23
  TaskSeverity,
@@ -150,6 +151,18 @@ export interface TaskFieldUpdates {
150
151
  updatedAt?: string | null;
151
152
  assignee?: string | null;
152
153
  pipelineStage?: string | null;
154
+ /**
155
+ * Agent claim lease columns (T12502). Change them through
156
+ * `DataAccessor.claimTask` / `unclaimTask`, which pair them with a
157
+ * {@link TaskClaimGuard} so the write is a compare-and-set on the holder.
158
+ */
159
+ claimedBySession?: string | null;
160
+ /** Agent identity of the lease holder (T12502). */
161
+ claimedByAgent?: string | null;
162
+ /** ISO-8601 UTC instant the lease was taken (T12502). */
163
+ claimedAt?: string | null;
164
+ /** ISO-8601 UTC instant the lease lapses unless renewed (T12502). */
165
+ leaseExpiresAt?: string | null;
153
166
  }
154
167
 
155
168
  /**
@@ -281,16 +294,165 @@ export interface TaskAuditLogRow {
281
294
  * Optimistic-concurrency guard for a single-task write (T12503).
282
295
  *
283
296
  * A task's version is its `updatedAt` timestamp (falling back to `createdAt`
284
- * for a row that was never updated). Writers keep it strictly increasing, so a
285
- * caller that read version `v` and passes `expectedUpdatedAt: v` either writes
286
- * against exactly the row it read or fails with `E_CONFLICT`
287
- * (`ExitCode.VERSION_CONFLICT`) carrying the current version. The comparison
297
+ * for a row that was never updated). The update, complete and field-update
298
+ * paths advance it strictly, so a caller that read version `v` and passes
299
+ * `expectedUpdatedAt: v` either writes against exactly the row it read or
300
+ * fails with `E_CONFLICT` (`ExitCode.VERSION_CONFLICT`) carrying the current
301
+ * version. Some other writers still stamp it from the clock, leaving a narrow
302
+ * same-millisecond window (T12720). The comparison
288
303
  * runs inside the write transaction, after `BEGIN IMMEDIATE` holds the lock.
289
304
  * Omitting the guard keeps last-writer-wins semantics.
290
305
  */
291
306
  export interface TaskWriteGuard {
292
307
  /** Version the caller read; the write fails with `E_CONFLICT` if the row moved on. */
293
308
  expectedUpdatedAt?: string;
309
+ /**
310
+ * The task as the caller read it. Used only to build the conflict summary:
311
+ * on `E_CONFLICT`, the fields whose stored value differs from this snapshot
312
+ * are reported in {@link TaskConflictDetails.changedFields}. @task T12503
313
+ */
314
+ baseline?: Task;
315
+ /**
316
+ * Compare-and-set on the claim lease columns (T12502). The write matches
317
+ * only while the stored holder satisfies {@link TaskClaimGuard.mode}; when
318
+ * it does not, the write fails with `E_TASK_CLAIMED`
319
+ * ({@link TaskClaimedDetails}) instead of `E_CONFLICT`.
320
+ */
321
+ claim?: TaskClaimGuard;
322
+ /**
323
+ * Leave the task version (`updatedAt`) unchanged. For lease bookkeeping
324
+ * (renew, release) that must not invalidate another writer's `--if-match`.
325
+ * @task T12502
326
+ */
327
+ keepVersion?: boolean;
328
+ }
329
+
330
+ /**
331
+ * How a claim write treats the lease already stored on the task (T12502).
332
+ *
333
+ * - `acquire`: succeeds when the task is unclaimed, already held by the
334
+ * caller's session, or held by `handoffFrom` (an orchestrator handing the
335
+ * task to the agent it spawns). An expired lease still blocks: taking it is
336
+ * explicit.
337
+ * - `take-over`: `acquire`, or the stored lease has expired. Audited.
338
+ * - `force`: unconditional, even over a live lease. Audited.
339
+ * - `renew`: only the holder session may extend its lease.
340
+ * - `release`: only the holder session may clear its lease.
341
+ */
342
+ export type TaskClaimMode = 'acquire' | 'take-over' | 'force' | 'renew' | 'release';
343
+
344
+ /**
345
+ * Claim predicate for a guarded task write (T12502). Evaluated in the
346
+ * UPDATE's WHERE clause, inside the write transaction.
347
+ */
348
+ export interface TaskClaimGuard {
349
+ /** The caller's session, or `null` for an unbound caller (holds no lease). */
350
+ sessionId: string | null;
351
+ /** Which stored holders the write may replace. */
352
+ mode: TaskClaimMode;
353
+ /** ISO-8601 UTC "now" used for the lease-expiry comparison. */
354
+ now: string;
355
+ /** A session whose live lease `acquire` may take over (spawn hand-off). */
356
+ handoffFrom?: string | null;
357
+ }
358
+
359
+ /**
360
+ * A request to take, renew or override the claim lease on a task (T12502).
361
+ */
362
+ export interface TaskClaimRequest {
363
+ /** The claiming session; `null` checks the claim without taking a lease. */
364
+ sessionId: string | null;
365
+ /** Agent identity recorded with the lease, or `null`. */
366
+ agentId: string | null;
367
+ /** How to treat a stored lease (see {@link TaskClaimMode}); never `release`. */
368
+ mode: Exclude<TaskClaimMode, 'release'>;
369
+ /** Lease length in milliseconds; defaults to the core lease TTL. */
370
+ leaseMs?: number;
371
+ /** Clock override (ISO-8601 UTC) for tests. */
372
+ now?: string;
373
+ /** A session whose live lease `acquire` may take over (spawn hand-off). */
374
+ handoffFrom?: string | null;
375
+ }
376
+
377
+ /**
378
+ * `details` of an `E_TASK_CLAIMED` (`ExitCode.TASK_CLAIMED`) error: who holds
379
+ * the task, until when, and which explicit, audited override applies.
380
+ *
381
+ * @task T12502
382
+ */
383
+ export interface TaskClaimedDetails {
384
+ /** Always `'claimedBySession'`: the lease-holder column the claim compared. */
385
+ field: 'claimedBySession';
386
+ /** The claimed task. */
387
+ taskId: string;
388
+ /** The lease currently stored on the task. */
389
+ holder: TaskClaim;
390
+ /** `true` when the holder's lease has lapsed (take it with `--take-over`). */
391
+ expired: boolean;
392
+ /** The refused caller. */
393
+ requester: { sessionId: string | null; agentId: string | null };
394
+ /** The flag that would override: `--take-over` (expired) or `--force-claim` (live). */
395
+ override: '--take-over' | '--force-claim';
396
+ }
397
+
398
+ /**
399
+ * One field that differs between the caller's read and the stored row, as
400
+ * reported on an `E_CONFLICT` error. Values are JSON-serialised and truncated
401
+ * so the envelope stays small; `null` means the field was absent.
402
+ *
403
+ * @task T12503
404
+ */
405
+ export interface TaskConflictChange {
406
+ /** Task field name (camelCase, as on {@link Task}). */
407
+ field: string;
408
+ /** JSON of the value in the caller's read, or `null` when absent. */
409
+ was: string | null;
410
+ /** JSON of the value currently stored, or `null` when absent. */
411
+ now: string | null;
412
+ }
413
+
414
+ /**
415
+ * `details` of an `E_CONFLICT` (`ExitCode.VERSION_CONFLICT`) task error: the
416
+ * version the caller expected, the version now stored, and which fields moved
417
+ * so an agent can re-read, merge and retry with `--if-match <currentVersion>`.
418
+ *
419
+ * `changedFields` is diffed against the caller's read when the write path
420
+ * holds it (`cleo update`, `cleo complete`, or a guard with `baseline`). When
421
+ * the caller's read was already older than the one this command made, the
422
+ * summary covers only changes after that read; `current` always carries the
423
+ * stored values to merge against.
424
+ *
425
+ * @task T12503
426
+ */
427
+ export interface TaskConflictDetails {
428
+ /** Always `'updatedAt'`: the version field. */
429
+ field: 'updatedAt';
430
+ /** The version the caller expected. */
431
+ expected: string;
432
+ /** The version currently stored (same as {@link currentVersion}). */
433
+ actual: string;
434
+ /** The version to pass as `--if-match` after merging. */
435
+ currentVersion: string;
436
+ /** Names of the fields that differ between the caller's read and the stored row. */
437
+ changedFields: string[];
438
+ /** Per-field before/after summary for {@link changedFields}. */
439
+ changes: TaskConflictChange[];
440
+ /**
441
+ * Merge-relevant stored values of the task right now, bounded so the
442
+ * envelope stays small: `title` is cut at 200 characters and `labels` /
443
+ * `depends` hold at most the first 50 entries, with the full counts in
444
+ * `labelsTotal` / `dependsTotal`.
445
+ */
446
+ current: {
447
+ title: string;
448
+ status: TaskStatus;
449
+ priority: TaskPriority;
450
+ labels: string[];
451
+ labelsTotal: number;
452
+ depends: string[];
453
+ dependsTotal: number;
454
+ parentId: string | null;
455
+ } | null;
294
456
  }
295
457
 
296
458
  /**
@@ -299,6 +461,11 @@ export interface TaskWriteGuard {
299
461
  */
300
462
  export interface TransactionAccessor {
301
463
  upsertSingleTask(task: Task): Promise<void>;
464
+ /**
465
+ * Insert a NEW task; never overwrites. Throws `ID_COLLISION` when the id is
466
+ * already stored (T12724).
467
+ */
468
+ insertNewTask(task: Task): Promise<void>;
302
469
  archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
303
470
  removeSingleTask(taskId: string): Promise<void>;
304
471
  setMetaValue(key: string, value: unknown): Promise<void>;
@@ -436,6 +603,13 @@ export interface DataAccessor {
436
603
  /** Upsert a single task (targeted write, no full-file reload). */
437
604
  upsertSingleTask(task: Task): Promise<void>;
438
605
 
606
+ /**
607
+ * Insert a NEW task under a freshly allocated or computed id. Never
608
+ * overwrites: throws `ID_COLLISION` when the id is already stored, and
609
+ * writes nothing (T12724).
610
+ */
611
+ insertNewTask(task: Task): Promise<void>;
612
+
439
613
  /** Archive a single task by ID (sets status='archived' + archive metadata). */
440
614
  archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
441
615
 
@@ -579,29 +753,50 @@ export interface DataAccessor {
579
753
  /** Get a single agent instance by ID. Returns null if not found. */
580
754
  getAgentInstance(agentId: string): Promise<DataAccessorAgentInstance | null>;
581
755
 
582
- // ---- Agent task claiming ----
756
+ // ---- Agent task claiming (leased, T12502) ----
583
757
 
584
758
  /**
585
- * Atomically claim a task for an agent.
759
+ * Take, renew or override the claim lease on a task in one compare-and-set
760
+ * write (`updateTaskFields` with a {@link TaskClaimGuard}). The human
761
+ * `assignee` is never touched.
586
762
  *
587
- * Uses `UPDATE ... WHERE assignee IS NULL OR assignee = agentId` to prevent
588
- * race conditions. Throws if the task is already claimed by a different agent.
763
+ * With `sessionId: null` no lease is written; the call only verifies, in
764
+ * the same way, that no other session holds the task.
589
765
  *
590
766
  * @param taskId - ID of the task to claim.
591
- * @param agentId - Agent identifier claiming the task.
592
- * @throws {Error} When the task is not found or is already claimed by another agent.
767
+ * @param request - Claimant, mode and lease length.
768
+ * @returns The lease now held, or `null` when `sessionId` is `null`.
769
+ * @throws {Error} When the task is not found.
770
+ * @throws CleoError `E_TASK_CLAIMED` with {@link TaskClaimedDetails} when
771
+ * another session holds the task and `mode` does not allow replacing it.
593
772
  */
594
- claimTask(taskId: string, agentId: string): Promise<void>;
773
+ claimTask(taskId: string, request: TaskClaimRequest): Promise<TaskClaim | null>;
595
774
 
596
775
  /**
597
- * Release a claimed task, clearing its assignee.
776
+ * Release the claim lease on a task. Without `force`, only the holder
777
+ * session's lease is cleared; with `force` any lease is cleared.
598
778
  *
599
- * No-op if the task is not currently claimed.
600
- *
601
- * @param taskId - ID of the task to unclaim.
779
+ * @param taskId - ID of the task to release.
780
+ * @param release - The releasing session and whether to force.
781
+ * @returns `true` when a lease was cleared, `false` when there was none to clear.
602
782
  * @throws {Error} When the task is not found.
783
+ * @throws CleoError `E_TASK_CLAIMED` when another session holds the lease and `force` is not set.
784
+ */
785
+ unclaimTask(
786
+ taskId: string,
787
+ release: { sessionId: string | null; force?: boolean },
788
+ ): Promise<boolean>;
789
+
790
+ /**
791
+ * Heartbeat: extend every lease held by `sessionId` to `leaseExpiresAt`,
792
+ * without changing any task version (T12502). A session holding no lease
793
+ * costs one indexed read and takes no write lock.
794
+ *
795
+ * @param sessionId - The holder session.
796
+ * @param leaseExpiresAt - New lease expiry (ISO-8601 UTC).
797
+ * @returns Number of leases renewed.
603
798
  */
604
- unclaimTask(taskId: string): Promise<void>;
799
+ renewSessionClaims(sessionId: string, leaseExpiresAt: string): Promise<number>;
605
800
  }
606
801
 
607
802
  // Factory functions (createDataAccessor, getTaskAccessor) live in @cleocode/core,
package/src/decide.ts CHANGED
@@ -209,7 +209,11 @@ export interface DecisionOutcome {
209
209
  readonly costUsd?: number;
210
210
  /** Provider-reported cost in integer micro-dollars, when known (T12664). */
211
211
  readonly costMicros?: number;
212
- /** Account balance in integer micro-dollars reported with this answer, when known. */
212
+ /**
213
+ * Account balance in integer micro-dollars reported with this answer, when
214
+ * known. The Jev wire adapter never sets it (the layahost OpenAPI reports
215
+ * balance only via `GET /v1/usage`, T12715); other providers may.
216
+ */
213
217
  readonly balanceMicros?: number;
214
218
  /** Provider model checkpoint that answered, when reported. */
215
219
  readonly checkpoint?: string;
@@ -234,9 +238,131 @@ export type DecisionProviderKind = (typeof DECISION_PROVIDER_KINDS)[number];
234
238
  /** Base URL of the layahost API (its OpenAPI `servers[0].url`). */
235
239
  export const LAYAHOST_BASE_URL = 'https://layahost.com';
236
240
 
241
+ /**
242
+ * Base URL of Jev's own API (TypeSafe AI). Source: the canonical spec
243
+ * `system-one-integration`, "Provider research: capability map" ("Jev itself
244
+ * (TypeSafe AI, `https://api.typesafe.ai`, OpenAPI v0.2.0)").
245
+ */
246
+ export const JEV_DEFAULT_BASE_URL = 'https://api.typesafe.ai';
247
+
237
248
  /** layahost's routing model, the default for a layahost connection. */
238
249
  export const LAYAHOST_DEFAULT_MODEL = 'laya-auto';
239
250
 
251
+ // ─── Named provider profiles (T12733) ────────────────────────────────────────
252
+
253
+ /**
254
+ * A valid profile name: 1-32 characters of lowercase letters, digits and
255
+ * `-`, starting and ending with a letter or digit (e.g. `default`, `work`,
256
+ * `team-b`). A profile is addressed as `<provider>/<name>`, e.g.
257
+ * `layahost/default` or `jev/team`; one provider may hold several profiles
258
+ * (accounts), each with its own key.
259
+ */
260
+ export const DECIDE_PROFILE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
261
+
262
+ /** The profile name used when none is given: `<provider>/default`. */
263
+ export const DECIDE_DEFAULT_PROFILE_NAME = 'default';
264
+
265
+ /**
266
+ * The stored `baseUrl` of a profile that uses its provider's preset URL. It
267
+ * is resolved when the profile is used, so a changed preset URL flows
268
+ * through to every profile that kept the default.
269
+ */
270
+ export const DECIDE_PROFILE_DEFAULT_URL = 'default';
271
+
272
+ /** How a profile's base URL is set: the provider's preset URL, or an override. */
273
+ export type DecideProfileUrlSource = 'default' | 'override';
274
+
275
+ /**
276
+ * Reachability of a decision provider, as `GET /v1/models` reports it.
277
+ *
278
+ * - `reachable` — the listing answered 2xx.
279
+ * - `unauthorized` — the provider rejected the key.
280
+ * - `key_limit_reached` — the key's monthly decision limit is reached.
281
+ * - `unconfigured` — no valid base URL + key stored.
282
+ * - `unreachable` — network failure, timeout, or any other HTTP status.
283
+ */
284
+ export type DecideProviderState =
285
+ | 'reachable'
286
+ | 'unauthorized'
287
+ | 'key_limit_reached'
288
+ | 'unconfigured'
289
+ | 'unreachable';
290
+
291
+ /** Result of probing one profile (`cleo decide profiles --probe`). Secret-free. */
292
+ export interface DecideProfileProbe {
293
+ /** Reachability verdict. */
294
+ readonly state: DecideProviderState;
295
+ /** HTTP status of the probe, when one was received. */
296
+ readonly httpStatus?: number;
297
+ /** Probe latency, ms. */
298
+ readonly latencyMs?: number;
299
+ /** Secret-free explanation, when there is one. */
300
+ readonly detail?: string;
301
+ }
302
+
303
+ /** One named System One provider profile, as listed. Carries no key, only its masked preview. */
304
+ export interface DecideProfileSummary {
305
+ /** Profile id, `<provider>/<name>`, e.g. `layahost/work`. */
306
+ readonly id: string;
307
+ /** Profile name within its provider, e.g. `work`. */
308
+ readonly name: string;
309
+ /** Whether everyday decisions use this profile. Exactly one profile is active when any is. */
310
+ readonly active: boolean;
311
+ /** Whether the profile holds an acceptable base URL and a non-blank key. */
312
+ readonly configured: boolean;
313
+ /** Provider kind. */
314
+ readonly provider: DecisionProviderKind;
315
+ /** Resolved base URL (userinfo removed). */
316
+ readonly baseUrl: string;
317
+ /** Whether the URL is the provider's preset (`default`) or an override. */
318
+ readonly urlSource: DecideProfileUrlSource;
319
+ /** Default model, when stored. */
320
+ readonly model?: string;
321
+ /** Masked key preview (`…abcd`). */
322
+ readonly keyPreview: string;
323
+ /** ISO timestamp of the profile's last write, when known. */
324
+ readonly updatedAt?: string;
325
+ /** Reachability, when probed. */
326
+ readonly probe?: DecideProfileProbe;
327
+ }
328
+
329
+ /**
330
+ * A profile resolved for use (`resolveDecideProfile`): its id, provider, base
331
+ * URL (a stored `default` already resolved to the preset), plaintext API key
332
+ * and default model. Contains the key: pass it to the wire and never log or
333
+ * emit it. `profile` repeats `name` so the value can be used directly as a
334
+ * decision connection whose provider state is cached per profile.
335
+ */
336
+ export interface DecideProfileConnection {
337
+ /** Profile id, `<provider>/<name>`. */
338
+ readonly name: string;
339
+ /** Same as {@link DecideProfileConnection.name}. */
340
+ readonly profile: string;
341
+ /** Provider kind. */
342
+ readonly provider: DecisionProviderKind;
343
+ /** Resolved base URL. */
344
+ readonly baseUrl: string;
345
+ /** Plaintext API key. SECRET. */
346
+ readonly apiKey: string;
347
+ /** Default model, when stored. */
348
+ readonly model?: string;
349
+ }
350
+
351
+ /** Result of listing the System One profiles. Secret-free. */
352
+ export interface DecideProfileListResult {
353
+ /** Absolute path of the credential store. */
354
+ readonly path: string;
355
+ /** Id (`<provider>/<name>`) of the active profile, or `null` when none is configured. */
356
+ readonly active: string | null;
357
+ /** Every stored profile, sorted by id. */
358
+ readonly profiles: readonly DecideProfileSummary[];
359
+ /**
360
+ * True when the top-level settings disagreed with the active profile's
361
+ * entry (an older CLEO rewrote the file) and were taken as authoritative.
362
+ */
363
+ readonly reconciled: boolean;
364
+ }
365
+
240
366
  /** Connection settings for a decision provider. */
241
367
  export interface DecisionProviderConfig {
242
368
  /** Absolute base URL of the provider's API (no trailing path to the endpoint). */