@cleocode/contracts 2026.9.21 → 2026.9.23

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 (60) hide show
  1. package/dist/__tests__/evidence-atom-schema.test.js +20 -2
  2. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  3. package/dist/acceptance-gate-schema.d.ts.map +1 -1
  4. package/dist/acceptance-gate-schema.js +4 -1
  5. package/dist/acceptance-gate-schema.js.map +1 -1
  6. package/dist/data-accessor.d.ts +4 -0
  7. package/dist/data-accessor.d.ts.map +1 -1
  8. package/dist/data-accessor.js.map +1 -1
  9. package/dist/decide-sites.d.ts +157 -0
  10. package/dist/decide-sites.d.ts.map +1 -0
  11. package/dist/decide-sites.js +28 -0
  12. package/dist/decide-sites.js.map +1 -0
  13. package/dist/decide.d.ts +105 -0
  14. package/dist/decide.d.ts.map +1 -1
  15. package/dist/decide.js +31 -0
  16. package/dist/decide.js.map +1 -1
  17. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  18. package/dist/dispatch/operations-registry.js +38 -2
  19. package/dist/dispatch/operations-registry.js.map +1 -1
  20. package/dist/done-plan.d.ts +19 -0
  21. package/dist/done-plan.d.ts.map +1 -1
  22. package/dist/evidence-atom-schema.d.ts +12 -44
  23. package/dist/evidence-atom-schema.d.ts.map +1 -1
  24. package/dist/evidence-atom-schema.js +32 -14
  25. package/dist/evidence-atom-schema.js.map +1 -1
  26. package/dist/index.d.ts +6 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/operations/admin.d.ts +5 -0
  31. package/dist/operations/admin.d.ts.map +1 -1
  32. package/dist/operations/docs.d.ts +5 -2
  33. package/dist/operations/docs.d.ts.map +1 -1
  34. package/dist/operations/docs.js.map +1 -1
  35. package/dist/operations/tasks.d.ts +24 -0
  36. package/dist/operations/tasks.d.ts.map +1 -1
  37. package/dist/operations/tasks.js.map +1 -1
  38. package/dist/task.d.ts +32 -1
  39. package/dist/task.d.ts.map +1 -1
  40. package/dist/tools/index.d.ts +1 -1
  41. package/dist/tools/index.d.ts.map +1 -1
  42. package/dist/tools/score-task-priority.d.ts +28 -3
  43. package/dist/tools/score-task-priority.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  46. package/src/__tests__/evidence-atom-schema.test.ts +25 -2
  47. package/src/acceptance-gate-schema.ts +4 -1
  48. package/src/data-accessor.ts +4 -0
  49. package/src/decide-sites.ts +179 -0
  50. package/src/decide.ts +109 -0
  51. package/src/dispatch/operations-registry.ts +40 -2
  52. package/src/done-plan.ts +20 -0
  53. package/src/evidence-atom-schema.ts +34 -18
  54. package/src/index.ts +25 -0
  55. package/src/operations/admin.ts +5 -0
  56. package/src/operations/docs.ts +5 -2
  57. package/src/operations/tasks.ts +26 -0
  58. package/src/task.ts +32 -1
  59. package/src/tools/index.ts +1 -0
  60. package/src/tools/score-task-priority.ts +29 -3
