@cleocode/contracts 2026.5.120 → 2026.5.122

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 (63) hide show
  1. package/dist/__tests__/changesets.test.d.ts +8 -0
  2. package/dist/__tests__/changesets.test.d.ts.map +1 -0
  3. package/dist/__tests__/changesets.test.js +146 -0
  4. package/dist/__tests__/changesets.test.js.map +1 -0
  5. package/dist/__tests__/evidence-atom-schema.test.js +75 -1
  6. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  7. package/dist/__tests__/satisfies-atom.test.d.ts +24 -0
  8. package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
  9. package/dist/__tests__/satisfies-atom.test.js +302 -0
  10. package/dist/__tests__/satisfies-atom.test.js.map +1 -0
  11. package/dist/__tests__/validator-types.test.d.ts +13 -0
  12. package/dist/__tests__/validator-types.test.d.ts.map +1 -0
  13. package/dist/__tests__/validator-types.test.js +293 -0
  14. package/dist/__tests__/validator-types.test.js.map +1 -0
  15. package/dist/changesets.d.ts +91 -0
  16. package/dist/changesets.d.ts.map +1 -1
  17. package/dist/changesets.js +72 -2
  18. package/dist/changesets.js.map +1 -1
  19. package/dist/config.d.ts +56 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/data-accessor.d.ts +117 -0
  22. package/dist/data-accessor.d.ts.map +1 -1
  23. package/dist/data-accessor.js.map +1 -1
  24. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  25. package/dist/dispatch/operations-registry.js +29 -0
  26. package/dist/dispatch/operations-registry.js.map +1 -1
  27. package/dist/evidence-atom-schema.d.ts +143 -8
  28. package/dist/evidence-atom-schema.d.ts.map +1 -1
  29. package/dist/evidence-atom-schema.js +258 -1
  30. package/dist/evidence-atom-schema.js.map +1 -1
  31. package/dist/exit-codes.d.ts +41 -1
  32. package/dist/exit-codes.d.ts.map +1 -1
  33. package/dist/exit-codes.js +40 -0
  34. package/dist/exit-codes.js.map +1 -1
  35. package/dist/index.d.ts +8 -6
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/operations/tasks.d.ts +75 -0
  40. package/dist/operations/tasks.d.ts.map +1 -1
  41. package/dist/operations/tasks.js.map +1 -1
  42. package/dist/task.d.ts +38 -0
  43. package/dist/task.d.ts.map +1 -1
  44. package/dist/validator/index.d.ts +375 -0
  45. package/dist/validator/index.d.ts.map +1 -0
  46. package/dist/validator/index.js +162 -0
  47. package/dist/validator/index.js.map +1 -0
  48. package/package.json +2 -2
  49. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  50. package/src/__tests__/changesets.test.ts +176 -0
  51. package/src/__tests__/evidence-atom-schema.test.ts +88 -0
  52. package/src/__tests__/satisfies-atom.test.ts +363 -0
  53. package/src/__tests__/validator-types.test.ts +359 -0
  54. package/src/changesets.ts +93 -2
  55. package/src/config.ts +58 -0
  56. package/src/data-accessor.ts +120 -0
  57. package/src/dispatch/operations-registry.ts +31 -0
  58. package/src/evidence-atom-schema.ts +319 -8
  59. package/src/exit-codes.ts +42 -0
  60. package/src/index.ts +39 -0
  61. package/src/operations/tasks.ts +76 -0
  62. package/src/task.ts +39 -0
  63. package/src/validator/index.ts +385 -0
@@ -7629,6 +7629,37 @@ export const OPERATIONS: OperationDef[] = [
7629
7629
  ] satisfies ParamDef[],
7630
7630
  },
7631
7631
 
7632
+ // release query: validate-changelog (T9937 / Saga T9862 — canonical CHANGELOG
7633
+ // header validator that replaces the brittle inline grep step in
7634
+ // .github/workflows/release.yml). Read-only — exposed only under the query
7635
+ // gateway because no DB or filesystem mutation occurs.
7636
+ {
7637
+ gateway: 'query',
7638
+ domain: 'release',
7639
+ operation: 'validate-changelog',
7640
+ description:
7641
+ 'release.validate-changelog (query) — validate that CHANGELOG.md contains the canonical `## [VERSION]` header per ADR-028 §2.5. Replaces the brittle inline grep in .github/workflows/release.yml (T9937 / Saga T9862).',
7642
+ tier: 1,
7643
+ idempotent: true,
7644
+ sessionRequired: false,
7645
+ requiredParams: ['version'],
7646
+ params: [
7647
+ {
7648
+ name: 'version',
7649
+ type: 'string',
7650
+ required: true,
7651
+ description: 'Release version (accepts v2026.5.94 or 2026.5.94)',
7652
+ cli: { positional: true },
7653
+ },
7654
+ {
7655
+ name: 'changelogPath',
7656
+ type: 'string',
7657
+ required: false,
7658
+ description: 'Override the CHANGELOG file path (default: <projectRoot>/CHANGELOG.md)',
7659
+ },
7660
+ ] satisfies ParamDef[],
7661
+ },
7662
+
7632
7663
  // ---------------------------------------------------------------------------
