@cleocode/contracts 2026.9.22 → 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.
- package/dist/__tests__/evidence-atom-schema.test.js +3 -2
- package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
- package/dist/acceptance-gate-schema.d.ts.map +1 -1
- package/dist/acceptance-gate-schema.js +4 -1
- package/dist/acceptance-gate-schema.js.map +1 -1
- package/dist/branch-lock.d.ts +9 -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 +209 -16
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/decide-sites.d.ts +157 -0
- package/dist/decide-sites.d.ts.map +1 -0
- package/dist/decide-sites.js +28 -0
- package/dist/decide-sites.js.map +1 -0
- package/dist/decide.d.ts +214 -0
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -0
- package/dist/decide.js.map +1 -1
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +174 -8
- 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/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +3 -0
- package/dist/evidence-atom-schema.js.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 +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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/admin.d.ts +5 -0
- package/dist/operations/admin.d.ts.map +1 -1
- 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 +97 -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/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 +49 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/score-task-priority.d.ts +28 -3
- 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/__tests__/evidence-atom-schema.test.ts +3 -2
- package/src/acceptance-gate-schema.ts +4 -1
- package/src/branch-lock.ts +38 -3
- package/src/data-accessor.ts +215 -16
- package/src/decide-sites.ts +179 -0
- package/src/decide.ts +235 -0
- package/src/dispatch/operations-registry.ts +183 -8
- package/src/doctor.ts +9 -0
- package/src/evidence-atom-schema.ts +3 -0
- package/src/exit-codes.ts +7 -0
- package/src/index.ts +72 -0
- package/src/knowledge-health.ts +22 -0
- package/src/nexus-account.ts +181 -0
- package/src/operations/admin.ts +5 -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 +104 -12
- package/src/project-lifecycle.ts +6 -2
- package/src/session.ts +20 -2
- package/src/task.ts +51 -1
- package/src/tools/index.ts +1 -0
- package/src/tools/score-task-priority.ts +29 -3
|
@@ -684,7 +684,8 @@ describe('formatGateRequirement (T10337)', () => {
|
|
|
684
684
|
expect(s).toContain('[decision AND note]');
|
|
685
685
|
expect(s).toContain('[pr AND files]');
|
|
686
686
|
// Joiner between combinations is ' OR '.
|
|
687
|
-
|
|
687
|
+
// T12689: + [pr AND note] for a deletion-only PR.
|
|
688
|
+
expect(s.split(' OR ').length).toBe(6);
|
|
688
689
|
});
|
|
689
690
|
|
|
690
691
|
it('formats single-element combination without AND', () => {
|
|
@@ -759,7 +760,7 @@ describe('validateEvidenceForGate failure surface (T9949)', () => {
|
|
|
759
760
|
if (!r.ok) {
|
|
760
761
|
// Error includes the same artifact requirement as the validator.
|
|
761
762
|
expect(r.message).toBe(
|
|
762
|
-
"Gate 'implemented' requires evidence: [commit AND files] OR [commit AND note] OR [decision AND files] OR [decision AND note] OR [pr AND files]",
|
|
763
|
+
"Gate 'implemented' requires evidence: [commit AND files] OR [commit AND note] OR [decision AND files] OR [decision AND note] OR [pr AND files] OR [pr AND note]",
|
|
763
764
|
);
|
|
764
765
|
// New rich hint includes the same content as formatGateRequirementHint.
|
|
765
766
|
expect(r.hint).toBe(formatGateRequirementHint('implemented'));
|
|
@@ -536,7 +536,10 @@ export const acceptanceGateResultSchema = z
|
|
|
536
536
|
message: 'Result cannot precede input capture',
|
|
537
537
|
});
|
|
538
538
|
const executable = ['test', 'command', 'lint'].includes(result.kind);
|
|
539
|
-
|
|
539
|
+
// T12718: a command the runner refused to split (shell syntax, open
|
|
540
|
+
// quote) never had an invocation. It is bound as an unmet `error` so the
|
|
541
|
+
// rest of its batch still records; any verdict needs the invocation.
|
|
542
|
+
if (executable && !binding.invocation && result.result !== 'error')
|
|
540
543
|
context.addIssue({
|
|
541
544
|
code: 'custom',
|
|
542
545
|
path: ['binding', 'invocation'],
|
package/src/branch-lock.ts
CHANGED
|
@@ -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
|
|
603
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
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
|
/**
|
|
@@ -255,6 +268,8 @@ export interface AcBindingRow {
|
|
|
255
268
|
|
|
256
269
|
/** Query options for bounded reads from the append-only task audit log. @task T10594 */
|
|
257
270
|
export interface TaskAuditLogQuery {
|
|
271
|
+
/** Exact audit row ids (T12693: revert one ranking change). */
|
|
272
|
+
ids?: readonly string[];
|
|
258
273
|
taskIds?: readonly string[];
|
|
259
274
|
actions?: readonly string[];
|
|
260
275
|
since?: string;
|
|
@@ -271,22 +286,173 @@ export interface TaskAuditLogRow {
|
|
|
271
286
|
detailsJson: string | null;
|
|
272
287
|
beforeJson: string | null;
|
|
273
288
|
afterJson: string | null;
|
|
289
|
+
/** Session the mutation ran in, when recorded (T12693). */
|
|
290
|
+
sessionId?: string | null;
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
/**
|
|
277
294
|
* Optimistic-concurrency guard for a single-task write (T12503).
|
|
278
295
|
*
|
|
279
296
|
* A task's version is its `updatedAt` timestamp (falling back to `createdAt`
|
|
280
|
-
* for a row that was never updated).
|
|
281
|
-
* caller that read version `v` and passes
|
|
282
|
-
* against exactly the row it read or
|
|
283
|
-
* (`ExitCode.VERSION_CONFLICT`) carrying the current
|
|
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
|
|
284
303
|
* runs inside the write transaction, after `BEGIN IMMEDIATE` holds the lock.
|
|
285
304
|
* Omitting the guard keeps last-writer-wins semantics.
|
|
286
305
|
*/
|
|
287
306
|
export interface TaskWriteGuard {
|
|
288
307
|
/** Version the caller read; the write fails with `E_CONFLICT` if the row moved on. */
|
|
289
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;
|
|
290
456
|
}
|
|
291
457
|
|
|
292
458
|
/**
|
|
@@ -295,6 +461,11 @@ export interface TaskWriteGuard {
|
|
|
295
461
|
*/
|
|
296
462
|
export interface TransactionAccessor {
|
|
297
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>;
|
|
298
469
|
archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
|
|
299
470
|
removeSingleTask(taskId: string): Promise<void>;
|
|
300
471
|
setMetaValue(key: string, value: unknown): Promise<void>;
|
|
@@ -432,6 +603,13 @@ export interface DataAccessor {
|
|
|
432
603
|
/** Upsert a single task (targeted write, no full-file reload). */
|
|
433
604
|
upsertSingleTask(task: Task): Promise<void>;
|
|
434
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
|
+
|
|
435
613
|
/** Archive a single task by ID (sets status='archived' + archive metadata). */
|
|
436
614
|
archiveSingleTask(taskId: string, fields: ArchiveFields): Promise<void>;
|
|
437
615
|
|
|
@@ -575,29 +753,50 @@ export interface DataAccessor {
|
|
|
575
753
|
/** Get a single agent instance by ID. Returns null if not found. */
|
|
576
754
|
getAgentInstance(agentId: string): Promise<DataAccessorAgentInstance | null>;
|
|
577
755
|
|
|
578
|
-
// ---- Agent task claiming ----
|
|
756
|
+
// ---- Agent task claiming (leased, T12502) ----
|
|
579
757
|
|
|
580
758
|
/**
|
|
581
|
-
*
|
|
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.
|
|
582
762
|
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
763
|
+
* With `sessionId: null` no lease is written; the call only verifies, in
|
|
764
|
+
* the same way, that no other session holds the task.
|
|
585
765
|
*
|
|
586
766
|
* @param taskId - ID of the task to claim.
|
|
587
|
-
* @param
|
|
588
|
-
* @
|
|
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.
|
|
589
772
|
*/
|
|
590
|
-
claimTask(taskId: string,
|
|
773
|
+
claimTask(taskId: string, request: TaskClaimRequest): Promise<TaskClaim | null>;
|
|
591
774
|
|
|
592
775
|
/**
|
|
593
|
-
* Release a
|
|
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.
|
|
594
778
|
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* @
|
|
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.
|
|
598
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.
|
|
599
798
|
*/
|
|
600
|
-
|
|
799
|
+
renewSessionClaims(sessionId: string, leaseExpiresAt: string): Promise<number>;
|
|
601
800
|
}
|
|
602
801
|
|
|
603
802
|
// Factory functions (createDataAccessor, getTaskAccessor) live in @cleocode/core,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision-site registry contracts — the "decision ladder as code" (spec
|
|
3
|
+
* `system-one-integration` §3, owner decisions D11158 and D11159).
|
|
4
|
+
*
|
|
5
|
+
* Every place CLEO makes a judgement is a decision site: it has one primary
|
|
6
|
+
* rung, an ordered ladder of rungs it may escalate to, a fallback, an
|
|
7
|
+
* owner-escalation rule, a mode and (to act on a System One answer) go-live
|
|
8
|
+
* evidence. The registry rows live in `packages/core/src/decide/sites/`;
|
|
9
|
+
* these are their types.
|
|
10
|
+
*
|
|
11
|
+
* Types and const data only (arch gate 10).
|
|
12
|
+
*
|
|
13
|
+
* @task T12662
|
|
14
|
+
* @epic T12486
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The decision rungs, cheapest first. Escalation only moves up this list.
|
|
19
|
+
*
|
|
20
|
+
* - `rule` — the answer follows from data (ids, statuses, thresholds).
|
|
21
|
+
* - `system-one` — a typed judgement over text with a small fixed answer set.
|
|
22
|
+
* - `generative` — the output must be free text or code.
|
|
23
|
+
* - `agent` — multi-step work.
|
|
24
|
+
* - `owner` — authority only the owner can take (the ask tool).
|
|
25
|
+
*/
|
|
26
|
+
export const DECISION_RUNGS = ['rule', 'system-one', 'generative', 'agent', 'owner'] as const;
|
|
27
|
+
|
|
28
|
+
/** One rung of the decision ladder. */
|
|
29
|
+
export type DecisionRung = (typeof DECISION_RUNGS)[number];
|
|
30
|
+
|
|
31
|
+
/** Site modes: `off` never asks, `shadow` asks and audits, `on` acts. */
|
|
32
|
+
export const DECISION_SITE_MODES = ['off', 'shadow', 'on'] as const;
|
|
33
|
+
|
|
34
|
+
/** How a site uses its System One rung. */
|
|
35
|
+
export type DecisionSiteModeValue = (typeof DECISION_SITE_MODES)[number];
|
|
36
|
+
|
|
37
|
+
/** The shape of the question a site answers. */
|
|
38
|
+
export type DecisionSiteQuestionType =
|
|
39
|
+
| 'noul'
|
|
40
|
+
| 'choice'
|
|
41
|
+
| 'score'
|
|
42
|
+
| 'text'
|
|
43
|
+
| 'multi-step'
|
|
44
|
+
| 'owner-choice';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* When a site may put a question to the owner.
|
|
48
|
+
*
|
|
49
|
+
* - `never` — the site never asks the owner.
|
|
50
|
+
* - `escalate-only` — it may add an owner question, never clear one.
|
|
51
|
+
* - `below-floor` — it asks the owner when every lower rung is below its floor.
|
|
52
|
+
* - `always` — the owner always decides.
|
|
53
|
+
*/
|
|
54
|
+
export type DecisionSiteOwnerEscalation = 'never' | 'escalate-only' | 'below-floor' | 'always';
|
|
55
|
+
|
|
56
|
+
/** Classes of text a site may send off the machine (spec §8). */
|
|
57
|
+
export type DecisionSiteTextClass =
|
|
58
|
+
| 'task-text'
|
|
59
|
+
| 'memory-text'
|
|
60
|
+
| 'diff'
|
|
61
|
+
| 'skill-text'
|
|
62
|
+
| 'external-text';
|
|
63
|
+
|
|
64
|
+
/** Measured evidence that lets a site act on its System One answer (`mode: on`). */
|
|
65
|
+
export interface DecisionSiteGoLive {
|
|
66
|
+
/** Canonical doc slug holding the measurement. */
|
|
67
|
+
readonly evidenceDoc: string;
|
|
68
|
+
/** ISO date of the measurement. */
|
|
69
|
+
readonly measuredAt: string;
|
|
70
|
+
/** Number of labelled decisions measured. */
|
|
71
|
+
readonly n: number;
|
|
72
|
+
/** Metric name, e.g. `precision@acting-class`. */
|
|
73
|
+
readonly metric: string;
|
|
74
|
+
/** Measured value. */
|
|
75
|
+
readonly value: number;
|
|
76
|
+
/** Floor the value had to clear. */
|
|
77
|
+
readonly floor: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One registry row: everything CLEO knows about a decision site. */
|
|
81
|
+
export interface DecisionSiteDefinition {
|
|
82
|
+
/** Site id; the string `decide()` and the audit log use, e.g. `tasks.duplicate-detection`. */
|
|
83
|
+
readonly id: string;
|
|
84
|
+
/** Human title. */
|
|
85
|
+
readonly title: string;
|
|
86
|
+
/** Repo-relative files where the site lives. */
|
|
87
|
+
readonly files: readonly string[];
|
|
88
|
+
/** The shape of the question. */
|
|
89
|
+
readonly questionType: DecisionSiteQuestionType;
|
|
90
|
+
/** The rung that answers first. */
|
|
91
|
+
readonly primaryRung: DecisionRung;
|
|
92
|
+
/** Rungs it may escalate to, in order, all above the primary. */
|
|
93
|
+
readonly ladder: readonly DecisionRung[];
|
|
94
|
+
/** What acts when the primary is unavailable. */
|
|
95
|
+
readonly fallback: DecisionRung | 'none';
|
|
96
|
+
/**
|
|
97
|
+
* Minimum confidence per question before `on` may act on a System One
|
|
98
|
+
* answer. The key `*` applies to every question of the site.
|
|
99
|
+
*/
|
|
100
|
+
readonly floors?: Readonly<Record<string, number>>;
|
|
101
|
+
/** When the site may ask the owner. */
|
|
102
|
+
readonly ownerEscalation: DecisionSiteOwnerEscalation;
|
|
103
|
+
/** Config key holding the mode (the existing `decide.sites.*` keys, D11159). */
|
|
104
|
+
readonly modeKey?: string;
|
|
105
|
+
/** Config key opting the site into its generative rung (`decide.generativeFallback.*`). */
|
|
106
|
+
readonly generativeKey?: string;
|
|
107
|
+
/** Mode when the config key is unset. */
|
|
108
|
+
readonly defaultMode: DecisionSiteModeValue;
|
|
109
|
+
/** Generative rung: the role or system key it resolves its model with. */
|
|
110
|
+
readonly llmSystemKey?: string;
|
|
111
|
+
/** True for a site on a write path: 300 ms budget, never blocks the write. */
|
|
112
|
+
readonly writePath: boolean;
|
|
113
|
+
/** Text classes it sends to a provider. */
|
|
114
|
+
readonly sends: readonly DecisionSiteTextClass[];
|
|
115
|
+
/** Go-live evidence, required before a System One site runs `on`. */
|
|
116
|
+
readonly goLive?: DecisionSiteGoLive;
|
|
117
|
+
/** Owning task. */
|
|
118
|
+
readonly task: string;
|
|
119
|
+
/** Free-form note (constraints such as "interactive only"). */
|
|
120
|
+
readonly note?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Audit counts for one site over the last seven days. */
|
|
124
|
+
export interface DecisionSiteActivity {
|
|
125
|
+
/** Audit lines written for the site. */
|
|
126
|
+
readonly asked: number;
|
|
127
|
+
/** Answered by the provider. */
|
|
128
|
+
readonly provider: number;
|
|
129
|
+
/** Answered from the decision cache. */
|
|
130
|
+
readonly cache: number;
|
|
131
|
+
/** Answered by the fallback. */
|
|
132
|
+
readonly fallback: number;
|
|
133
|
+
/** Escalated to a higher rung (0 until the ladder executor lands, T12665). */
|
|
134
|
+
readonly escalated: number;
|
|
135
|
+
/** Share of compared shadow lines where decision and heuristic agreed, when any were compared. */
|
|
136
|
+
readonly agreement?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One site as `cleo decide sites` reports it. */
|
|
140
|
+
export interface DecisionSiteSummary {
|
|
141
|
+
/** Site id. */
|
|
142
|
+
readonly id: string;
|
|
143
|
+
/** Human title. */
|
|
144
|
+
readonly title: string;
|
|
145
|
+
/** Question shape. */
|
|
146
|
+
readonly questionType: DecisionSiteQuestionType;
|
|
147
|
+
/** Primary rung. */
|
|
148
|
+
readonly primaryRung: DecisionRung;
|
|
149
|
+
/** Escalation ladder. */
|
|
150
|
+
readonly ladder: readonly DecisionRung[];
|
|
151
|
+
/** Fallback. */
|
|
152
|
+
readonly fallback: DecisionRung | 'none';
|
|
153
|
+
/** Owner-escalation rule. */
|
|
154
|
+
readonly ownerEscalation: DecisionSiteOwnerEscalation;
|
|
155
|
+
/** Registry default mode. */
|
|
156
|
+
readonly mode: DecisionSiteModeValue;
|
|
157
|
+
/** Mode set in config, when the site has a mode key and it is set. */
|
|
158
|
+
readonly configuredMode?: DecisionSiteModeValue;
|
|
159
|
+
/** Mode in force: `off` for a System One site while no provider is configured. */
|
|
160
|
+
readonly effectiveMode: DecisionSiteModeValue;
|
|
161
|
+
/** Config key holding the mode, when the site has one. */
|
|
162
|
+
readonly modeKey?: string;
|
|
163
|
+
/** Go-live evidence, when recorded. */
|
|
164
|
+
readonly goLive?: DecisionSiteGoLive;
|
|
165
|
+
/** Owning task. */
|
|
166
|
+
readonly task: string;
|
|
167
|
+
/** Audit activity over the last seven days. */
|
|
168
|
+
readonly last7d: DecisionSiteActivity;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Result of `cleo decide sites`. */
|
|
172
|
+
export interface DecisionSitesListResult {
|
|
173
|
+
/** Whether a System One provider is configured (else System One sites are `off`). */
|
|
174
|
+
readonly providerConfigured: boolean;
|
|
175
|
+
/** Registered sites total, before filters. */
|
|
176
|
+
readonly total: number;
|
|
177
|
+
/** Matching sites. */
|
|
178
|
+
readonly sites: readonly DecisionSiteSummary[];
|
|
179
|
+
}
|