@@ -499,6 +499,28 @@ describe('parseEvidenceString (T10337)', () => {
499
499
 
500
500
  // ─── GATE_EVIDENCE_REQUIREMENTS — gate-to-atom mapping ─────────────────────
501
501
 
502
+ describe('pr:/ci: component references (T12671)', () => {
503
+ it('pr:<component>@<integration> names the integration PR and its component', () => {
504
+ expect(parseEvidenceString('pr:42@41')).toEqual([
505
+ { kind: 'pr', prNumber: 41, componentPrNumber: 42 },
506
+ ]);
507
+ expect(parseEvidenceString('ci:42@41')).toEqual([
508
+ { kind: 'ci', prNumber: 41, componentPrNumber: 42 },
509
+ ]);
510
+ expect(parseEvidenceString('pr:41')).toEqual([{ kind: 'pr', prNumber: 41 }]);
511
+ });
512
+
513
+ it.each(['pr:42@42', 'pr:42@', 'pr:@41', 'ci:0@41', 'pr:42@4x'])('rejects %s', (raw) => {
514
+ expect(() => parseEvidenceString(raw)).toThrow(EvidenceParseError);
515
+ });
516
+
517
+ it('the schema accepts the component field', () => {
518
+ expect(
519
+ EvidenceAtomSchema.safeParse({ kind: 'ci', prNumber: 41, componentPrNumber: 42 }).success,
520
+ ).toBe(true);
521
+ });
522
+ });
523
+
502
524
  describe('GATE_EVIDENCE_REQUIREMENTS (T10337)', () => {
503
525
  it('covers every VerificationGate', () => {
504
526
  const expected: VerificationGate[] = [
@@ -662,7 +684,8 @@ describe('formatGateRequirement (T10337)', () => {
662
684
  expect(s).toContain('[decision AND note]');
663
685
  expect(s).toContain('[pr AND files]');
664
686
  // Joiner between combinations is ' OR '.
665
- expect(s.split(' OR ').length).toBe(5);
687
+ // T12689: + [pr AND note] for a deletion-only PR.
688
+ expect(s.split(' OR ').length).toBe(6);
666
689
  });
667
690
 
668
691
  it('formats single-element combination without AND', () => {
@@ -737,7 +760,7 @@ describe('validateEvidenceForGate failure surface (T9949)', () => {
737
760
  if (!r.ok) {
738
761
  // Error includes the same artifact requirement as the validator.
739
762
  expect(r.message).toBe(
740
- "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]",
741
764
  );
742
765
  // New rich hint includes the same content as formatGateRequirementHint.
743
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
- if (executable && !binding.invocation)
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'],
@@ -255,6 +255,8 @@ export interface AcBindingRow {
255
255
 
256
256
  /** Query options for bounded reads from the append-only task audit log. @task T10594 */
257
257
  export interface TaskAuditLogQuery {
258
+ /** Exact audit row ids (T12693: revert one ranking change). */
259
+ ids?: readonly string[];
258
260
  taskIds?: readonly string[];
259
261
  actions?: readonly string[];
260
262
  since?: string;
@@ -271,6 +273,8 @@ export interface TaskAuditLogRow {
271
273
  detailsJson: string | null;
272
274
  beforeJson: string | null;
273
275
  afterJson: string | null;
276
+ /** Session the mutation ran in, when recorded (T12693). */
277
+ sessionId?: string | null;
274
278
  }
275
279
 
276
280
  /**
@@ -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
+ }
package/src/decide.ts CHANGED
@@ -143,6 +143,17 @@ export interface DecisionRequest {
143
143
  readonly state: DecisionState;
144
144
  /** Question name → question (1–32 entries). Names key the answers in {@link DecisionOutcome}. */
145
145
  readonly questions: Readonly<Record<string, DecisionQuestion>>;
146
+ /**
147
+ * Language hint (e.g. `en`). Sent only to a provider whose capabilities
148
+ * include `langHint`; others never see it (T12664).
149
+ */
150
+ readonly lang?: string;
151
+ /**
152
+ * Provider-side answer cache. `false` asks a provider with `cacheControl`
153
+ * not to answer from its cache (real latency, sensitive text). Ignored by
154
+ * providers without the capability.
155
+ */
156
+ readonly cache?: boolean;
146
157
  }
147
158
 
148
159
  /** Answer to a {@link NoulDecisionQuestion}. */
@@ -196,14 +207,100 @@ export interface DecisionOutcome {
196
207
  readonly latencyMs: number;
197
208
  /** Provider-reported cost in US dollars, when known. */
198
209
  readonly costUsd?: number;
210
+ /** Provider-reported cost in integer micro-dollars, when known (T12664). */
211
+ readonly costMicros?: number;
212
+ /** Account balance in integer micro-dollars reported with this answer, when known. */
213
+ readonly balanceMicros?: number;
214
+ /** Provider model checkpoint that answered, when reported. */
215
+ readonly checkpoint?: string;
199
216
  /** Provider-reported input-token count, when known. */
200
217
  readonly inputTokens?: number;
201
218
  }
202
219
 
220
+ // ─── Provider kinds (T12713) ─────────────────────────────────────────────────
221
+
222
+ /**
223
+ * Every supported decision-provider kind, recommended first.
224
+ *
225
+ * - `layahost` — the hosted provider at {@link LAYAHOST_BASE_URL}; the user
226
+ * supplies only an API key.
227
+ * - `jev` — any other Jev-compatible endpoint; the user supplies the URL.
228
+ */
229
+ export const DECISION_PROVIDER_KINDS = ['layahost', 'jev'] as const;
230
+
231
+ /** A decision-provider kind. See {@link DECISION_PROVIDER_KINDS}. */
232
+ export type DecisionProviderKind = (typeof DECISION_PROVIDER_KINDS)[number];
233
+
234
+ /** Base URL of the layahost API (its OpenAPI `servers[0].url`). */
235
+ export const LAYAHOST_BASE_URL = 'https://layahost.com';
236
+
237
+ /** layahost's routing model, the default for a layahost connection. */
238
+ export const LAYAHOST_DEFAULT_MODEL = 'laya-auto';
239
+
203
240
  /** Connection settings for a decision provider. */
204
241
  export interface DecisionProviderConfig {
205
242
  /** Absolute base URL of the provider's API (no trailing path to the endpoint). */
206
243
  readonly baseUrl: string;
244
+ /** Provider kind; absent means a custom Jev-compatible endpoint (`jev`). */
245
+ readonly provider?: DecisionProviderKind;
246
+ }
247
+
248
+ // ─── Provider capabilities (T12664) ──────────────────────────────────────────
249
+
250
+ /**
251
+ * What a decision provider can do beyond the Jev minimum
252
+ * (`POST /v1/systemone`, 32 questions, 32,000-character state). A caller asks
253
+ * for a capability; the client degrades when it is absent (spec
254
+ * `system-one-integration` §2.2).
255
+ */
256
+ export interface DecisionProviderCapabilities {
257
+ /** Wire protocol the provider speaks. */
258
+ readonly wire: string;
259
+ /** Most questions one request may carry. */
260
+ readonly maxQuestionsPerRequest: number;
261
+ /** Largest state, in characters. */
262
+ readonly maxStateChars: number;
263
+ /** Batch endpoint limits, when the provider has one. */
264
+ readonly batch?: { readonly maxRequests: number; readonly maxQuestions: number };
265
+ /** Built-in template names, when the provider has templates. */
266
+ readonly templates?: readonly string[];
267
+ /** Server-side flows are available. */
268
+ readonly flows?: boolean;
269
+ /** A usage/balance endpoint and cost headers are available. */
270
+ readonly usage?: boolean;
271
+ /** The request's `cache` field is honoured. */
272
+ readonly cacheControl?: boolean;
273
+ /** The request's `lang` field is honoured. */
274
+ readonly langHint?: boolean;
275
+ /** How the provider reports cost. */
276
+ readonly reportsCost?: 'micros' | 'usd' | false;
277
+ }
278
+
279
+ /** The Jev minimum every provider supports; the default when a provider reports nothing. */
280
+ export const JEV_MINIMUM_CAPABILITIES: DecisionProviderCapabilities = {
281
+ wire: 'jev-systemone/1',
282
+ maxQuestionsPerRequest: 32,
283
+ maxStateChars: 32_000,
284
+ reportsCost: 'usd',
285
+ };
286
+
287
+ /** One item of a batch call: its outcome, or why it failed. */
288
+ export type DecisionBatchItem =
289
+ | { readonly ok: true; readonly outcome: DecisionOutcome }
290
+ | { readonly ok: false; readonly status?: number; readonly errorKind: string };
291
+
292
+ /** Account usage and balance as a provider reports it. */
293
+ export interface DecisionProviderUsage {
294
+ /** Balance in integer micro-dollars. */
295
+ readonly balanceMicros?: number;
296
+ /** Decisions the balance still covers. */
297
+ readonly decisionsLeft?: number;
298
+ /** Plan name. */
299
+ readonly plan?: string;
300
+ /** End of the current plan period, ISO-8601. */
301
+ readonly periodEnd?: string;
302
+ /** Decisions and cost over the queried window. */
303
+ readonly totals?: { readonly decisions?: number; readonly costMicros?: number };
207
304
  }
208
305
 
209
306
  // ─── Zod schemas ──────────────────────────────────────────────────────────────
@@ -290,6 +387,11 @@ export const decisionQuestionSchema = z.discriminatedUnion('type', [
290
387
  */
291
388
  export const decisionRequestSchema = z.object({
292
389
  model: z.string().min(1).optional(),
390
+ lang: z
391
+ .string()
392
+ .regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/)
393
+ .optional(),
394
+ cache: z.boolean().optional(),
293
395
  state: decisionStateSchema,
294
396
  questions: z.record(z.string().min(1), decisionQuestionSchema).refine(
295
397
  (questions) => {
@@ -346,10 +448,17 @@ export const decisionOutcomeSchema = z.object({
346
448
  requestId: z.string().min(1).optional(),
347
449
  latencyMs: z.number().nonnegative(),
348
450
  costUsd: z.number().nonnegative().optional(),
451
+ costMicros: z.number().int().nonnegative().optional(),
452
+ balanceMicros: z.number().int().optional(),
453
+ checkpoint: z.string().min(1).optional(),
349
454
  inputTokens: z.number().int().nonnegative().optional(),
350
455
  });
351
456
 
457
+ /** Zod schema for {@link DecisionProviderKind}. */
458
+ export const decisionProviderKindSchema = z.enum(DECISION_PROVIDER_KINDS);
459
+
352
460
  /** Zod schema for {@link DecisionProviderConfig}; `baseUrl` must be an absolute URL. */
353
461
  export const decisionProviderConfigSchema = z.object({
354
462
  baseUrl: z.url(),
463
+ provider: decisionProviderKindSchema.optional(),
355
464
  });
@@ -814,12 +814,22 @@ export const OPERATIONS: OperationDef[] = [
814
814
  gateway: 'query' as const,
815
815
  domain: 'tasks' as const,
816
816
  operation: 'history',
817
- description: 'Show task work history (time tracked per task)',
817
+ description:
818
+ 'Show task work history; with taskId, the task audit trail; with ranking, who changed its priority/severity/kind/depends and why (T12693)',
818
819
  tier: 1,
819
820
  idempotent: true,
820
821
  sessionRequired: false,
821
822
  requiredParams: [],
822
- params: [],
823
+ params: [
824
+ { name: 'taskId', type: 'string', required: false, description: 'Task to show history for' },
825
+ { name: 'limit', type: 'number', required: false, description: 'Maximum entries' },
826
+ {
827
+ name: 'ranking',
828
+ type: 'boolean',
829
+ required: false,
830
+ description: 'Only ranking-input changes (actor, session, reason, before/after)',
831
+ },
832
+ ] satisfies ParamDef[],
823
833
  },
824
834
  {
825
835
  gateway: 'query',
@@ -3364,6 +3374,33 @@ export const OPERATIONS: OperationDef[] = [
3364
3374
  ] satisfies ParamDef[],
3365
3375
  outputSchema: OUTPUT_CONTRACTS['tasks.assignee'],
3366
3376
  },
3377
+ {
3378
+ gateway: 'mutate',
3379
+ domain: 'tasks',
3380
+ operation: 'ranking.revert',
3381
+ description:
3382
+ 'tasks.ranking.revert (mutate) — undo one recorded ranking change (priority/severity/kind/depends) as a new audited change (T12693, D11161)',
3383
+ tier: 1,
3384
+ idempotent: false,
3385
+ sessionRequired: false,
3386
+ requiredParams: ['entryId'],
3387
+ params: [
3388
+ {
3389
+ name: 'entryId',
3390
+ type: 'string',
3391
+ required: true,
3392
+ description: 'ranking_changed audit row id (cleo history ranking <taskId>)',
3393
+ cli: { positional: true },
3394
+ },
3395
+ { name: 'reason', type: 'string', required: false, description: 'Why it is reverted' },
3396
+ {
3397
+ name: 'force',
3398
+ type: 'boolean',
3399
+ required: false,
3400
+ description: 'Revert even though a field changed again since',
3401
+ },
3402
+ ] satisfies ParamDef[],
3403
+ },
3367
3404
  {
3368
3405
  gateway: 'mutate',
3369
3406
  domain: 'tasks',
@@ -8456,6 +8493,7 @@ export const OPERATIONS: OperationDef[] = [
8456
8493
  enum: DOCS_LIFECYCLE_STATUSES,
8457
8494
  description:
8458
8495
  'Override the new lifecycle status. Defaults to "draft" on every update. ' +
8496
+ 'Alone (no file or content) it changes only the lifecycle status. ' +
8459
8497
  `Valid: ${DOCS_LIFECYCLE_STATUSES.join('|')}.`,
8460
8498
  },
8461
8499
  {
package/src/done-plan.ts CHANGED
@@ -119,6 +119,11 @@ export interface TaskChangeSet {
119
119
  executionRoot: string;
120
120
  /** Why {@link executionRoot} was chosen. */
121
121
  rootSource: ChangeSetRootSource;
122
+ /**
123
+ * Component PR whose change the (integration) `prNumber` landed on the
124
+ * default branch (T12671). Evidence is then `pr:<component>@<prNumber>`.
125
+ */
126
+ componentPrNumber?: number;
122
127
  /** Merged PR number (`source === 'pr'`). */
123
128
  prNumber?: number;
124
129
  /**
@@ -168,8 +173,23 @@ export interface TaskChangeSet {
168
173
  blockers: DonePlanBlocker[];
169
174
  /** Non-blocking observations, e.g. PR discovery that could not reach `gh`. */
170
175
  warnings: string[];
176
+ /**
177
+ * The merged-PR lookup failed (e.g. `gh` unavailable), so a non-PR change set
178
+ * does not prove the change is unmerged (T12656).
179
+ */
180
+ prDiscoveryFailed?: boolean;
181
+ /**
182
+ * Whether the task's PR has merged to the default branch, as the PR lookup
183
+ * found it — independent of `source` (T12656 review): a merged PR whose
184
+ * `pr:` check was refused still falls back to the task branch, and the
185
+ * branch outlives a squash merge. Absent when no PR was examined.
186
+ */
187
+ mergeState?: ChangeSetMergeState;
171
188
  }
172
189
 
190
+ /** Whether a task's change has merged to the default branch. */
191
+ export type ChangeSetMergeState = 'merged' | 'unmerged' | 'unknown';
192
+
173
193
  /** Planned state of one verification gate. */
174
194
  export interface DonePlanGate {
175
195
  /** Gate name. */
@@ -198,7 +198,11 @@ export const decisionAtomSchema = z.object({
198
198
  * checks task relationship and changed artifacts. Implementation also needs
199
199
  * inspected file evidence; testing and review require actual result atoms.
200
200
  *
201
- * Format: `pr:<positive integer>` (e.g. `pr:357`).
201
+ * Format: `pr:<positive integer>` (e.g. `pr:357`), or
202
+ * `pr:<component>@<integration>` (e.g. `pr:356@357`, T12671): component PR
203
+ * #356 merged into an integration branch that PR #357 landed on the default
204
+ * branch. The integration PR supplies the merge commit; the component PR
205
+ * supplies the task linkage and the changed files.
202
206
  *
203
207
  * @task T9764
204
208
  * @task T9838
@@ -206,6 +210,7 @@ export const decisionAtomSchema = z.object({
206
210
  export const prAtomSchema = z.object({
207
211
  kind: z.literal('pr'),
208
212
  prNumber: z.number().int().positive('pr atom requires a positive integer PR number'),
213
+ componentPrNumber: z.number().int().positive().optional(),
209
214
  });
210
215
 
211
216
  /**
@@ -214,13 +219,16 @@ export const prAtomSchema = z.object({
214
219
  * `qaPassed` only when the project opts in with `evidence.ciSatisfies`
215
220
  * (owner decision D11149).
216
221
  *
217
- * Format: `ci:<positive integer>` (e.g. `ci:357`).
222
+ * Format: `ci:<positive integer>` (e.g. `ci:357`), or
223
+ * `ci:<component>@<integration>` (e.g. `ci:356@357`, T12671): the integration
224
+ * PR's merge-commit CI, for a task linked through its component PR.
218
225
  *
219
226
  * @task T12634
220
227
  */
221
228
  export const ciAtomSchema = z.object({
222
229
  kind: z.literal('ci'),
223
230
  prNumber: z.number().int().positive('ci atom requires a positive integer PR number'),
231
+ componentPrNumber: z.number().int().positive().optional(),
224
232
  });
225
233
 
226
234
  /**
@@ -492,6 +500,9 @@ export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
492
500
  ['decision', 'files'],
493
501
  ['decision', 'note'],
494
502
  ['pr', 'files'],
503
+ // T12689: a deletion-only PR (no bytes to hash). Contextual validation
504
+ // accepts this only when every change the PR made is a deletion.
505
+ ['pr', 'note'],
495
506
  ],
496
507
  },
497
508
  testsPassed: { oneOf: [['test-run'], ['tool'], ['ci']] },
@@ -771,6 +782,21 @@ export class EvidenceParseError extends Error {
771
782
  * @task T10337
772
783
  * @adr ADR-051
773
784
  */
785
+ /**
786
+ * `<n>` or `<component>@<integration>` (T12671), each a positive integer.
787
+ * The integration PR is the atom's `prNumber`.
788
+ */
789
+ function parsePrReference(
790
+ payload: string,
791
+ ): { prNumber: number; componentPrNumber?: number } | null {
792
+ const match = /^(\d+)(?:@(\d+))?$/.exec(payload);
793
+ if (!match) return null;
794
+ const first = Number(match[1]);
795
+ const second = match[2] === undefined ? null : Number(match[2]);
796
+ if (first <= 0 || (second !== null && (second <= 0 || second === first))) return null;
797
+ return second === null ? { prNumber: first } : { prNumber: second, componentPrNumber: first };
798
+ }
799
+
774
800
  export function parseEvidenceString(raw: string): EvidenceAtom[] {
775
801
  if (!raw || typeof raw !== 'string') {
776
802
  throw new EvidenceParseError(
@@ -840,26 +866,16 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
840
866
  atoms.push({ kind: 'decision', decisionId: payload });
841
867
  break;
842
868
  }
843
- case 'pr': {
844
- const prNumber = Number(payload);
845
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
846
- throw new EvidenceParseError(
847
- `pr atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
848
- 'Use format: pr:<number> e.g. pr:357',
849
- );
850
- }
851
- atoms.push({ kind: 'pr', prNumber });
852
- break;
853
- }
869
+ case 'pr':
854
870
  case 'ci': {
855
- const prNumber = Number(payload);
856
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
871
+ const ref = parsePrReference(payload);
872
+ if (!ref) {
857
873
  throw new EvidenceParseError(
858
- `ci atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
859
- 'Use format: ci:<number> e.g. ci:357',
874
+ `${kind} atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
875
+ `Use format: ${kind}:<number> e.g. ${kind}:357, or ${kind}:<component>@<integration> e.g. ${kind}:356@357`,
860
876
  );
861
877
  }
862
- atoms.push({ kind: 'ci', prNumber });
878
+ atoms.push(kind === 'pr' ? { kind: 'pr', ...ref } : { kind: 'ci', ...ref });
863
879
  break;
864
880
  }
865
881
  case 'loc-drop': {
package/src/index.ts CHANGED
@@ -516,6 +516,7 @@ export type {
516
516
  // === Done planner: read-only `cleo done --plan` evidence plan (T12623, T12624) ===
517
517
  export type {
518
518
  ChangeSetDoc,
519
+ ChangeSetMergeState,
519
520
  ChangeSetPrCandidate,
520
521
  ChangeSetRootSource,
521
522
  ChangeSetSource,
@@ -2702,6 +2703,7 @@ export type {
2702
2703
  ScoreFactor,
2703
2704
  ScoreTaskContext,
2704
2705
  ScoreTaskInput,
2706
+ ScoreTaskKey,
2705
2707
  ScoreTaskResult,
2706
2708
  } from './tools/score-task-priority.js';
2707
2709
  // === Transport (low-level wire protocol) ===
@@ -2889,11 +2891,15 @@ export type {
2889
2891
  ChoiceDecisionAnswer,
2890
2892
  ChoiceDecisionQuestion,
2891
2893
  DecisionAnswer,
2894
+ DecisionBatchItem,
2892
2895
  DecisionInstructions,
2893
2896
  DecisionJsonValue,
2894
2897
  DecisionOutcome,
2895
2898
  DecisionOutcomeSource,
2899
+ DecisionProviderCapabilities,
2896
2900
  DecisionProviderConfig,
2901
+ DecisionProviderKind,
2902
+ DecisionProviderUsage,
2897
2903
  DecisionQuestion,
2898
2904
  DecisionQuestionType,
2899
2905
  DecisionRequest,
@@ -2907,6 +2913,7 @@ export {
2907
2913
  choiceDecisionAnswerSchema,
2908
2914
  choiceDecisionQuestionSchema,
2909
2915
  DECISION_OUTCOME_SOURCES,
2916
+ DECISION_PROVIDER_KINDS,
2910
2917
  DECISION_QUESTION_TYPES,
2911
2918
  DECISION_REQUEST_LIMITS,
2912
2919
  decisionAnswerSchema,
@@ -2914,14 +2921,32 @@ export {
2914
2921
  decisionOutcomeSchema,
2915
2922
  decisionOutcomeSourceSchema,
2916
2923
  decisionProviderConfigSchema,
2924
+ decisionProviderKindSchema,
2917
2925
  decisionQuestionSchema,
2918
2926
  decisionQuestionTypeSchema,
2919
2927
  decisionRequestSchema,
2920
2928
  decisionStateSchema,
2929
+ JEV_MINIMUM_CAPABILITIES,
2930
+ LAYAHOST_BASE_URL,
2931
+ LAYAHOST_DEFAULT_MODEL,
2921
2932
  noulDecisionAnswerSchema,
2922
2933
  noulDecisionQuestionSchema,
2923
2934
  scoreDecisionAnswerSchema,
2924
2935
  scoreDecisionQuestionSchema,
2925
2936
  } from './decide.js';
2937
+ // T12662: decision-site registry (the decision ladder as code).
2938
+ export type {
2939
+ DecisionRung,
2940
+ DecisionSiteActivity,
2941
+ DecisionSiteDefinition,
2942
+ DecisionSiteGoLive,
2943
+ DecisionSiteModeValue,
2944
+ DecisionSiteOwnerEscalation,
2945
+ DecisionSiteQuestionType,
2946
+ DecisionSiteSummary,
2947
+ DecisionSitesListResult,
2948
+ DecisionSiteTextClass,
2949
+ } from './decide-sites.js';
2950
+ export { DECISION_RUNGS, DECISION_SITE_MODES } from './decide-sites.js';
2926
2951
 
2927
2952
  export { isStorableTaskId, isTaskId, TASK_ID_REGEX, type TaskId } from './task-id.js';