7633
7664
  // Provenance domain — `cleo provenance` CLI surface (T9528 · Phase 2 of T9493)
7634
7665
  // ---------------------------------------------------------------------------
@@ -195,6 +195,117 @@ export const callsiteCoverageAtomSchema = z.object({
195
195
  .min(1, 'callsite-coverage atom requires a non-empty relativeSourcePath'),
196
196
  });
197
197
 
198
+ // ---------------------------------------------------------------------------
199
+ // ADR-079-r2: satisfies atom — cross-task AC binding
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Strict UUIDv4 regex (lowercase, hyphenated 8-4-4-4-12 with the `4` major
204
+ * version nibble and `[89ab]` variant nibble) per ADR-079-r2 §2.1 ABNF
205
+ * (`ac-uuid` production) and RFC 9562 §5.4. Used by the `satisfies:` atom
206
+ * to validate the target-AC UUID form.
207
+ *
208
+ * Validators MUST reject mixed-case to prevent silent dedupe failures — the
209
+ * canonical column casing is lowercase per ADR-079-r1 §2.1.
210
+ *
211
+ * @task T10506
212
+ * @adr ADR-079-r2 §2.1
213
+ */
214
+ export const AC_UUID_REGEX =
215
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
216
+
217
+ /**
218
+ * Positional-alias regex for `satisfies:` atom target-AC identifiers per
219
+ * ADR-079-r2 §2.1 ABNF (`ac-alias` production). Format: `AC<1-4 digits>`
220
+ * (capped at AC9999 — three orders of magnitude above realistic per-task AC
221
+ * counts).
222
+ *
223
+ * @task T10506
224
+ * @adr ADR-079-r2 §2.1
225
+ */
226
+ export const AC_ALIAS_REGEX = /^AC[0-9]{1,4}$/;
227
+
228
+ /**
229
+ * Task-id regex for `satisfies:` atom target-task identifiers per ADR-079-r2
230
+ * §2.1 ABNF (`task-id` production). Format: `T<1-7 digits>` (capped at
231
+ * T9999999 — three orders of magnitude above current CLEO task IDs as of
232
+ * 2026-05-24).
233
+ *
234
+ * @task T10506
235
+ * @adr ADR-079-r2 §2.1
236
+ */
237
+ export const SATISFIES_TASK_ID_REGEX = /^T[0-9]{1,7}$/;
238
+
239
+ /**
240
+ * Version-pin regex for `satisfies:` atom optional `@<ts>` suffix per
241
+ * ADR-079-r2 §2.1 ABNF (`version-pin` production). Format: `YYYYMMDDhhmmss`
242
+ * (ISO-8601-basic timestamp, 14 digits).
243
+ *
244
+ * @task T10506
245
+ * @adr ADR-079-r2 §2.1
246
+ */
247
+ export const SATISFIES_VERSION_PIN_REGEX = /^[0-9]{14}$/;
248
+
249
+ /**
250
+ * `satisfies:<task-id>#<ac-id>[@<version-pin>]` atom — cross-task AC binding
251
+ * per ADR-079-r2 §2.1 (full grammar) and ADR-079-r1 §2.4 (basic shape).
252
+ *
253
+ * Format:
254
+ *
255
+ * - `satisfies:T1234#a1b2c3d4-5e6f-4890-abcd-ef1234567890` — canonical
256
+ * UUID form (preferred for long-lived specs).
257
+ * - `satisfies:T1234#AC2` — positional
258
+ * alias form (preferred in fresh PRs where the target task's AC list
259
+ * is stable).
260
+ * - `satisfies:T1234#AC2@20260524223045` — alias +
261
+ * optional version-pin (Validator emissions where pin-on-mint is
262
+ * required by ADR-079-r2 §3.4).
263
+ *
264
+ * Exactly ONE of `targetAcId` (UUID form) or `targetAcAlias` (alias form)
265
+ * is populated per parsed atom. `versionPin` is always optional.
266
+ *
267
+ * ## Scope
268
+ *
269
+ * This Zod schema covers PARSING ONLY. The runtime validator semantics —
270
+ * the 5-check accept/reject pipeline (target exists, target not terminal,
271
+ * AC exists, same-saga scope rule) — ship in T10507 alongside the
272
+ * `evidence_satisfies_bindings` side-effect table.
273
+ *
274
+ * Per ADR-079-r2 §2.2 the maximum atom length is 120 chars; the schema does
275
+ * NOT enforce length here because the per-field regexes already cap the
276
+ * surface (`satisfies:` + 8-char T+digits + `#` + 36-char UUID + `@` +
277
+ * 14-char pin = 78 chars max — well under 120).
278
+ *
279
+ * @task T10506
280
+ * @adr ADR-079-r2 §2.1
281
+ * @adr ADR-079-r1 §2.4
282
+ */
283
+ export const satisfiesAtomSchema = z.object({
284
+ kind: z.literal('satisfies'),
285
+ /** Target task ID — `T<1-7 digits>` per ADR-079-r2 §2.1. */
286
+ targetTaskId: z
287
+ .string()
288
+ .regex(SATISFIES_TASK_ID_REGEX, 'satisfies atom targetTaskId must match /^T[0-9]{1,7}$/'),
289
+ /** Lowercase UUIDv4 — populated for the canonical form; undefined for alias form. */
290
+ targetAcId: z
291
+ .string()
292
+ .regex(AC_UUID_REGEX, 'satisfies atom targetAcId must be a lowercase UUIDv4')
293
+ .optional(),
294
+ /** Positional alias `AC<1-4 digits>` — populated for alias form; undefined for UUID form. */
295
+ targetAcAlias: z
296
+ .string()
297
+ .regex(AC_ALIAS_REGEX, 'satisfies atom targetAcAlias must match /^AC[0-9]{1,4}$/')
298
+ .optional(),
299
+ /** Optional `@<14-digit YYYYMMDDhhmmss>` pin captured at mint time. */
300
+ versionPin: z
301
+ .string()
302
+ .regex(
303
+ SATISFIES_VERSION_PIN_REGEX,
304
+ 'satisfies atom versionPin must be 14 digits (YYYYMMDDhhmmss)',
305
+ )
306
+ .optional(),
307
+ });
308
+
198
309
  // ---------------------------------------------------------------------------
