@cleocode/contracts 2026.9.20 → 2026.9.21

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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/decide.test.d.ts +7 -0
  3. package/dist/__tests__/decide.test.d.ts.map +1 -0
  4. package/dist/__tests__/decide.test.js +91 -0
  5. package/dist/__tests__/decide.test.js.map +1 -0
  6. package/dist/__tests__/evidence-atom-schema.test.js +2 -1
  7. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -1
  8. package/dist/__tests__/memory-wire-shapes.test.js.map +1 -1
  9. package/dist/acceptance-gate-schema.d.ts +5 -0
  10. package/dist/acceptance-gate-schema.d.ts.map +1 -1
  11. package/dist/acceptance-gate-schema.js +26 -0
  12. package/dist/acceptance-gate-schema.js.map +1 -1
  13. package/dist/acceptance-gate.d.ts +70 -3
  14. package/dist/acceptance-gate.d.ts.map +1 -1
  15. package/dist/backup-manifest.d.ts +13 -2
  16. package/dist/backup-manifest.d.ts.map +1 -1
  17. package/dist/branch-lock.d.ts +11 -0
  18. package/dist/branch-lock.d.ts.map +1 -1
  19. package/dist/branch-lock.js +31 -4
  20. package/dist/branch-lock.js.map +1 -1
  21. package/dist/cloud/api.d.ts +607 -0
  22. package/dist/cloud/api.d.ts.map +1 -0
  23. package/dist/cloud/api.js +417 -0
  24. package/dist/cloud/api.js.map +1 -0
  25. package/dist/cloud/envelope.d.ts +84 -0
  26. package/dist/cloud/envelope.d.ts.map +1 -0
  27. package/dist/cloud/envelope.js +54 -0
  28. package/dist/cloud/envelope.js.map +1 -0
  29. package/dist/cloud/ids.d.ts +30 -0
  30. package/dist/cloud/ids.d.ts.map +1 -0
  31. package/dist/cloud/ids.js +32 -0
  32. package/dist/cloud/ids.js.map +1 -0
  33. package/dist/cloud/index.d.ts +17 -0
  34. package/dist/cloud/index.d.ts.map +1 -0
  35. package/dist/cloud/index.js +17 -0
  36. package/dist/cloud/index.js.map +1 -0
  37. package/dist/data-accessor.d.ts +23 -3
  38. package/dist/data-accessor.d.ts.map +1 -1
  39. package/dist/data-accessor.js.map +1 -1
  40. package/dist/decide.d.ts +333 -0
  41. package/dist/decide.d.ts.map +1 -0
  42. package/dist/decide.js +184 -0
  43. package/dist/decide.js.map +1 -0
  44. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  45. package/dist/dispatch/operations-registry.js +55 -0
  46. package/dist/dispatch/operations-registry.js.map +1 -1
  47. package/dist/done-plan.d.ts +311 -0
  48. package/dist/done-plan.d.ts.map +1 -0
  49. package/dist/done-plan.js +17 -0
  50. package/dist/done-plan.js.map +1 -0
  51. package/dist/evidence-atom-schema.d.ts +18 -1
  52. package/dist/evidence-atom-schema.d.ts.map +1 -1
  53. package/dist/evidence-atom-schema.js +27 -2
  54. package/dist/evidence-atom-schema.js.map +1 -1
  55. package/dist/exit-codes.d.ts +40 -0
  56. package/dist/exit-codes.d.ts.map +1 -1
  57. package/dist/exit-codes.js +41 -0
  58. package/dist/exit-codes.js.map +1 -1
  59. package/dist/graph.d.ts +40 -0
  60. package/dist/graph.d.ts.map +1 -1
  61. package/dist/index.d.ts +13 -6
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/memory/observe.d.ts +16 -0
  66. package/dist/memory/observe.d.ts.map +1 -1
  67. package/dist/operations/__tests__/release-shared-surface.test.d.ts +10 -0
  68. package/dist/operations/__tests__/release-shared-surface.test.d.ts.map +1 -0
  69. package/dist/operations/__tests__/release-shared-surface.test.js +84 -0
  70. package/dist/operations/__tests__/release-shared-surface.test.js.map +1 -0
  71. package/dist/operations/dialectic.d.ts +16 -0
  72. package/dist/operations/dialectic.d.ts.map +1 -1
  73. package/dist/operations/memory.d.ts +48 -0
  74. package/dist/operations/memory.d.ts.map +1 -1
  75. package/dist/operations/nexus-scope-map.d.ts +8 -0
  76. package/dist/operations/nexus-scope-map.d.ts.map +1 -1
  77. package/dist/operations/nexus-scope-map.js +8 -0
  78. package/dist/operations/nexus-scope-map.js.map +1 -1
  79. package/dist/operations/nexus-user-profile.d.ts +27 -1
  80. package/dist/operations/nexus-user-profile.d.ts.map +1 -1
  81. package/dist/operations/nexus-user-profile.js +11 -1
  82. package/dist/operations/nexus-user-profile.js.map +1 -1
  83. package/dist/operations/nexus.d.ts +394 -3
  84. package/dist/operations/nexus.d.ts.map +1 -1
  85. package/dist/operations/release.d.ts +259 -0
  86. package/dist/operations/release.d.ts.map +1 -1
  87. package/dist/operations/release.js +130 -1
  88. package/dist/operations/release.js.map +1 -1
  89. package/dist/operations/session.d.ts +6 -4
  90. package/dist/operations/session.d.ts.map +1 -1
  91. package/dist/operations/tasks.d.ts +6 -0
  92. package/dist/operations/tasks.d.ts.map +1 -1
  93. package/dist/operations/tasks.js +1 -0
  94. package/dist/operations/tasks.js.map +1 -1
  95. package/dist/operations/validate.d.ts +5 -0
  96. package/dist/operations/validate.d.ts.map +1 -1
  97. package/dist/operations/worktree.d.ts +125 -1
  98. package/dist/operations/worktree.d.ts.map +1 -1
  99. package/dist/process-probe.d.ts +17 -0
  100. package/dist/process-probe.d.ts.map +1 -0
  101. package/dist/process-probe.js +21 -0
  102. package/dist/process-probe.js.map +1 -0
  103. package/dist/project-context.d.ts +44 -2
  104. package/dist/project-context.d.ts.map +1 -1
  105. package/dist/project-lifecycle.d.ts +143 -0
  106. package/dist/project-lifecycle.d.ts.map +1 -0
  107. package/dist/project-lifecycle.js +21 -0
  108. package/dist/project-lifecycle.js.map +1 -0
  109. package/dist/release/plan.d.ts +6 -2
  110. package/dist/release/plan.d.ts.map +1 -1
  111. package/dist/release/plan.js +8 -2
  112. package/dist/release/plan.js.map +1 -1
  113. package/dist/release/plan.test.js +6 -1
  114. package/dist/release/plan.test.js.map +1 -1
  115. package/dist/table-classification.d.ts +208 -0
  116. package/dist/table-classification.d.ts.map +1 -0
  117. package/dist/table-classification.js +23 -0
  118. package/dist/table-classification.js.map +1 -0
  119. package/dist/task.d.ts +79 -0
  120. package/dist/task.d.ts.map +1 -1
  121. package/package.json +6 -2
  122. package/schemas/gate-result.schema.json +43 -0
  123. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  124. package/src/__tests__/decide.test.ts +139 -0
  125. package/src/__tests__/evidence-atom-schema.test.ts +2 -1
  126. package/src/__tests__/memory-wire-shapes.test.ts +3 -0
  127. package/src/acceptance-gate-schema.ts +29 -0
  128. package/src/acceptance-gate.ts +68 -3
  129. package/src/backup-manifest.ts +13 -2
  130. package/src/branch-lock.ts +36 -4
  131. package/src/cloud/api.ts +512 -0
  132. package/src/cloud/envelope.ts +63 -0
  133. package/src/cloud/ids.ts +51 -0
  134. package/src/cloud/index.ts +16 -0
  135. package/src/data-accessor.ts +24 -3
  136. package/src/decide.ts +355 -0
  137. package/src/dispatch/operations-registry.ts +58 -0
  138. package/src/done-plan.ts +350 -0
  139. package/src/evidence-atom-schema.ts +31 -2
  140. package/src/exit-codes.ts +42 -0
  141. package/src/graph.ts +44 -0
  142. package/src/index.ts +117 -0
  143. package/src/memory/observe.ts +16 -0
  144. package/src/operations/__tests__/release-shared-surface.test.ts +112 -0
  145. package/src/operations/dialectic.ts +17 -0
  146. package/src/operations/memory.ts +48 -0
  147. package/src/operations/nexus-scope-map.ts +9 -0
  148. package/src/operations/nexus-user-profile.ts +29 -1
  149. package/src/operations/nexus.ts +416 -3
  150. package/src/operations/release.ts +197 -0
  151. package/src/operations/session.ts +6 -4
  152. package/src/operations/tasks.ts +7 -0
  153. package/src/operations/validate.ts +5 -0
  154. package/src/operations/worktree.ts +133 -1
  155. package/src/process-probe.ts +21 -0
  156. package/src/project-context.ts +42 -2
  157. package/src/project-lifecycle.ts +149 -0
  158. package/src/release/plan.test.ts +6 -1
  159. package/src/release/plan.ts +8 -2
  160. package/src/table-classification.ts +232 -0
  161. package/src/task.ts +84 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Tests for the typed-decision contracts (T12489).
