@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.
- package/dist/branch-lock.d.ts +51 -0
- package/dist/branch-lock.d.ts.map +1 -1
- package/dist/branch-lock.js +25 -3
- package/dist/branch-lock.js.map +1 -1
- package/dist/cloud/api.d.ts +1 -1
- package/dist/data-accessor.d.ts +270 -16
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/decide.d.ts +110 -1
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +23 -0
- package/dist/decide.js.map +1 -1
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +137 -6
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/exit-codes.d.ts +7 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +7 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge-health.d.ts +21 -0
- package/dist/knowledge-health.d.ts.map +1 -1
- package/dist/nexus-account.d.ts +160 -0
- package/dist/nexus-account.d.ts.map +1 -0
- package/dist/nexus-account.js +68 -0
- package/dist/nexus-account.js.map +1 -0
- package/dist/operations/focus.d.ts +7 -3
- package/dist/operations/focus.d.ts.map +1 -1
- package/dist/operations/nexus-scope-map.d.ts +8 -0
- package/dist/operations/nexus-scope-map.d.ts.map +1 -1
- package/dist/operations/nexus-scope-map.js +8 -0
- package/dist/operations/nexus-scope-map.js.map +1 -1
- package/dist/operations/nexus.d.ts +265 -0
- package/dist/operations/nexus.d.ts.map +1 -1
- package/dist/operations/nexus.js +9 -1
- package/dist/operations/nexus.js.map +1 -1
- package/dist/operations/orchestrate.d.ts +6 -1
- package/dist/operations/orchestrate.d.ts.map +1 -1
- package/dist/operations/output-contracts-data.d.ts.map +1 -1
- package/dist/operations/output-contracts-data.js +115 -1
- package/dist/operations/output-contracts-data.js.map +1 -1
- package/dist/operations/tasks.d.ts +73 -9
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js.map +1 -1
- package/dist/project-lifecycle.d.ts +6 -2
- package/dist/project-lifecycle.d.ts.map +1 -1
- package/dist/row-identity.d.ts +128 -0
- package/dist/row-identity.d.ts.map +1 -0
- package/dist/row-identity.js +20 -0
- package/dist/row-identity.js.map +1 -0
- package/dist/session.d.ts +19 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +9 -2
- package/dist/session.js.map +1 -1
- package/dist/task.d.ts +56 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/tools/score-task-priority.d.ts +1 -1
- package/dist/tools/score-task-priority.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
- package/src/branch-lock.ts +80 -3
- package/src/data-accessor.ts +283 -17
- package/src/decide.ts +127 -1
- package/src/dispatch/operations-registry.ts +144 -6
- package/src/doctor.ts +9 -0
- package/src/exit-codes.ts +7 -0
- package/src/index.ts +56 -0
- package/src/knowledge-health.ts +22 -0
- package/src/nexus-account.ts +181 -0
- package/src/operations/focus.ts +7 -3
- package/src/operations/nexus-scope-map.ts +9 -0
- package/src/operations/nexus.ts +281 -0
- package/src/operations/orchestrate.ts +6 -2
- package/src/operations/output-contracts-data.ts +125 -1
- package/src/operations/tasks.ts +78 -12
- package/src/project-lifecycle.ts +6 -2
- package/src/row-identity.ts +132 -0
- package/src/session.ts +20 -2
- package/src/task.ts +58 -2
- package/src/tools/score-task-priority.ts +1 -1
package/src/branch-lock.ts
CHANGED
|
@@ -111,8 +111,50 @@ export interface WorktreeMergeResult {
|
|
|
111
111
|
*
|
|
112
112
|
* Callers should treat `nothingToIntegrate: true` as success-with-no-work,
|
|
113
113
|
* and reserve failure handling for `merged: false` WITHOUT this flag.
|
|
114
|
+
*
|
|
115
|
+
* T12773: also set when {@link WorktreeMergeResult.landedUpstream} is true —
|
|
116
|
+
* the task branch already landed on `origin/<targetBranch>`, so there is no
|
|
117
|
+
* local integration left to do.
|
|
114
118
|
*/
|
|
115
119
|
nothingToIntegrate?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* True when the `task/<taskId>` branch had **already landed upstream** — its
|
|
122
|
+
* tip is an ancestor of `origin/<targetBranch>`, or merging it into
|
|
123
|
+
* `origin/<targetBranch>` would change nothing (squash-merged PR).
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* T12773: `cleo done --pr <n>` on a task whose PR had already merged on
|
|
127
|
+
* origin ran a local `git merge --no-ff task/<id>` into a stale local
|
|
128
|
+
* default branch, forking it from origin. When this flag is set no local
|
|
129
|
+
* merge was attempted and the operator's checkout was NOT moved; when the
|
|
130
|
+
* local default branch is behind, {@link WorktreeMergeResult.syncCommand}
|
|
131
|
+
* names the command to sync it (hint-only).
|
|
132
|
+
*
|
|
133
|
+
* Not set for a task branch with zero commits beyond the local default
|
|
134
|
+
* branch — that is plain `nothingToIntegrate`.
|
|
135
|
+
*/
|
|
136
|
+
landedUpstream?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* True when the local merge was REFUSED because the local default branch is
|
|
139
|
+
* behind `origin/<targetBranch>` (or diverged from it: behind and ahead). A
|
|
140
|
+
* merge there would fork it from origin (T12773). The worktree and task
|
|
141
|
+
* branch are preserved; sync the default branch
|
|
142
|
+
* ({@link WorktreeMergeResult.syncCommand}) and re-run the integration.
|
|
143
|
+
*/
|
|
144
|
+
staleTarget?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Exact shell command that syncs the local default branch with
|
|
147
|
+
* `origin/<targetBranch>` — `git merge --ff-only` when it is only behind,
|
|
148
|
+
* a plain `git merge origin/<targetBranch>` when it is diverged (never a
|
|
149
|
+
* rebase, which would drop the local ADR-062 `--no-ff` task merge commits
|
|
150
|
+
* and rewrite agent SHAs). CLEO never runs it (T12773).
|
|
151
|
+
*/
|
|
152
|
+
syncCommand?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Operator-facing next step when integration was skipped or refused without
|
|
155
|
+
* a merge (T12773) — e.g. how to sync a stale local default branch.
|
|
156
|
+
*/
|
|
157
|
+
hint?: string;
|
|
116
158
|
}
|
|
117
159
|
|
|
118
160
|
/**
|
|
@@ -366,6 +408,19 @@ export const ISOLATION_ENV_KEYS = [
|
|
|
366
408
|
/** Union of all isolation env key strings. */
|
|
367
409
|
export type IsolationEnvKey = (typeof ISOLATION_ENV_KEYS)[number];
|
|
368
410
|
|
|
411
|
+
/**
|
|
412
|
+
* The identity keys re-exported on every shell call of a spawned agent
|
|
413
|
+
* (T12502) — see {@link IsolationResult.perCallLine}. `CLEO_AGENT_ROLE` is
|
|
414
|
+
* among them because the git shim reads it on every `git` call to enforce
|
|
415
|
+
* the worker's restricted-role policy; a shell that lost it would run git
|
|
416
|
+
* unrestricted.
|
|
417
|
+
*/
|
|
418
|
+
const IDENTITY_ENV_KEYS = [
|
|
419
|
+
'CLEO_SESSION_ID',
|
|
420
|
+
'CLEO_AGENT_ID',
|
|
421
|
+
'CLEO_AGENT_ROLE',
|
|
422
|
+
] as const satisfies readonly IsolationEnvKey[];
|
|
423
|
+
|
|
369
424
|
/**
|
|
370
425
|
* Absolute-path validation rules enforced for Edit/Write SDK tool operations.
|
|
371
426
|
*
|
|
@@ -515,6 +570,15 @@ export interface IsolationResult {
|
|
|
515
570
|
* The trailing newline is included so callers can concatenate directly.
|
|
516
571
|
*/
|
|
517
572
|
preamble: string;
|
|
573
|
+
/**
|
|
574
|
+
* The one line an agent must run at the start of EVERY shell call (T12502):
|
|
575
|
+
* `cd <worktree> && export CLEO_SESSION_ID=… CLEO_AGENT_ID=… CLEO_AGENT_ROLE=… || exit 1`
|
|
576
|
+
* (identity keys only when set; every value shell-quoted). Harness shells
|
|
577
|
+
* keep neither cwd nor env between calls, so this line re-enters the
|
|
578
|
+
* worktree AND re-binds the agent's own session and the role the git shim
|
|
579
|
+
* enforces. It is also Step 1 of {@link IsolationResult.preamble}.
|
|
580
|
+
*/
|
|
581
|
+
perCallLine: string;
|
|
518
582
|
/**
|
|
519
583
|
* Boundary contract for downstream enforcement layers (git-shim, tests).
|
|
520
584
|
*
|
|
@@ -596,11 +660,24 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
|
|
|
596
660
|
// T12520 — every path is emitted through `shellQuote`, never interpolated
|
|
597
661
|
// bare or inside double quotes, so the snippet parses for any worktree path.
|
|
598
662
|
const quotedPath = shellQuote(worktreePath);
|
|
663
|
+
// T12502 — env does not persist between an agent harness's Bash calls, so
|
|
664
|
+
// the per-call line re-binds the agent's own identity (and the role the git
|
|
665
|
+
// shim enforces) along with the cwd.
|
|
666
|
+
// Without it a worker's `cleo start` resolves to the ORCHESTRATOR's session
|
|
667
|
+
// and is refused on its own (spawn-claimed) task with E_TASK_CLAIMED.
|
|
668
|
+
const identityExports = IDENTITY_ENV_KEYS.filter((k) => env[k] !== '').map(
|
|
669
|
+
(k) => `${k}=${shellQuote(env[k])}`,
|
|
670
|
+
);
|
|
671
|
+
const perCallLine =
|
|
672
|
+
identityExports.length > 0
|
|
673
|
+
? `cd ${quotedPath} && export ${identityExports.join(' ')} || exit 1`
|
|
674
|
+
: `cd ${quotedPath} || exit 1`;
|
|
599
675
|
const preamble = [
|
|
600
676
|
'## Worktree Isolation (REQUIRED — do not skip)',
|
|
601
677
|
'',
|
|
602
|
-
'# Step 1: Enter the worktree (exits
|
|
603
|
-
|
|
678
|
+
'# Step 1: Enter the worktree and bind your session (exits if the path is missing).',
|
|
679
|
+
'# Repeat this line at the start of EVERY Bash call: cwd and env do not persist.',
|
|
680
|
+
perCallLine,
|
|
604
681
|
'',
|
|
605
682
|
'# Step 2: Verify working directory (guards against shell/symlink quirks)',
|
|
606
683
|
'case "$PWD" in',
|
|
@@ -630,7 +707,7 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
|
|
|
630
707
|
absolutePathRules,
|
|
631
708
|
};
|
|
632
709
|
|
|
633
|
-
return { cwd, env, preamble, boundaryContract };
|
|
710
|
+
return { cwd, env, preamble, perCallLine, boundaryContract };
|
|
634
711
|
}
|
|
635
712
|
|
|
636
713
|
// ---------------------------------------------------------------------------
|
package/src/data-accessor.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -180,6 +193,14 @@ export interface AcRow {
|
|
|
180
193
|
updatedAt: string | null;
|
|
181
194
|
/** Optional sha256(text) snapshot; writers MAY populate, readers MUST treat null as "unknown". */
|
|
182
195
|
contentHash: string | null;
|
|
196
|
+
/**
|
|
197
|
+
* Row uid (T12341): the criterion's identity across devices and across edits
|
|
198
|
+
* (`id` changes with the text; `uid` does not). `null` until the store fills
|
|
199
|
+
* it; absent from accessors that predate it.
|
|
200
|
+
*/
|
|
201
|
+
uid?: string | null;
|
|
202
|
+
/** Birth fingerprint (T12341): write-once, carried with the uid across edits. */
|
|
203
|
+
birthFp?: string | null;
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
/** Machine-readable AC child-projection drift codes for doctor/audit output. */
|
|
@@ -251,6 +272,12 @@ export interface AcBindingRow {
|
|
|
251
272
|
bindingType: 'direct' | 'satisfies' | 'coverage';
|
|
252
273
|
/** ISO-8601 timestamp of binding creation. */
|
|
253
274
|
createdAt: string;
|
|
275
|
+
/**
|
|
276
|
+
* The evidence was recorded against a different text of this criterion
|
|
277
|
+
* (T12341): the criterion kept its identity through an edit, but no gate may
|
|
278
|
+
* count this binding until the evidence is re-verified. Absent means valid.
|
|
279
|
+
*/
|
|
280
|
+
stale?: boolean;
|
|
254
281
|
}
|
|
255
282
|
|
|
256
283
|
/** Query options for bounded reads from the append-only task audit log. @task T10594 */
|
|
@@ -281,16 +308,165 @@ export interface TaskAuditLogRow {
|
|
|
281
308
|
* Optimistic-concurrency guard for a single-task write (T12503).
|
|
282
309
|
*
|
|
283
310
|
* A task's version is its `updatedAt` timestamp (falling back to `createdAt`
|
|
284
|
-
* for a row that was never updated).
|
|
285
|
-
* caller that read version `v` and passes
|
|
286
|
-
* against exactly the row it read or
|
|
287
|
-
* (`ExitCode.VERSION_CONFLICT`) carrying the current
|
|
311
|
+
* for a row that was never updated). The update, complete and field-update
|
|
312
|
+
* paths advance it strictly, so a caller that read version `v` and passes
|
|
313
|
+
* `expectedUpdatedAt: v` either writes against exactly the row it read or
|
|
314
|
+
* fails with `E_CONFLICT` (`ExitCode.VERSION_CONFLICT`) carrying the current
|
|
315
|
+
* version. Some other writers still stamp it from the clock, leaving a narrow
|
|
316
|
+
* same-millisecond window (T12720). The comparison
|
|
288
317
|
* runs inside the write transaction, after `BEGIN IMMEDIATE` holds the lock.
|
|
289
318
|
* Omitting the guard keeps last-writer-wins semantics.
|
|
290
319
|
*/
|
|
291
320
|
export interface TaskWriteGuard {
|
|
292
321
|
/** Version the caller read; the write fails with `E_CONFLICT` if the row moved on. */
|
|
293
322
|
expectedUpdatedAt?: string;
|
|
323
|
+
/**
|
|
324
|
+
* The task as the caller read it. Used only to build the conflict summary:
|
|
325
|
+
* on `E_CONFLICT`, the fields whose stored value differs from this snapshot
|
|
326
|
+
* are reported in {@link TaskConflictDetails.changedFields}. @task T12503
|
|
327
|
+
*/
|
|
328
|
+
baseline?: Task;
|
|
329
|
+
/**
|
|
330
|
+
* Compare-and-set on the claim lease columns (T12502). The write matches
|
|
331
|
+
* only while the stored holder satisfies {@link TaskClaimGuard.mode}; when
|
|
332
|
+
* it does not, the write fails with `E_TASK_CLAIMED`
|
|
333
|
+
* ({@link TaskClaimedDetails}) instead of `E_CONFLICT`.
|
|
334
|
+
*/
|
|
335
|
+
claim?: TaskClaimGuard;
|
|
336
|
+
/**
|
|
337
|
+
* Leave the task version (`updatedAt`) unchanged. For lease bookkeeping
|
|
338
|
+
* (renew, release) that must not invalidate another writer's `--if-match`.
|
|
339
|
+
* @task T12502
|
|
340
|
+
*/
|
|
341
|
+
keepVersion?: boolean;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* How a claim write treats the lease already stored on the task (T12502).
|
|
346
|
+
*
|
|
347
|
+
* - `acquire`: succeeds when the task is unclaimed, already held by the
|
|
348
|
+
* caller's session, or held by `handoffFrom` (an orchestrator handing the
|
|
349
|
+
* task to the agent it spawns). An expired lease still blocks: taking it is
|
|
350
|
+
* explicit.
|
|
351
|
+
* - `take-over`: `acquire`, or the stored lease has expired. Audited.
|
|
352
|
+
* - `force`: unconditional, even over a live lease. Audited.
|
|
353
|
+
* - `renew`: only the holder session may extend its lease.
|
|
354
|
+
* - `release`: only the holder session may clear its lease.
|
|
355
|
+
*/
|
|
356
|
+
export type TaskClaimMode = 'acquire' | 'take-over' | 'force' | 'renew' | 'release';
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Claim predicate for a guarded task write (T12502). Evaluated in the
|
|
360
|
+
* UPDATE's WHERE clause, inside the write transaction.
|
|
361
|
+
*/
|
|
362
|
+
export interface TaskClaimGuard {
|
|
363
|
+
/** The caller's session, or `null` for an unbound caller (holds no lease). */
|
|
364
|
+
sessionId: string | null;
|
|
365
|
+
/** Which stored holders the write may replace. */
|
|
366
|
+
mode: TaskClaimMode;
|
|
367
|
+
/** ISO-8601 UTC "now" used for the lease-expiry comparison. */
|
|
368
|
+
now: string;
|
|
369
|
+
/** A session whose live lease `acquire` may take over (spawn hand-off). */
|
|
370
|
+
handoffFrom?: string | null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A request to take, renew or override the claim lease on a task (T12502).
|
|
375
|
+
*/
|
|
376
|
+
export interface TaskClaimRequest {
|
|
377
|
+
/** The claiming session; `null` checks the claim without taking a lease. */
|
|
378
|
+
sessionId: string | null;
|
|
379
|
+
/** Agent identity recorded with the lease, or `null`. */
|
|
380
|
+
agentId: string | null;
|
|
381
|
+
/** How to treat a stored lease (see {@link TaskClaimMode}); never `release`. */
|
|
382
|
+
mode: Exclude<TaskClaimMode, 'release'>;
|
|
383
|
+
/** Lease length in milliseconds; defaults to the core lease TTL. */
|
|
384
|
+
leaseMs?: number;
|
|
385
|
+
/** Clock override (ISO-8601 UTC) for tests. */
|
|
386
|
+
now?: string;
|
|
387
|
+
/** A session whose live lease `acquire` may take over (spawn hand-off). */
|
|
388
|
+
handoffFrom?: string | null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* `details` of an `E_TASK_CLAIMED` (`ExitCode.TASK_CLAIMED`) error: who holds
|
|
393
|
+
* the task, until when, and which explicit, audited override applies.
|
|
394
|
+
*
|
|
395
|
+
* @task T12502
|
|
396
|
+
*/
|
|
397
|
+
export interface TaskClaimedDetails {
|
|
398
|
+
/** Always `'claimedBySession'`: the lease-holder column the claim compared. */
|
|
399
|
+
field: 'claimedBySession';
|
|
400
|
+
/** The claimed task. */
|
|
401
|
+
taskId: string;
|
|
402
|
+
/** The lease currently stored on the task. */
|
|
403
|
+
holder: TaskClaim;
|
|
404
|
+
/** `true` when the holder's lease has lapsed (take it with `--take-over`). */
|
|
405
|
+
expired: boolean;
|
|
406
|
+
/** The refused caller. */
|
|
407
|
+
requester: { sessionId: string | null; agentId: string | null };
|
|
408
|
+
/** The flag that would override: `--take-over` (expired) or `--force-claim` (live). */
|
|
409
|
+
override: '--take-over' | '--force-claim';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* One field that differs between the caller's read and the stored row, as
|
|
414
|
+
* reported on an `E_CONFLICT` error. Values are JSON-serialised and truncated
|
|
415
|
+
* so the envelope stays small; `null` means the field was absent.
|
|
416
|
+
*
|
|
417
|
+
* @task T12503
|
|
418
|
+
*/
|
|
419
|
+
export interface TaskConflictChange {
|
|
420
|
+
/** Task field name (camelCase, as on {@link Task}). */
|
|
421
|
+
field: string;
|
|
422
|
+
/** JSON of the value in the caller's read, or `null` when absent. */
|
|
423
|
+
was: string | null;
|
|
424
|
+
/** JSON of the value currently stored, or `null` when absent. */
|
|
425
|
+
now: string | null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `details` of an `E_CONFLICT` (`ExitCode.VERSION_CONFLICT`) task error: the
|
|
430
|
+
* version the caller expected, the version now stored, and which fields moved
|
|
431
|
+
* so an agent can re-read, merge and retry with `--if-match <currentVersion>`.
|
|
432
|
+
*
|
|
433
|
+
* `changedFields` is diffed against the caller's read when the write path
|
|
434
|
+
* holds it (`cleo update`, `cleo complete`, or a guard with `baseline`). When
|
|
435
|
+
* the caller's read was already older than the one this command made, the
|
|
436
|
+
* summary covers only changes after that read; `current` always carries the
|
|
437
|
+
* stored values to merge against.
|
|
438
|
+
*
|
|
439
|
+
* @task T12503
|
|
440
|
+
*/
|
|
441
|
+
export interface TaskConflictDetails {
|
|
442
|
+
/** Always `'updatedAt'`: the version field. */
|
|
443
|
+
field: 'updatedAt';
|
|
444
|
+
/** The version the caller expected. */
|
|
445
|
+
expected: string;
|
|
446
|
+
/** The version currently stored (same as {@link currentVersion}). */
|
|
447
|
+
actual: string;
|
|
448
|
+
/** The version to pass as `--if-match` after merging. */
|
|
449
|
+
currentVersion: string;
|
|
450
|
+
/** Names of the fields that differ between the caller's read and the stored row. */
|
|
451
|
+
changedFields: string[];
|
|
452
|
+
/** Per-field before/after summary for {@link changedFields}. */
|
|
453
|
+
changes: TaskConflictChange[];
|
|
454
|
+
/**
|
|
455
|
+
* Merge-relevant stored values of the task right now, bounded so the
|
|
456
|
+
* envelope stays small: `title` is cut at 200 characters and `labels` /
|
|
457
|
+
* `depends` hold at most the first 50 entries, with the full counts in
|
|
458
|
+
* `labelsTotal` / `dependsTotal`.
|
|
459
|
+
*/
|
|
460
|
+
current: {
|
|
461
|
+
title: string;
|
|
462
|
+
status: TaskStatus;
|
|
463
|
+
priority: TaskPriority;
|
|
464
|
+
labels: string[];
|
|
465
|
+
labelsTotal: number;
|
|
466
|
+
depends: string[];
|
|
467
|
+
dependsTotal: number;
|
|
468
|
+
parentId: string | null;
|
|
469
|
+
} | null;
|
|
294
470
|
}
|
|
295
471
|
|
|
296
472
|
/**
|
|
@@ -299,6 +475,11 @@ export interface TaskWriteGuard {
|
|
|
299
475
|
*/
|
|
300
476
|
export interface TransactionAccessor {
|
|
301
477
|
upsertSingleTask(task: Task): Promise<void>;
|
|
478
|
+
/**
|
|
479
|
+
* Insert a NEW task; never overwrites. Throws `ID_COLLISION` when the id is
|
|
480
|
+
* already stored (T12724).
|
|
481
|
+
*/
|
|
482
|
+
insertNewTask(task: Task): Promise<void>;
|
|
302
483
|
archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
|
|
303
484
|
removeSingleTask(taskId: string): Promise<void>;
|
|
304
485
|
setMetaValue(key: string, value: unknown): Promise<void>;
|
|
@@ -334,6 +515,10 @@ export interface TransactionAccessor {
|
|
|
334
515
|
targetTaskId?: string | null;
|
|
335
516
|
projection?: string;
|
|
336
517
|
contentHash?: string | null;
|
|
518
|
+
/** Row uid to keep (T12341); omitted → a new uid is minted. */
|
|
519
|
+
uid?: string | null;
|
|
520
|
+
/** Birth fingerprint to keep with the uid (T12341). */
|
|
521
|
+
birthFp?: string | null;
|
|
337
522
|
}>,
|
|
338
523
|
): Promise<void>;
|
|
339
524
|
/**
|
|
@@ -349,13 +534,48 @@ export interface TransactionAccessor {
|
|
|
349
534
|
* @task T10508
|
|
350
535
|
*/
|
|
351
536
|
deleteAcRowsForTask(taskId: string): Promise<void>;
|
|
537
|
+
/**
|
|
538
|
+
* Delete only the named AC rows of `taskId` (rows of other tasks are never
|
|
539
|
+
* touched). Used by the diff apply path so ACs that survive an edit keep
|
|
540
|
+
* their row — and therefore their evidence bindings.
|
|
541
|
+
*
|
|
542
|
+
* `keepBindingsForUids`: uids the same write carries onto new rows (an
|
|
543
|
+
* edited criterion keeps its uid, T12341); their bindings are kept, as
|
|
544
|
+
* stale evidence, instead of pruned.
|
|
545
|
+
* @task T12789
|
|
546
|
+
*/
|
|
547
|
+
deleteAcRowsByIds(
|
|
548
|
+
taskId: string,
|
|
549
|
+
ids: readonly string[],
|
|
550
|
+
keepBindingsForUids?: readonly string[],
|
|
551
|
+
): Promise<void>;
|
|
552
|
+
/**
|
|
553
|
+
* Update existing AC rows of their own task in place, keyed by `(id, taskId)`.
|
|
554
|
+
* Every mutable column is rewritten from the supplied row; `id` and
|
|
555
|
+
* `taskId` are never changed. A row that does not exist for that task is
|
|
556
|
+
* an error, never an insert.
|
|
557
|
+
* @task T12789
|
|
558
|
+
*/
|
|
559
|
+
updateAcRows(
|
|
560
|
+
rows: Array<{
|
|
561
|
+
id: string;
|
|
562
|
+
taskId: string;
|
|
563
|
+
ordinal: number;
|
|
564
|
+
text: string;
|
|
565
|
+
kind?: 'text' | 'child_task' | 'evidence_bound';
|
|
566
|
+
sourceKey?: string;
|
|
567
|
+
targetTaskId?: string | null;
|
|
568
|
+
projection?: string;
|
|
569
|
+
contentHash?: string | null;
|
|
570
|
+
}>,
|
|
571
|
+
): Promise<void>;
|
|
352
572
|
/**
|
|
353
573
|
* Append a history row to `task_acceptance_criteria_history` capturing the
|
|
354
574
|
* AC text that is about to be superseded.
|
|
355
575
|
* @task T10508
|
|
356
576
|
*/
|
|
357
577
|
appendAcHistory(
|
|
358
|
-
rows: Array<{ acId: string; previousText: string; reason: string }>,
|
|
578
|
+
rows: Array<{ acId: string; previousText: string; reason: string; acUid?: string | null }>,
|
|
359
579
|
): Promise<void>;
|
|
360
580
|
/**
|
|
361
581
|
* Read all `evidence_ac_bindings` rows whose `ac_id` ∈ the given set.
|
|
@@ -387,6 +607,16 @@ export interface TransactionAccessor {
|
|
|
387
607
|
bindingType: 'direct' | 'satisfies' | 'coverage';
|
|
388
608
|
}>,
|
|
389
609
|
): Promise<void>;
|
|
610
|
+
/**
|
|
611
|
+
* Delete every `evidence_ac_bindings` row whose `ac_id` names no AC row
|
|
612
|
+
* (bindings left behind before AC removal pruned them), recording each
|
|
613
|
+
* removed row in the task audit log (`ac.bindings.pruned`) first. Backs
|
|
614
|
+
* `cleo doctor ac-bindings --fix`.
|
|
615
|
+
*
|
|
616
|
+
* @returns the removed bindings
|
|
617
|
+
* @task T12790
|
|
618
|
+
*/
|
|
619
|
+
pruneOrphanAcBindings(): Promise<AcBindingRow[]>;
|
|
390
620
|
}
|
|
391
621
|
|
|
392
622
|
// Re-export AcRow at the module level for both transaction + outer accessor use.
|
|
@@ -436,6 +666,13 @@ export interface DataAccessor {
|
|
|
436
666
|
/** Upsert a single task (targeted write, no full-file reload). */
|
|
437
667
|
upsertSingleTask(task: Task): Promise<void>;
|
|
438
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Insert a NEW task under a freshly allocated or computed id. Never
|
|
671
|
+
* overwrites: throws `ID_COLLISION` when the id is already stored, and
|
|
672
|
+
* writes nothing (T12724).
|
|
673
|
+
*/
|
|
674
|
+
insertNewTask(task: Task): Promise<void>;
|
|
675
|
+
|
|
439
676
|
/** Archive a single task by ID (sets status='archived' + archive metadata). */
|
|
440
677
|
archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
|
|
441
678
|
|
|
@@ -471,6 +708,14 @@ export interface DataAccessor {
|
|
|
471
708
|
*/
|
|
472
709
|
getAcBindings(acIds: readonly string[]): Promise<AcBindingRow[]>;
|
|
473
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Read every `evidence_ac_bindings` row whose `ac_id` names no row of
|
|
713
|
+
* `task_acceptance_criteria` — a binding for an AC that no longer exists.
|
|
714
|
+
* Read-only; backs `cleo doctor ac-bindings`.
|
|
715
|
+
* @task T12790
|
|
716
|
+
*/
|
|
717
|
+
findOrphanAcBindings(): Promise<AcBindingRow[]>;
|
|
718
|
+
|
|
474
719
|
// ---- Metadata (schema_meta KV store) ----
|
|
475
720
|
|
|
476
721
|
/** Read a typed value from the metadata store. Returns null if not found. */
|
|
@@ -579,29 +824,50 @@ export interface DataAccessor {
|
|
|
579
824
|
/** Get a single agent instance by ID. Returns null if not found. */
|
|
580
825
|
getAgentInstance(agentId: string): Promise<DataAccessorAgentInstance | null>;
|
|
581
826
|
|
|
582
|
-
// ---- Agent task claiming ----
|
|
827
|
+
// ---- Agent task claiming (leased, T12502) ----
|
|
583
828
|
|
|
584
829
|
/**
|
|
585
|
-
*
|
|
830
|
+
* Take, renew or override the claim lease on a task in one compare-and-set
|
|
831
|
+
* write (`updateTaskFields` with a {@link TaskClaimGuard}). The human
|
|
832
|
+
* `assignee` is never touched.
|
|
586
833
|
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
834
|
+
* With `sessionId: null` no lease is written; the call only verifies, in
|
|
835
|
+
* the same way, that no other session holds the task.
|
|
589
836
|
*
|
|
590
837
|
* @param taskId - ID of the task to claim.
|
|
591
|
-
* @param
|
|
592
|
-
* @
|
|
838
|
+
* @param request - Claimant, mode and lease length.
|
|
839
|
+
* @returns The lease now held, or `null` when `sessionId` is `null`.
|
|
840
|
+
* @throws {Error} When the task is not found.
|
|
841
|
+
* @throws CleoError `E_TASK_CLAIMED` with {@link TaskClaimedDetails} when
|
|
842
|
+
* another session holds the task and `mode` does not allow replacing it.
|
|
593
843
|
*/
|
|
594
|
-
claimTask(taskId: string,
|
|
844
|
+
claimTask(taskId: string, request: TaskClaimRequest): Promise<TaskClaim | null>;
|
|
595
845
|
|
|
596
846
|
/**
|
|
597
|
-
* Release a
|
|
598
|
-
*
|
|
599
|
-
* No-op if the task is not currently claimed.
|
|
847
|
+
* Release the claim lease on a task. Without `force`, only the holder
|
|
848
|
+
* session's lease is cleared; with `force` any lease is cleared.
|
|
600
849
|
*
|
|
601
|
-
* @param taskId - ID of the task to
|
|
850
|
+
* @param taskId - ID of the task to release.
|
|
851
|
+
* @param release - The releasing session and whether to force.
|
|
852
|
+
* @returns `true` when a lease was cleared, `false` when there was none to clear.
|
|
602
853
|
* @throws {Error} When the task is not found.
|
|
854
|
+
* @throws CleoError `E_TASK_CLAIMED` when another session holds the lease and `force` is not set.
|
|
855
|
+
*/
|
|
856
|
+
unclaimTask(
|
|
857
|
+
taskId: string,
|
|
858
|
+
release: { sessionId: string | null; force?: boolean },
|
|
859
|
+
): Promise<boolean>;
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Heartbeat: extend every lease held by `sessionId` to `leaseExpiresAt`,
|
|
863
|
+
* without changing any task version (T12502). A session holding no lease
|
|
864
|
+
* costs one indexed read and takes no write lock.
|
|
865
|
+
*
|
|
866
|
+
* @param sessionId - The holder session.
|
|
867
|
+
* @param leaseExpiresAt - New lease expiry (ISO-8601 UTC).
|
|
868
|
+
* @returns Number of leases renewed.
|
|
603
869
|
*/
|
|
604
|
-
|
|
870
|
+
renewSessionClaims(sessionId: string, leaseExpiresAt: string): Promise<number>;
|
|
605
871
|
}
|
|
606
872
|
|
|
607
873
|
// 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
|
-
/**
|
|
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). */
|