199
310
  // Discriminated union
200
311
  // ---------------------------------------------------------------------------
@@ -218,6 +329,8 @@ export const callsiteCoverageAtomSchema = z.object({
218
329
  * - `pr` — merged-PR retroactive proof (T9764 / T9838)
219
330
  * - `loc-drop` — engine-migration LOC reduction (T1604)
220
331
  * - `callsite-coverage` — exported-symbol production callsite (T1605)
332
+ * - `satisfies` — cross-task AC binding (T10506 / ADR-079-r2;
333
+ * validator semantics ship in T10507)
221
334
  *
222
335
  * @task T10337
223
336
  * @adr ADR-051
@@ -233,6 +346,7 @@ export const EvidenceAtomSchema = z.discriminatedUnion('kind', [
233
346
  prAtomSchema,
234
347
  locDropAtomSchema,
235
348
  callsiteCoverageAtomSchema,
349
+ satisfiesAtomSchema,
236
350
  ]);
237
351
 
238
352
  /**
@@ -332,14 +446,25 @@ export const GATE_EVIDENCE_REQUIREMENTS: Readonly<
332
446
  * Result of {@link validateEvidenceForGate}.
333
447
  *
334
448
  * Success carries no extra payload (the caller already holds the atom list).
335
- * Failure carries a human-readable message in the legacy
336
- * `Gate '<gate>' requires evidence: [<combo>] OR [<combo>]` format so error
337
- * surfaces (E_EVIDENCE_INSUFFICIENT) read identically across the CLI and any
338
- * client-side validator built on top of this schema.
449
+ * Failure carries:
450
+ * - {@link EvidenceValidationFailure.message} — the legacy single-line
451
+ * `Gate '<gate>' requires evidence: [<combo>] OR [<combo>]` format,
452
+ * preserved byte-for-byte for backward-compat with surfaces and tests
453
+ * that match against it.
454
+ * - {@link EvidenceValidationFailure.hint} — a richer, multi-line,
455
+ * example-bearing remediation hint suitable for CLI `fix:` surfaces.
456
+ * Added by T9949 so `E_EVIDENCE_INSUFFICIENT` errors point clearly at
457
+ * the alternative atom the caller needs (e.g. "this gate requires
458
+ * real proof — note: alone is not accepted; use commit+files,
459
+ * commit+note, or pr instead").
339
460
  */
340
- export type EvidenceValidationResult =
341
- | { readonly ok: true }
342
- | { readonly ok: false; readonly message: string };
461
+ export interface EvidenceValidationFailure {
462
+ readonly ok: false;
463
+ readonly message: string;
464
+ readonly hint: string;
465
+ }
466
+
467
+ export type EvidenceValidationResult = { readonly ok: true } | EvidenceValidationFailure;
343
468
 
344
469
  /**
345
470
  * Format the satisfying-combinations spec for a gate as the legacy
@@ -354,6 +479,115 @@ export function formatGateRequirement(gate: VerificationGate): string {
354
479
  return requirement.oneOf.map((set) => `[${set.join(' AND ')}]`).join(' OR ');
355
480
  }
356
481
 
482
+ /**
483
+ * Example `--evidence` strings for each atom kind. Used by
484
+ * {@link formatGateRequirementHint} to render copy-pasteable remediation
485
+ * commands in CLI error surfaces.
486
+ *
487
+ * @task T9949
488
+ */
489
+ const ATOM_EXAMPLES: Readonly<Record<EvidenceAtomKind, string>> = Object.freeze({
490
+ commit: 'commit:<sha>',
491
+ files: 'files:path/a.ts,path/b.ts',
492
+ 'test-run': 'test-run:/tmp/vitest-out.json',
493
+ tool: 'tool:test',
494
+ url: 'url:https://example.com/docs',
495
+ note: 'note:<short description>',
496
+ decision: 'decision:D-arch-001',
497
+ pr: 'pr:357',
498
+ 'loc-drop': 'loc-drop:<fromLines>:<toLines>',
499
+ 'callsite-coverage': 'callsite-coverage:<symbolName>:<relativeSourcePath>',
500
+ satisfies: 'satisfies:T1234#AC2',
501
+ });
502
+
503
+ /**
504
+ * Render a satisfying atom combination as a copy-pasteable `--evidence`
505
+ * string fragment, e.g. `['commit', 'files']` → `commit:<sha>;files:path/a.ts,path/b.ts`.
506
+ *
507
+ * @internal
508
+ */
509
+ function renderCombinationAsEvidence(combination: ReadonlyArray<EvidenceAtomKind>): string {
510
+ return combination.map((kind) => ATOM_EXAMPLES[kind]).join(';');
511
+ }
512
+
513
+ /**
514
+ * Produce a multi-line, example-bearing remediation hint for a gate.
515
+ *
516
+ * Designed for CLI `fix:` surfaces so `E_EVIDENCE_INSUFFICIENT` errors point
517
+ * the caller at the specific alternative atom shape they need. The hint lists
518
+ * every satisfying combination as a copy-pasteable `cleo verify` invocation
519
+ * plus a note-specific clarification when the gate does NOT accept `note:`
520
+ * alone.
521
+ *
522
+ * The motivating bug (T9949) was that note-only deliverables (decision-only
523
+ * tasks, deletion-only refactors) hit `E_EVIDENCE_INSUFFICIENT` with the
524
+ * legacy single-line error and had to consult ADR-051 to figure out which
525
+ * additional atom kind the gate required. The richer hint inlines the answer.
526
+ *
527
+ * @param gate - Verification gate the hint is for.
528
+ * @returns Multi-line remediation hint suitable for an `engineError({fix:})`
529
+ * surface. Always ends in a clarifying sentence about whether `note:` alone
530
+ * is accepted for the gate.
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * formatGateRequirementHint('implemented');
535
+ * // "Gate 'implemented' requires programmatic evidence. Use ONE of:
536
+ * // - cleo verify T#### --gate implemented --evidence 'commit:<sha>;files:path/a.ts,path/b.ts'
537
+ * // - cleo verify T#### --gate implemented --evidence 'commit:<sha>;note:<short description>'
538
+ * // - cleo verify T#### --gate implemented --evidence 'decision:D-arch-001;files:path/a.ts,path/b.ts'
539
+ * // - cleo verify T#### --gate implemented --evidence 'decision:D-arch-001;note:<short description>'
540
+ * // - cleo verify T#### --gate implemented --evidence 'pr:357'
541
+ * // Note: 'note:' alone is NOT accepted for this gate — pair it with 'commit:' or 'decision:'."
542
+ * ```
543
+ *
544
+ * @task T9949
545
+ * @adr ADR-051 §2.3
546
+ */
547
+ export function formatGateRequirementHint(gate: VerificationGate): string {
548
+ const requirement = GATE_EVIDENCE_REQUIREMENTS[gate];
549
+ if (!requirement) {
550
+ // Unknown gate — match validateEvidenceForGate's defensive default.
551
+ return `Gate '${gate}' has no documented evidence requirements.`;
552
+ }
553
+
554
+ const lines: string[] = [
555
+ `Gate '${gate}' requires programmatic evidence. Use ONE of:`,
556
+ ...requirement.oneOf.map(
557
+ (combo) =>
558
+ ` - cleo verify T#### --gate ${gate} --evidence '${renderCombinationAsEvidence(combo)}'`,
559
+ ),
560
+ ];
561
+
562
+ // Note-specific clarification — T9949's headline pain point.
563
+ // The gate accepts note: alone IFF some single-element combination is ['note'].
564
+ const noteAloneAccepted = requirement.oneOf.some(
565
+ (combo) => combo.length === 1 && combo[0] === 'note',
566
+ );
567
+ if (noteAloneAccepted) {
568
+ lines.push(`Note: 'note:' alone IS accepted for this gate (waiver-style evidence).`);
569
+ } else {
570
+ // Surface every alternative pairing involving note for at-a-glance routing.
571
+ const notePairings = requirement.oneOf.filter(
572
+ (combo) => combo.length > 1 && combo.includes('note'),
573
+ );
574
+ if (notePairings.length > 0) {
575
+ const partners = Array.from(
576
+ new Set(notePairings.flatMap((combo) => combo.filter((k) => k !== 'note'))),
577
+ ).map((k) => `'${k}:'`);
578
+ lines.push(
579
+ `Note: 'note:' alone is NOT accepted for this gate — pair it with ${partners.join(' or ')}.`,
580
+ );
581
+ } else {
582
+ lines.push(
583
+ `Note: 'note:' is NOT accepted for this gate (even when paired). See ADR-051 for the full grammar.`,
584
+ );
585
+ }
586
+ }
587
+
588
+ return lines.join('\n');
589
+ }
590
+
357
591
  /**
358
592
  * Check whether a set of parsed atoms satisfies the requirement spec for
359
593
  * `gate`.
@@ -401,6 +635,11 @@ export function validateEvidenceForGate(
401
635
  return {
402
636
  ok: false,
403
637
  message: `Gate '${gate}' requires evidence: ${formatGateRequirement(gate)}`,
638
+ // T9949: rich, example-bearing remediation hint suitable for CLI `fix:`.
639
+ // Always populated alongside `message` so consumers can choose between
640
+ // the legacy single-line surface and the multi-line hint without re-
641
+ // computing the spec.
642
+ hint: formatGateRequirementHint(gate),
404
643
  };
405
644
  }
406
645
 
@@ -594,6 +833,78 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
594
833
  atoms.push({ kind: 'callsite-coverage', symbolName, relativeSourcePath });
595
834
  break;
596
835
  }
836
+ case 'satisfies': {
837
+ // ADR-079-r2 §2.1 ABNF:
838
+ // satisfies-atom = "satisfies:" task-id "#" ac-id [ "@" version-pin ]
839
+ // Payload shape: <task-id>#<ac-id>[@<version-pin>]
840
+ //
841
+ // PARSING ONLY per T10506 — runtime validator semantics (5-check
842
+ // accept pipeline + same-saga scope rule) ship in T10507.
843
+ const hashIdx = payload.indexOf('#');
844
+ if (hashIdx < 1 || hashIdx === payload.length - 1) {
845
+ throw new EvidenceParseError(
846
+ `Malformed satisfies atom: "${chunk}" (expected satisfies:<task-id>#<ac-id>[@<version-pin>])`,
847
+ 'Use format: satisfies:T1234#AC2 or satisfies:T1234#<uuid> e.g. satisfies:T10506#AC1',
848
+ );
849
+ }
850
+ const targetTaskId = payload.slice(0, hashIdx).trim();
851
+ const acAndPin = payload.slice(hashIdx + 1).trim();
852
+
853
+ // task-id must match strict ABNF /^T[0-9]{1,7}$/
854
+ if (!SATISFIES_TASK_ID_REGEX.test(targetTaskId)) {
855
+ throw new EvidenceParseError(
856
+ `satisfies atom targetTaskId "${targetTaskId}" must match /^T[0-9]{1,7}$/ in "${chunk}"`,
857
+ 'Use format: satisfies:T<digits>#<ac-id> e.g. satisfies:T1234#AC2',
858
+ );
859
+ }
860
+
861
+ // Split optional version-pin (after `@`)
862
+ const atIdx = acAndPin.indexOf('@');
863
+ let acIdRaw: string;
864
+ let versionPin: string | undefined;
865
+ if (atIdx === -1) {
866
+ acIdRaw = acAndPin;
867
+ versionPin = undefined;
868
+ } else {
869
+ if (atIdx < 1 || atIdx === acAndPin.length - 1) {
870
+ throw new EvidenceParseError(
871
+ `Malformed satisfies atom version-pin in "${chunk}" (expected <ac-id>@<14-digit YYYYMMDDhhmmss>)`,
872
+ 'Use format: satisfies:T1234#AC2@20260524223045',
873
+ );
874
+ }
875
+ acIdRaw = acAndPin.slice(0, atIdx).trim();
876
+ versionPin = acAndPin.slice(atIdx + 1).trim();
877
+ if (!SATISFIES_VERSION_PIN_REGEX.test(versionPin)) {
878
+ throw new EvidenceParseError(
879
+ `satisfies atom versionPin "${versionPin}" must be 14 digits (YYYYMMDDhhmmss) in "${chunk}"`,
880
+ 'Use format: satisfies:T1234#AC2@<YYYYMMDDhhmmss> e.g. satisfies:T1234#AC2@20260524223045',
881
+ );
882
+ }
883
+ }
884
+
885
+ // ac-id is EITHER a strict UUIDv4 OR an AC<digits> alias — exactly one.
886
+ let targetAcId: string | undefined;
887
+ let targetAcAlias: string | undefined;
888
+ if (AC_UUID_REGEX.test(acIdRaw)) {
889
+ targetAcId = acIdRaw;
890
+ } else if (AC_ALIAS_REGEX.test(acIdRaw)) {
891
+ targetAcAlias = acIdRaw;
892
+ } else {
893
+ throw new EvidenceParseError(
894
+ `satisfies atom ac-id "${acIdRaw}" must be either a lowercase UUIDv4 or AC<1-4 digits> in "${chunk}"`,
895
+ 'Use format: satisfies:T1234#AC2 (alias) or satisfies:T1234#<lowercase-uuidv4> (canonical)',
896
+ );
897
+ }
898
+
899
+ atoms.push({
900
+ kind: 'satisfies',
901
+ targetTaskId,
902
+ ...(targetAcId !== undefined ? { targetAcId } : {}),
903
+ ...(targetAcAlias !== undefined ? { targetAcAlias } : {}),
904
+ ...(versionPin !== undefined ? { versionPin } : {}),
905
+ });
906
+ break;
907
+ }
597
908
  case 'state': {
598
909
  // T9838: explicit-form modifier for the preceding pr: atom.
599
910
  // Format: pr:<num>;state:MERGED. The resolver always requires
@@ -619,7 +930,7 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
619
930
  default:
620
931
  throw new EvidenceParseError(
621
932
  `Unknown evidence kind: "${kind}" in atom "${chunk}"`,
622
- 'Valid kinds: commit, files, test-run, tool, url, note, loc-drop, callsite-coverage, decision, pr, state',
933
+ 'Valid kinds: commit, files, test-run, tool, url, note, loc-drop, callsite-coverage, decision, pr, satisfies, state',
623
934
  );
624
935
  }
625
936
  }
package/src/exit-codes.ts CHANGED
@@ -224,6 +224,29 @@ export enum ExitCode {
224
224
  * @task T9230
225
225
  */
226
226
  LEAD_BYPASS_DETECTED = 107,
227
+
228
+ /**
229
+ * `cleo complete <tid>` was rejected because one or more acceptance criteria
230
+ * on the task have no `evidence_ac_bindings` rows pointing at them. The
231
+ * coverage gate is the load-bearing IVTR closure piece — every AC the task
232
+ * declared MUST be backed by a `direct`, `satisfies`, or `coverage` binding
233
+ * before completion is allowed.
234
+ *
235
+ * `error.details.unsatisfied` carries the list of offending ACs (each row
236
+ * has `acId`, `alias` (`AC<n>`), and `text`). Recovery options:
237
+ * 1. Add programmatic evidence that resolves the AC via `cleo verify` so
238
+ * a binding row is written.
239
+ * 2. Pass `--waive-ac "<csv>" --waive-reason "<text>"` on `cleo complete`
240
+ * to record an audited waiver (`.cleo/audit/ac-waiver.jsonl`).
241
+ * 3. Set `CLEO_OWNER_OVERRIDE=1` + `CLEO_OWNER_OVERRIDE_REASON=<text>` for
242
+ * a full bypass (logged to `.cleo/audit/force-bypass.jsonl`).
243
+ *
244
+ * @codeName E_AC_COVERAGE_INCOMPLETE
245
+ * @task T10509
246
+ * @saga T10377 (SG-IVTR-AC-BINDING)
247
+ * @adr ADR-079-r4
248
+ */
249
+ AC_COVERAGE_INCOMPLETE = 108,
227
250
  }
228
251
 
229
252
  // ---------------------------------------------------------------------------
@@ -362,6 +385,25 @@ export const E_INVALID_SEVERITY_VALUE = 'E_INVALID_SEVERITY_VALUE' as const;
362
385
  */
363
386
  export const E_INVALID_PIPELINE_STAGE = 'E_INVALID_PIPELINE_STAGE' as const;
364
387
 
388
+ /**
389
+ * E_AC_COVERAGE_INCOMPLETE — `cleo complete <tid>` was rejected because at
390
+ * least one acceptance criterion on the task has no `evidence_ac_bindings`
391
+ * row. The coverage gate is the load-bearing IVTR closure piece — every AC
392
+ * the task declared MUST be backed by a binding (`direct`, `satisfies`, or
393
+ * `coverage`) before completion is allowed.
394
+ *
395
+ * `error.details.unsatisfied` carries the offending ACs (each entry has
396
+ * `acId`, `alias`, and `text`). Recovery options live on the {@link ExitCode.AC_COVERAGE_INCOMPLETE}
397
+ * JSDoc.
398
+ *
399
+ * Maps to {@link ExitCode.AC_COVERAGE_INCOMPLETE} (108) at the CLI boundary.
400
+ *
401
+ * @task T10509
402
+ * @saga T10377 (SG-IVTR-AC-BINDING)
403
+ * @adr ADR-079-r4
404
+ */
405
+ export const E_AC_COVERAGE_INCOMPLETE = 'E_AC_COVERAGE_INCOMPLETE' as const;
406
+
365
407
  /** Check if an exit code represents an error (1-99). */
366
408
  export function isErrorCode(code: ExitCode): boolean {
367
409
  return code >= 1 && code < 100;
package/src/index.ts CHANGED
@@ -257,6 +257,8 @@ export type {
257
257
  DepsRequiredAt,
258
258
  EnforcementProfile,
259
259
  HierarchyConfig,
260
+ LeadRollupConfig,
261
+ LeadRollupMode,
260
262
  LifecycleConfig,
261
263
  LifecycleEnforcementMode,
262
264
  LlmConfig,
@@ -288,6 +290,8 @@ export type {
288
290
  export { parseClaudeCodeCredentials } from './credentials.js';
289
291
  // === DataAccessor Interface ===
290
292
  export type {
293
+ AcBindingRow,
294
+ AcRow,
291
295
  ArchiveFields,
292
296
  ArchiveFile,
293
297
  DataAccessor,
@@ -475,10 +479,13 @@ export {
475
479
  export type {
476
480
  EvidenceAtom as EvidenceAtomInput,
477
481
  EvidenceAtomKind,
482
+ EvidenceValidationFailure,
478
483
  EvidenceValidationResult,
479
484
  GateEvidenceRequirement,
480
485
  } from './evidence-atom-schema.js';
481
486
  export {
487
+ AC_ALIAS_REGEX,
488
+ AC_UUID_REGEX,
482
489
  callsiteCoverageAtomSchema,
483
490
  commitAtomSchema,
484
491
  decisionAtomSchema,
@@ -486,11 +493,15 @@ export {
486
493
  EvidenceParseError,
487
494
  filesAtomSchema,
488
495
  formatGateRequirement,
496
+ formatGateRequirementHint,
489
497
  GATE_EVIDENCE_REQUIREMENTS,
490
498
  locDropAtomSchema,
491
499
  noteAtomSchema,
492
500
  parseEvidenceString,
493
501
  prAtomSchema,
502
+ SATISFIES_TASK_ID_REGEX,
503
+ SATISFIES_VERSION_PIN_REGEX,
504
+ satisfiesAtomSchema,
494
505
  testRunAtomSchema,
495
506
  toolAtomSchema,
496
507
  urlAtomSchema,
@@ -525,6 +536,8 @@ export {
525
536
  } from './evidence-record-schema.js';
526
537
  // === Exit Codes ===
527
538
  export {
539
+ // T10509 / Saga T10377 — AC-coverage gate at cleo complete (IVTR closure)
540
+ E_AC_COVERAGE_INCOMPLETE,
528
541
  // T10105 / Saga T10099 — fail-loud changeset parse
529
542
  E_CHANGESET_YAML_INVALID,
530
543
  // SPEC-T9345 release pipeline v2 error code names (T9525)
@@ -1273,6 +1286,7 @@ export type {
1273
1286
  DepGraphIssue,
1274
1287
  DepsTreeEdge,
1275
1288
  DepsTreeNode,
1289
+ TaskShowAcRowEntry,
1276
1290
  TaskShowAttachmentEntry,
1277
1291
  TasksAddBatchEntry,
1278
1292
  TasksAddBatchParams,
@@ -1992,6 +2006,31 @@ export type {
1992
2006
  Transport,
1993
2007
  TransportConnectConfig,
1994
2008
  } from './transport.js';
2009
+ // === Validator Role Contracts (T10510 / Saga T10377 SG-IVTR-AC-BINDING) ===
2010
+ // Canonical AgentRole enum + per-AC ValidatorFinding + ValidatorAttestation /
2011
+ // ValidatorRejection envelopes + ValidatorVerdict discriminated union. Consumed
2012
+ // by SDK tools (T10511) and the Max-N runtime (T10512) under Epic T10383
2013
+ // E-VALIDATOR-ROLE.
2014
+ export type {
2015
+ AgentRole,
2016
+ ValidatorAttestation,
2017
+ ValidatorFinding,
2018
+ ValidatorFindingStatus,
2019
+ ValidatorRejection,
2020
+ ValidatorVerdict,
2021
+ } from './validator/index.js';
2022
+ export {
2023
+ AGENT_ROLES,
2024
+ isAgentRole,
2025
+ isValidatorAttestation,
2026
+ isValidatorRejection,
2027
+ isValidatorVerdict,
2028
+ VALIDATOR_ID_REGEX,
2029
+ validatorAttestationSchema,
2030
+ validatorFindingSchema,
2031
+ validatorRejectionSchema,
2032
+ validatorVerdictSchema,
2033
+ } from './validator/index.js';
1995
2034
  // === WarpChain Types ===
1996
2035
  export type {
1997
2036
  ChainShape,
@@ -138,6 +138,32 @@ export interface TasksFindParams {
138
138
  * @task T9905
139
139
  */
140
140
  urgent?: boolean;
141
+ /**
142
+ * Filter by label — selects tasks whose `labels` array contains this
143
+ * value. Composes with other filters via AND.
144
+ *
145
+ * Closes GH#393 — gives `cleo find --label <name>` parity with the
146
+ * positional `cleo labels <name>` surface.
147
+ *
148
+ * @task T9904
149
+ */
150
+ label?: string;
151
+ /**
152
+ * Filter by parent task ID — restricts the result set to tasks whose
153
+ * `parentId` equals this value. Mirrors the `--parent` axis on
154
+ * `cleo list`. When the parent task is a Saga (Epic with
155
+ * `label='saga'`), routing goes through `task_relations.type='groups'`
156
+ * member IDs instead of the `parentId` column (ADR-073 §1).
157
+ *
158
+ * Composes with other filters via AND. Closes T10108 — pre-fix,
159
+ * `cleo find "" --parent <id>` returned every task in the project
160
+ * because the empty-string query bypassed all filters via
161
+ * `fuzzyScore('', '<title>')===80`.
162
+ *
163
+ * @task T10108
164
+ * @saga T9862
165
+ */
166
+ parent?: string;
141
167
  }
142
168
  export type TasksFindResult = MinimalTask[];
143
169
 
@@ -211,6 +237,38 @@ export interface TasksShowResult {
211
237
  * @task T9966
212
238
  */
213
239
  attachments: TaskShowAttachmentEntry[];
240
+ /**
241
+ * Acceptance-criterion rows hydrated from the `task_acceptance_criteria`
242
+ * table (T10502). Each entry carries the stable UUID `id`, the
243
+ * `AC<ordinal>` alias, the ordinal itself, and the canonical AC text.
244
+ *
245
+ * Optional — undefined when the task has no rows in the table (e.g.
246
+ * legacy tasks not yet backfilled by T10505). Consumers should fall
247
+ * back to `task.acceptance` (the legacy JSON string) in that case.
248
+ *
249
+ * @task T10508
250
+ * @epic T10381
251
+ */
252
+ acRows?: TaskShowAcRowEntry[];
253
+ }
254
+
255
+ /**
256
+ * One acceptance-criterion entry in {@link TasksShowResult.acRows}.
257
+ *
258
+ * Mirrors the `AcDetail` shape from `@cleocode/core/tasks/show.js` but
259
+ * lives in contracts so dispatch-layer consumers don't depend on core.
260
+ *
261
+ * @task T10508
262
+ */
263
+ export interface TaskShowAcRowEntry {
264
+ /** UUIDv4 stable identifier, immutable for the AC's lifetime. */
265
+ id: string;
266
+ /** Display alias derived from ordinal — `AC1`, `AC2`, etc. */
267
+ alias: string;
268
+ /** 1-based ordinal — never reused per task (gaps remain on shrink). */
269
+ ordinal: number;
270
+ /** The AC statement text. Structured gates round-trip as JSON. */
271
+ text: string;
214
272
  }
215
273
 
216
274
  // tasks.tree dispatch params (with optional withBlockers flag — dispatch alias)
@@ -900,6 +958,24 @@ export interface TasksCompleteQueryParams {
900
958
  overrideReason?: string;
901
959
  /** Reason for acknowledging CRITICAL nexus impact risk (bypasses nexusImpact gate). */
902
960
  acknowledgeRisk?: string;
961
+ /**
962
+ * Comma-separated AC tokens (UUIDs or `AC<n>` aliases) waived from the
963
+ * AC-coverage gate (T10509). MUST be paired with {@link waiveReason} —
964
+ * the gate rejects the call when one is set without the other.
965
+ *
966
+ * Waivers are recorded to `.cleo/audit/ac-waiver.jsonl` per ADR-079-r4 §4.
967
+ *
968
+ * @task T10509
969
+ * @saga T10377 (SG-IVTR-AC-BINDING)
970
+ */
971
+ waiveAc?: string;
972
+ /**
973
+ * Justification text for the {@link waiveAc} waiver. Mandatory whenever
974
+ * `waiveAc` is non-empty.
975
+ *
976
+ * @task T10509
977
+ */
978
+ waiveReason?: string;
903
979
  }
904
980
  /**
905
981
  * Result of `tasks.complete` — completion confirmation with unblocked tasks.