3
+ *
4
+ * @task T12489
5
+ */
6
+
7
+ import { describe, expect, expectTypeOf, it } from 'vitest';
8
+ import type { z } from 'zod';
9
+ import {
10
+ DECISION_REQUEST_LIMITS,
11
+ type DecisionAnswer,
12
+ type DecisionOutcome,
13
+ type DecisionQuestion,
14
+ decisionAnswerSchema,
15
+ decisionOutcomeSchema,
16
+ decisionProviderConfigSchema,
17
+ decisionQuestionSchema,
18
+ decisionRequestSchema,
19
+ } from '../decide.js';
20
+
21
+ const noul = { type: 'noul', criteria: 'The change is safe to merge' } as const;
22
+
23
+ describe('decisionRequestSchema', () => {
24
+ it('accepts a request with each question shape and structured state', () => {
25
+ const result = decisionRequestSchema.safeParse({
26
+ model: 'default',
27
+ state: { diff: ['a', 'b'], files: 2 },
28
+ questions: {
29
+ safe: { ...noul, instructions: { strict: true } },
30
+ area: { type: 'choice', criteria: { api: 'Touches the API', ui: 'Touches the UI' } },
31
+ risk: { type: 'score', criteria: ['low', 'medium', 'high'] },
32
+ },
33
+ });
34
+ expect(result.success).toBe(true);
35
+ });
36
+
37
+ it('enforces 1-32 questions', () => {
38
+ expect(decisionRequestSchema.safeParse({ state: 's', questions: {} }).success).toBe(false);
39
+ const questions = (n: number) =>
40
+ Object.fromEntries(Array.from({ length: n }, (_, i) => [`q${i}`, noul]));
41
+ expect(
42
+ decisionRequestSchema.safeParse({
43
+ state: 's',
44
+ questions: questions(DECISION_REQUEST_LIMITS.maxQuestions),
45
+ }).success,
46
+ ).toBe(true);
47
+ expect(
48
+ decisionRequestSchema.safeParse({
49
+ state: 's',
50
+ questions: questions(DECISION_REQUEST_LIMITS.maxQuestions + 1),
51
+ }).success,
52
+ ).toBe(false);
53
+ });
54
+
55
+ it('enforces the state size limit for string and structured state', () => {
56
+ const max = DECISION_REQUEST_LIMITS.maxStateChars;
57
+ const q = { q: noul };
58
+ expect(decisionRequestSchema.safeParse({ state: 'x'.repeat(max), questions: q }).success).toBe(
59
+ true,
60
+ );
61
+ expect(
62
+ decisionRequestSchema.safeParse({ state: 'x'.repeat(max + 1), questions: q }).success,
63
+ ).toBe(false);
64
+ expect(
65
+ decisionRequestSchema.safeParse({ state: ['x'.repeat(max)], questions: q }).success,
66
+ ).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe('decisionQuestionSchema', () => {
71
+ it('requires at least two choice options', () => {
72
+ expect(decisionQuestionSchema.safeParse({ type: 'choice', criteria: { a: 'A' } }).success).toBe(
73
+ false,
74
+ );
75
+ expect(
76
+ decisionQuestionSchema.safeParse({ type: 'choice', criteria: { a: 'A', b: 'B' } }).success,
77
+ ).toBe(true);
78
+ });
79
+
80
+ it('requires 2-10 score levels', () => {
81
+ const levels = (n: number) => Array.from({ length: n }, (_, i) => `level ${i}`);
82
+ expect(decisionQuestionSchema.safeParse({ type: 'score', criteria: levels(1) }).success).toBe(
83
+ false,
84
+ );
85
+ expect(decisionQuestionSchema.safeParse({ type: 'score', criteria: levels(2) }).success).toBe(
86
+ true,
87
+ );
88
+ expect(decisionQuestionSchema.safeParse({ type: 'score', criteria: levels(10) }).success).toBe(
89
+ true,
90
+ );
91
+ expect(decisionQuestionSchema.safeParse({ type: 'score', criteria: levels(11) }).success).toBe(
92
+ false,
93
+ );
94
+ });
95
+ });
96
+
97
+ describe('answer, outcome and provider config schemas', () => {
98
+ it('rejects probabilities outside [0, 1]', () => {
99
+ expect(
100
+ decisionAnswerSchema.safeParse({
101
+ type: 'noul',
102
+ value: true,
103
+ probability: 1.2,
104
+ confidence: 0.9,
105
+ }).success,
106
+ ).toBe(false);
107
+ });
108
+
109
+ it('accepts a cached outcome', () => {
110
+ const outcome: DecisionOutcome = {
111
+ answers: {
112
+ safe: { type: 'noul', value: true, probability: 0.8, confidence: 0.7 },
113
+ area: {
114
+ type: 'choice',
115
+ value: 'api',
116
+ probabilities: { api: 0.9, ui: 0.1 },
117
+ confidence: 0.6,
118
+ },
119
+ risk: { type: 'score', value: 0, probabilities: [0.7, 0.2, 0.1], confidence: 0.5 },
120
+ },
121
+ source: 'cache',
122
+ latencyMs: 3,
123
+ };
124
+ expect(decisionOutcomeSchema.parse(outcome)).toEqual(outcome);
125
+ });
126
+
127
+ it('requires an absolute baseUrl', () => {
128
+ expect(
129
+ decisionProviderConfigSchema.safeParse({ baseUrl: 'https://example.test' }).success,
130
+ ).toBe(true);
131
+ expect(decisionProviderConfigSchema.safeParse({ baseUrl: 'not a url' }).success).toBe(false);
132
+ });
133
+
134
+ it('schema outputs are assignable to the domain types', () => {
135
+ expectTypeOf<z.output<typeof decisionQuestionSchema>>().toExtend<DecisionQuestion>();
136
+ expectTypeOf<z.output<typeof decisionAnswerSchema>>().toExtend<DecisionAnswer>();
137
+ expectTypeOf<z.output<typeof decisionOutcomeSchema>>().toExtend<DecisionOutcome>();
138
+ });
139
+ });
@@ -31,7 +31,7 @@ describe('EvidenceAtomSchema (T10337)', () => {
31
31
  it('EVIDENCE_ATOM_KINDS covers every discriminated union member (T12030)', () => {
32
32
  // Verify the shared constant is in sync with the zod discriminated union.
33
33
  // The discriminated union keys are 'commit','files','test-run','tool','url',
34
- // 'note','decision','pr','loc-drop','callsite-coverage','satisfies'.
34
+ // 'note','decision','pr','ci','loc-drop','callsite-coverage','satisfies'.
35
35
  const expected = [
36
36
  'commit',
37
37
  'files',
@@ -41,6 +41,7 @@ describe('EvidenceAtomSchema (T10337)', () => {
41
41
  'note',
42
42
  'decision',
43
43
  'pr',
44
+ 'ci',
44
45
  'loc-drop',
45
46
  'callsite-coverage',
46
47
  'satisfies',
@@ -205,6 +205,7 @@ type _ObserveBrainParamsShape = {
205
205
  attachmentRefs?: string[];
206
206
  origin?: string | null;
207
207
  provenanceChain?: string[] | null;
208
+ askTypeDecision?: boolean;
208
209
  _skipGate?: boolean;
209
210
  _skipQueue?: boolean;
210
211
  };
@@ -219,6 +220,8 @@ type _ObserveBrainResultShape = {
219
220
  id: string;
220
221
  type: string;
221
222
  createdAt: string;
223
+ typeSource?: 'caller' | 'keyword' | 'system-one';
224
+ typeConfidence?: number;
222
225
  };
223
226
 
224
227
  type _AssertObserveResultPinned = AssertEquals1<
@@ -443,6 +443,25 @@ const acceptanceGateBindingSchema: z.ZodType<AcceptanceGateBinding> = z
443
443
  });
444
444
  }),
445
445
  ),
446
+ tree: z
447
+ .object({
448
+ headSha: z.string().regex(/^[0-9a-f]{40}$/),
449
+ clean: z.boolean(),
450
+ cwd: z
451
+ .string()
452
+ .min(1)
453
+ .refine(
454
+ (v) => !/^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(v) && !v.split(/[\\/]/).includes('..'),
455
+ 'Tree-relative cwd must be relative and stay inside the tree',
456
+ ),
457
+ inputsHash: z.string().regex(/^[a-f0-9]{64}$/),
458
+ baseSha: z
459
+ .string()
460
+ .regex(/^[0-9a-f]{40}$/)
461
+ .optional(),
462
+ })
463
+ .strict()
464
+ .optional(),
446
465
  })
447
466
  .strict()
448
467
  .superRefine((binding, context) => {
@@ -490,8 +509,18 @@ export const acceptanceGateResultSchema = z
490
509
  /** ISO 8601 timestamp. */
491
510
  checkedAt: z.string().datetime(),
492
511
  checkedBy: z.string().min(1),
512
+ /** T12621: `executed` in this run, or an authenticated pass reused from the cache. */
513
+ source: z.enum(['executed', 'cache']).optional(),
514
+ /** T12621: creation time of the reused cache entry. */
515
+ cachedAt: z.string().datetime().optional(),
493
516
  })
494
517
  .superRefine((result, context) => {
518
+ if ((result.source === 'cache') !== (result.cachedAt !== undefined))
519
+ context.addIssue({
520
+ code: 'custom',
521
+ path: ['cachedAt'],
522
+ message: 'A cached result carries its cache time, and only a cached result does',
523
+ });
495
524
  const binding = result.binding;
496
525
  if (binding) {
497
526
  if (binding.identity.actor !== result.checkedBy)
@@ -28,8 +28,8 @@ import type {
28
28
 
29
29
  /**
30
30
  * Captured options for evaluating acceptance gates without renewing operation authority.
31
- * @remarks An absent execution context receives a two-second shared foreground budget.
32
- * Per-gate timeouts can tighten it; long work requires explicit runtime admission.
31
+ * @remarks An absent execution context is admitted for every gate's tool deadline plus
32
+ * the two-second bookkeeping budget (T12516); per-gate timeouts bound each gate.
33
33
  * @example
34
34
  * ```typescript
35
35
  * const options: AcceptanceGateRunOptions = { projectRoot: '/project', execution };
@@ -49,6 +49,23 @@ export interface AcceptanceGateRunOptions
49
49
  maxOutputBytes?: number;
50
50
  /** Existing manager context, separate from the child environment. */
51
51
  systemdControl?: SystemdControlContext;
52
+ /**
53
+ * Typed-gate result cache policy (T12621). Keyed by gate definition hash, git
54
+ * HEAD, dirty-tree fingerprint and cwd; only passes are stored.
55
+ * - `off`: never read or write the cache.
56
+ * - `refresh`: always execute, then store a pass (`cleo verify --run`).
57
+ * - `use`: serve a fresh cached pass, otherwise execute and store a pass.
58
+ * - `only`: serve a fresh cached pass; a cacheable gate without one is an
59
+ * `error` result and is never executed (`cleo verify --no-run`).
60
+ *
61
+ * @defaultValue 'off'
62
+ */
63
+ cache?: 'off' | 'refresh' | 'use' | 'only';
64
+ /**
65
+ * Digest of the single gate's captured invocation and input artifacts; part of
66
+ * its cache key. Required, with a one-gate batch, whenever `cache` is not `off`.
67
+ */
68
+ cacheInputsHash?: string;
52
69
  }
53
70
 
54
71
  // ─── Base ────────────────────────────────────────────────────────────────────
@@ -80,7 +97,10 @@ export interface GateBase {
80
97
  /**
81
98
  * Gate timeout in milliseconds.
82
99
  *
83
- * @defaultValue 60_000 (further bounded by the shared operation deadline)
100
+ * @defaultValue The ADR-061 tool deadline for the gate kind: `CLEO_GATE_TIMEOUT_MS`,
101
+ * else `CLEO_TOOL_TIMEOUT_<KIND>`, else 1_800_000 for `test` and 300_000 otherwise
102
+ * (T12516). It bounds the gate's own execution; the two-second shared budget
103
+ * covers only the surrounding bookkeeping, never a gate (T12621).
84
104
  */
85
105
  timeoutMs?: number;
86
106
  }
@@ -434,6 +454,37 @@ export interface AcceptanceGateBinding {
434
454
  invocation?: AcceptanceGateInvocation;
435
455
  /** Explicit bounded input inventory, including untracked harness/task files. */
436
456
  artifacts: AcceptanceGateArtifact[];
457
+ /**
458
+ * Content binding of the tree the gate ran in (T12625). Lets a result
459
+ * verified in a worker's worktree be revalidated from another checkout of
460
+ * the same project — the orchestrator completing from main after merge —
461
+ * without re-entering the (often auto-cleaned) worktree. Absent on results
462
+ * recorded before T12625, which keep exact path-bound revalidation.
463
+ */
464
+ tree?: AcceptanceGateTreeBinding;
465
+ }
466
+
467
+ /**
468
+ * Tree-independent binding of a typed gate's inputs (T12625).
469
+ * @remarks Revalidation from another tree of the same project accepts only when
470
+ * the verified tree was clean, `headSha` is an ancestor of the completing HEAD,
471
+ * and `inputsHash` recomputes identically there.
472
+ */
473
+ export interface AcceptanceGateTreeBinding {
474
+ /** HEAD commit of the tree the gate ran in. */
475
+ headSha: string;
476
+ /** True when that tree had no uncommitted tracked changes. */
477
+ clean: boolean;
478
+ /** Gate working directory, relative to the tree root (`.` for the root). */
479
+ cwd: string;
480
+ /** SHA-256 over command, args, relative cwd, environment hash and repo-relative input bytes. */
481
+ inputsHash: string;
482
+ /**
483
+ * Fork point of the verified commit from origin's default branch at
484
+ * verification time. `baseSha..headSha` is the change the result attests;
485
+ * every path it touched must be unchanged in the completing tree.
486
+ */
487
+ baseSha?: string;
437
488
  }
438
489
 
439
490
  /**
@@ -504,6 +555,14 @@ export interface AcceptanceGateResult {
504
555
  checkedAt: string;
505
556
  /** Agent identifier or `"human"` that ran or attested the gate. */
506
557
  checkedBy: string;
558
+ /**
559
+ * Where the verdict came from (T12621): `executed` in this run, or `cache` —
560
+ * an authenticated pass reused from the ADR-061 evidence cache. Absent on
561
+ * results recorded before T12621.
562
+ */
563
+ source?: 'executed' | 'cache';
564
+ /** When the reused cache entry was created (the original execution); set iff `source` is `cache`. */
565
+ cachedAt?: string;
507
566
  }
508
567
 
509
568
  /** Canonical typed-verification audit details stored with results in the task transaction. */
@@ -516,4 +575,10 @@ export interface AcceptanceGateVerificationReceipt {
516
575
  operation: 'check.gate.verify';
517
576
  /** Overall generic-plus-typed verification outcome at this observation. */
518
577
  passed: boolean;
578
+ /**
579
+ * Results reused from the evidence cache instead of executed (T12621), by
580
+ * acceptance index, with each cache entry's creation time. Omitted when every
581
+ * result was executed, so receipts recorded before T12621 stay byte-identical.
582
+ */
583
+ cached?: Array<{ index: number; cachedAt: string }>;
519
584
  }
@@ -254,8 +254,19 @@ export interface BackupObservationInspection {
254
254
  atimeNs: string;
255
255
  /** Observed access time at the final identity/content-metadata recheck. */
256
256
  atimeAfterNs: string;
257
- /** Whether access time changed; this does not identify the responsible reader. */
258
- atimeChanged: boolean;
257
+ /**
258
+ * Whether access time changed; this does not identify the responsible reader.
259
+ * `null` when {@link atimeIsolation} is `'unavailable'`: the inspection's own
260
+ * read may have advanced atime, so a change is not evidence of another reader.
261
+ */
262
+ atimeChanged: boolean | null;
263
+ /**
264
+ * Whether the inspection's own read was kept from updating access time.
265
+ * `'noatime'`: the source was opened with O_NOATIME (Linux).
266
+ * `'unavailable'`: the platform has no O_NOATIME (macOS), so atime may
267
+ * reflect this inspection.
268
+ */
269
+ atimeIsolation: 'noatime' | 'unavailable';
259
270
  };
260
271
  /** Actual SQLite schema marker; not an inferred product/schema version. */
261
272
  userVersion: number;
@@ -320,6 +320,13 @@ export const BRANCH_LOCK_ERROR_CODES = {
320
320
  * reset the branch automatically.
321
321
  */
322
322
  E_DIRTY_BRANCH: 'E_DIRTY_BRANCH',
323
+ /**
324
+ * T12506: the per-task worktree lock is held by a live holder (pid alive with
325
+ * the recorded start time, heartbeat within the TTL). A second spawn would
326
+ * otherwise force-remove the holder's live worktree. Error details name the
327
+ * holder. Exit code `ExitCode.WORKTREE_LOCKED` (25).
328
+ */
329
+ E_WORKTREE_LOCKED: 'E_WORKTREE_LOCKED',
323
330
  } as const;
324
331
 
325
332
  /** Union of all branch-lock error code strings. */
@@ -501,6 +508,10 @@ export interface IsolationResult {
501
508
  * against symlink traversal and shell quirks).
502
509
  * 3. Exports the isolation env vars so they are visible to child processes.
503
510
  *
511
+ * Every path and value is POSIX single-quoted, so the snippet stays a valid
512
+ * shell program when the worktree path contains spaces (the macOS default
513
+ * `~/Library/Application Support/...`), `$`, backticks or quotes (T12520).
514
+ *
504
515
  * The trailing newline is included so callers can concatenate directly.
505
516
  */
506
517
  preamble: string;
@@ -512,6 +523,24 @@ export interface IsolationResult {
512
523
  boundaryContract: BoundaryContract;
513
524
  }
514
525
 
526
+ /**
527
+ * POSIX single-quote one shell word: wrap it in `'...'` and rewrite each
528
+ * embedded `'` as `'\''`. The result is one literal word for any input — no
529
+ * parameter, command or glob expansion — which is what a copy-paste snippet
530
+ * naming a path under `Application Support` needs (T12520).
531
+ *
532
+ * Module-private on purpose: contracts is types-only for its exported surface
533
+ * (gate 10), and {@link provisionIsolatedShell} is the one place that renders
534
+ * isolation paths into shell text, so callers consume the quoted preamble
535
+ * rather than re-quoting.
536
+ *
537
+ * @param value - Raw path or value.
538
+ * @returns The single-quoted shell word.
539
+ */
540
+ function shellQuote(value: string): string {
541
+ return `'${value.replace(/'/g, `'\\''`)}'`;
542
+ }
543
+
515
544
  /**
516
545
  * Provision an isolated shell context for an agent worktree.
517
546
  *
@@ -561,19 +590,22 @@ export function provisionIsolatedShell(options: IsolationOptions): IsolationResu
561
590
  // does not emit a clobbering `export KEY=""` that would clear an inherited
562
591
  // value. Worktree-binding keys are always non-empty so they always export.
563
592
  const exportBlock = ISOLATION_ENV_KEYS.filter((k) => env[k] !== '')
564
- .map((k) => `export ${k}="${env[k]}"`)
593
+ .map((k) => `export ${k}=${shellQuote(env[k])}`)
565
594
  .join('\n');
566
595
 
596
+ // T12520 — every path is emitted through `shellQuote`, never interpolated
597
+ // bare or inside double quotes, so the snippet parses for any worktree path.
598
+ const quotedPath = shellQuote(worktreePath);
567
599
  const preamble = [
568
600
  '## Worktree Isolation (REQUIRED — do not skip)',
569
601
  '',
570
602
  '# Step 1: Enter the worktree (exits immediately if path is missing)',
571
- `cd "${worktreePath}" || exit 1`,
603
+ `cd ${quotedPath} || exit 1`,
572
604
  '',
573
605
  '# Step 2: Verify working directory (guards against shell/symlink quirks)',
574
606
  'case "$PWD" in',
575
- ` "${worktreePath}"*) ;;`,
576
- ' *) echo "ISOLATION ERROR: pwd=$PWD expected prefix=' + worktreePath + '" >&2; exit 1 ;;',
607
+ ` ${quotedPath}*) ;;`,
608
+ ` *) echo "ISOLATION ERROR: pwd=$PWD expected prefix="${quotedPath} >&2; exit 1 ;;`,
577
609
  'esac',
578
610
  '',
579
611
  '# Step 3: Export isolation env vars',