@cleocode/contracts 2026.8.1 → 2026.8.3

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * CAAMP instruction-block marker grammar — the single source of truth.
3
+ *
4
+ * CAAMP delimits the region it owns inside an agent instruction file
5
+ * (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, …) with a pair of HTML comments:
6
+ *
7
+ * ```markdown
8
+ * <!-- CAAMP:START -->
9
+ * @~/.cleo/templates/CLEO-INJECTION.md
10
+ * <!-- CAAMP:END -->
11
+ * ```
12
+ *
13
+ * Before T12051 this grammar was re-declared as inline string and regex
14
+ * literals in at least five places — `caamp/core/instructions/injector.ts`,
15
+ * `caamp/core/harness/pi.ts`, `core/injection.ts`, `core/bootstrap.ts` and
16
+ * `core/validation/doctor/checks.ts`. Each copy recognised a slightly
17
+ * different dialect, so a file that one module considered healthy another
18
+ * considered corrupt.
19
+ *
20
+ * The grammar lives here, in the leaf package, because `@cleocode/core` and
21
+ * `@cleocode/caamp` depend on each other and therefore cannot import the
22
+ * grammar from one another without a cycle.
23
+ *
24
+ * This module is **const data only** — no runtime helpers — so it satisfies
25
+ * the contracts-purity gate (`scripts/lint-no-runtime-in-contracts.mjs`).
26
+ * The engine that consumes these patterns lives in
27
+ * `packages/caamp/src/core/instructions/markers.ts`.
28
+ *
29
+ * @task T12051
30
+ * @see {@link https://github.com/kryptobaseddev/cleo} ADR-064 (CAAMP↔Adapters boundary)
31
+ */
32
+
33
+ /** Canonical opening marker of a CAAMP-managed block. */
34
+ export const CAAMP_MARKER_START = '<!-- CAAMP:START -->';
35
+
36
+ /** Canonical closing marker of a CAAMP-managed block. */
37
+ export const CAAMP_MARKER_END = '<!-- CAAMP:END -->';
38
+
39
+ /**
40
+ * Source of the strict, canonical block pattern.
41
+ *
42
+ * Capture group 1 is the block's inner content. Build a fresh `RegExp` from
43
+ * this string at every use site — a module-level `RegExp` carrying the `g`
44
+ * flag keeps a mutable `lastIndex`, which silently skips matches when the
45
+ * same object is reused across `.test()` / `.exec()` calls.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const pattern = new RegExp(CAAMP_BLOCK_PATTERN_SOURCE, 'g');
50
+ * ```
51
+ */
52
+ export const CAAMP_BLOCK_PATTERN_SOURCE = '<!-- CAAMP:START -->([\\s\\S]*?)<!-- CAAMP:END -->';
53
+
54
+ /**
55
+ * Horizontal-whitespace class used by the damage-tolerant marker patterns.
56
+ *
57
+ * Deliberately excludes `\n` so the patterns stay anchored to a single line
58
+ * and cannot swallow surrounding content.
59
+ */
60
+ const H = '[ \\t\\r]';
61
+
62
+ /**
63
+ * Build the source of a damage-tolerant, whole-line marker pattern.
64
+ *
65
+ * Recognises the canonical marker plus the near-miss forms produced when a
66
+ * delimiter character is lost or mangled — a truncated write, an editor that
67
+ * reflows HTML comments, a shell heredoc, a careless hand edit.
68
+ *
69
+ * Two constraints keep this from over-matching, both learned from an
70
+ * adversarial review that demonstrated content loss with a looser pattern:
71
+ *
72
+ * 1. The `CAAMP:<KEYWORD>` token must be the *entire* line, modulo comment
73
+ * punctuation and horizontal whitespace. Prose such as "the CAAMP:START
74
+ * marker is written by …" is therefore never matched.
75
+ * 2. Comment punctuation must be present on **both** sides — at least one of
76
+ * `< ! -` before the token and at least one of `- >` after it. A bare
77
+ * `CAAMP:START` line is deliberately NOT treated as a damaged marker: it is
78
+ * not a plausible outcome of losing one delimiter character, and accepting
79
+ * it caused a documentation fence that merely *mentioned* CAAMP to be
80
+ * rewritten into a real marker, silently swallowing the fenced body.
81
+ */
82
+ const damagedMarkerSource = (keyword: 'START' | 'END'): string =>
83
+ `^${H}*[<!-]{1,4}${H}*CAAMP${H}*:${H}*${keyword}${H}*[->]{1,3}${H}*$`;
84
+
85
+ /**
86
+ * Source of the damage-tolerant pattern matching an opening marker line.
87
+ *
88
+ * Use with the `gmi` flags. Matches all of:
89
+ *
90
+ * | Form | Damage |
91
+ * | ------------------------ | -------------------------- |
92
+ * | `<!-- CAAMP:START -->` | none (canonical) |
93
+ * | `!-- CAAMP:START -->` | leading `<` lost |
94
+ * | `<!-- CAAMP:START --` | trailing `>` lost |
95
+ * | `<!--CAAMP:START-->` | spaces collapsed |
96
+ * | `<!-- caamp:start -->` | case folded |
97
+ *
98
+ * A bare `CAAMP:START` with no comment punctuation at all is deliberately NOT
99
+ * matched — see {@link damagedMarkerSource}.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const damaged = new RegExp(CAAMP_DAMAGED_START_PATTERN_SOURCE, 'gmi');
104
+ * const healed = content.replace(damaged, CAAMP_MARKER_START);
105
+ * ```
106
+ */
107
+ export const CAAMP_DAMAGED_START_PATTERN_SOURCE = damagedMarkerSource('START');
108
+
109
+ /**
110
+ * Source of the damage-tolerant pattern matching a closing marker line.
111
+ *
112
+ * Mirrors {@link CAAMP_DAMAGED_START_PATTERN_SOURCE} for `CAAMP:END`. Use
113
+ * with the `gmi` flags.
114
+ */
115
+ export const CAAMP_DAMAGED_END_PATTERN_SOURCE = damagedMarkerSource('END');
116
+
117
+ /**
118
+ * The outcome of writing a CAAMP block into an instruction file.
119
+ *
120
+ * Previously re-declared as an inline union in eight separate locations.
121
+ *
122
+ * - `created` — the file did not exist and was written from scratch.
123
+ * - `added` — the file existed with no CAAMP block; one was prepended.
124
+ * - `repaired` — at least one damaged marker was healed back to canonical form.
125
+ * - `consolidated` — several blocks were merged down to exactly one.
126
+ * - `updated` — a single block existed and its content changed.
127
+ * - `intact` — the file already had exactly the desired content; no write.
128
+ */
129
+ export type CaampInjectionAction =
130
+ | 'created'
131
+ | 'added'
132
+ | 'repaired'
133
+ | 'consolidated'
134
+ | 'updated'
135
+ | 'intact';
136
+
137
+ /**
138
+ * Every {@link CaampInjectionAction} value, ordered from most to least
139
+ * invasive. Useful for reporting the single most significant action taken
140
+ * across a batch of files.
141
+ */
142
+ export const CAAMP_INJECTION_ACTIONS = [
143
+ 'created',
144
+ 'added',
145
+ 'repaired',
146
+ 'consolidated',
147
+ 'updated',
148
+ 'intact',
149
+ ] as const;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The keys under which CLEO payloads carry a list of records.
3
+ *
4
+ * ## Why this is a shared constant (T12077)
5
+ *
6
+ * Different operations name their collection differently — `tasks.list` emits
7
+ * `tasks`, the SDK's generic list emits `items`, `tasks.find` emits `results`,
8
+ * and `tasks.next` emits `suggestions`. Each consumer that wants "the rows"
9
+ * has historically inlined its own guess, and each wrong guess fails SILENTLY,
10
+ * because an absent key is indistinguishable from an empty result set.
11
+ *
12
+ * Three separate outages traced to exactly this:
13
+ *
14
+ * 1. **T12067** — `output-mode.ts` knew only `tasks`/`items`, in three
15
+ * separately-inlined lists. `cleo find … --output count` reported 1626
16
+ * while `--output id` reported `No ids.` against the same payload.
17
+ * 2. **T12077 (this)** — `cleo next --output id` / `--output count` reported
18
+ * nothing and zero while the envelope held 836 candidates, because
19
+ * `suggestions` was in nobody's list.
20
+ * 3. **T12077 (the severe one)** — `defaultPickTask` in the sentient loop read
21
+ * `response.data.tasks` from `cleo.tasks.find()`, which returns
22
+ * `{results, total}` *unwrapped*. The key was wrong AND the `data` envelope
23
+ * it was reaching through does not exist on the SDK surface, so
24
+ * `allCandidates` was **always `[]`**. The autonomous loop therefore
25
+ * returned `no-task` on every tick it had ever run — 24 ticks, 0 tasks
26
+ * picked, across three months — and looked exactly like "there is no work
27
+ * to do" rather than like a bug.
28
+ *
29
+ * The lesson those three share is that this list must exist in ONE place that
30
+ * both the render layer and the SDK consumers import. Adding an operation with
31
+ * a new collection key now means adding it here, once.
32
+ *
33
+ * @task T12077
34
+ */
35
+
36
+ /**
37
+ * Collection keys in resolution order.
38
+ *
39
+ * Order matters only when a payload carries more than one (it should not);
40
+ * earlier keys win, which keeps the canonical `tasks` shape authoritative.
41
+ */
42
+ export const COLLECTION_KEYS = ['tasks', 'items', 'results', 'suggestions'] as const;
43
+
44
+ /** A key under which a payload may carry its rows. */
45
+ export type CollectionKey = (typeof COLLECTION_KEYS)[number];
@@ -8392,6 +8392,28 @@ export const OPERATIONS: OperationDef[] = [
8392
8392
  required: false,
8393
8393
  description: 'Commit plan file before dispatching',
8394
8394
  },
8395
+ // T12089: the plan SCOPE. The workflow regenerates the plan when no
8396
+ // committed plan is supplied, and `cleo release plan` requires
8397
+ // `--saga | --epic | --tasks` — so without one of these the dispatched
8398
+ // run dies at "Prepare bump-PR" after a fully green preflight.
8399
+ //
8400
+ // A param absent from this registry is STRIPPED before the domain handler
8401
+ // sees it, silently. `--tasks` was accepted by the CLI, forwarded by the
8402
+ // handler and by `releaseOpen`, and still arrived empty at the workflow
8403
+ // because this declaration was the fourth hop in the chain.
8404
+ {
8405
+ name: 'epic',
8406
+ type: 'string',
8407
+ required: false,
8408
+ description: 'Epic task ID forwarded as the plan scope (e.g. T9999)',
8409
+ },
8410
+ {
8411
+ name: 'tasks',
8412
+ type: 'string',
8413
+ required: false,
8414
+ description:
8415
+ 'Comma-separated task IDs forwarded as the plan scope for a task-scoped release',
8416
+ },
8395
8417
  ] satisfies ParamDef[],
8396
8418
  },
8397
8419
 
package/src/index.ts CHANGED
@@ -2653,4 +2653,18 @@ export {
2653
2653
  workGraphSliceParamsSchema,
2654
2654
  workGraphSliceSchema,
2655
2655
  } from './workgraph.js';
2656
+
2656
2657
  // === WASM SDK (Rust crate bindings) ===
2658
+
2659
+ export type { CaampInjectionAction } from './caamp-markers.js';
2660
+ // === CAAMP instruction-block marker grammar (T12051) ===
2661
+ export {
2662
+ CAAMP_BLOCK_PATTERN_SOURCE,
2663
+ CAAMP_DAMAGED_END_PATTERN_SOURCE,
2664
+ CAAMP_DAMAGED_START_PATTERN_SOURCE,
2665
+ CAAMP_INJECTION_ACTIONS,
2666
+ CAAMP_MARKER_END,
2667
+ CAAMP_MARKER_START,
2668
+ } from './caamp-markers.js';
2669
+ // T12077: SSoT for the keys under which payloads carry their rows.
2670
+ export { COLLECTION_KEYS, type CollectionKey } from './collection-keys.js';
@@ -124,6 +124,16 @@ export interface ResolveLLMForSystemOptions {
124
124
  * without changing the system identity.
125
125
  */
126
126
  roleOverride?: RoleName;
127
+ /**
128
+ * Providers to skip for this resolution (T12084).
129
+ *
130
+ * Mirrors `ResolveLLMForRoleOptions.excludeProviders` and exists for the same
131
+ * reason: a caller that has just watched a provider fail needs its retry to
132
+ * land elsewhere. Without it, every consumer of this resolver — fix-gen among
133
+ * them — is permanently pinned to the first provider the selector picks, no
134
+ * matter how reliably it fails.
135
+ */
136
+ excludeProviders?: readonly string[];
127
137
 
128
138
  /**
129
139
  * When `true`, the resolver skips the provider-registry defaultModel lookup
@@ -164,7 +164,9 @@ export type CredentialSourceWire =
164
164
  | 'cred-file'
165
165
  | 'claude-creds'
166
166
  | 'global-config'
167
- | 'project-config';
167
+ | 'project-config'
168
+ /** Keyless local inference daemon detected at runtime (T12082). */
169
+ | 'local-daemon';
168
170
 
169
171
  /**
170
172
  * Authentication scheme used when sending the credential to the provider.
@@ -223,6 +225,22 @@ export interface CredentialMetadataWire {
223
225
  source: CredentialSourceWire | undefined;
224
226
  /** Scheme used to present the credential to the provider. */
225
227
  authType: AuthTypeWire;
228
+ /**
229
+ * Custom API endpoint this credential targets, when it has one.
230
+ *
231
+ * NOT a secret — an endpoint URL — so it belongs on the metadata wire rather
232
+ * than behind the sealed handle. It is load-bearing: without it a consumer
233
+ * building a transport cannot learn that a credential points at a local or
234
+ * proxied endpoint, and silently falls back to the vendor's public API.
235
+ *
236
+ * T12081: `role-executor` set `baseUrl` from `deriveApiWire(provider)` — the
237
+ * provider DEFAULT — because this field did not exist. An Ollama credential
238
+ * carrying `http://localhost:11434/v1` was rewritten to `api.openai.com` and
239
+ * rejected 401; the same held for LM Studio, vLLM, OpenRouter and Azure.
240
+ * Every custom-endpoint credential resolved correctly, then called the wrong
241
+ * host.
242
+ */
243
+ baseUrl?: string | null;
226
244
  }
227
245
 
228
246
  // ============================================================================
@@ -365,6 +383,19 @@ export interface ResolveLLMForRoleOptions {
365
383
  * @task T11759
366
384
  */
367
385
  profileOverride?: string;
386
+ /**
387
+ * Providers to skip during this resolution (T12082).
388
+ *
389
+ * Set by the caller after a provider has already failed for this call, so
390
+ * the retry lands somewhere else. Applies to EVERY tier including config
391
+ * pins: a pinned provider that just returned a terminal error is not a
392
+ * better answer for having been pinned.
393
+ *
394
+ * Empty/absent leaves resolution completely unchanged.
395
+ *
396
+ * @task T12082
397
+ */
398
+ excludeProviders?: readonly string[];
368
399
  }
369
400
 
370
401
  // ============================================================================
@@ -18,6 +18,15 @@ export type Provider = string;
18
18
  * CAAMP-compatible spawn options (inlined for zero-dep contracts).
19
19
  */
20
20
  export interface CAAMPSpawnOptions {
21
+ /**
22
+ * Capacity-aware routing hint — the id of the least-loaded registered agent
23
+ * (Wave 6-C/D). Declared explicitly because `spawn-ops.ts` both writes it
24
+ * and reads it back for the `SubagentStart`/`SubagentStop` payloads; under
25
+ * the bare index signature it resolved to `unknown`, so
26
+ * `options?.preferredAgent ?? \`worker-${taskId}\`` widened to `{}` and the
27
+ * agentId silently lost its type. @task T12071
28
+ */
29
+ preferredAgent?: string;
21
30
  [key: string]: unknown;
22
31
  }
23
32
 
@@ -79,6 +88,17 @@ export interface CLEOSpawnResult extends CAAMPSpawnResult {
79
88
  durationMs?: number;
80
89
  };
81
90
 
91
+ /**
92
+ * Terminal (or in-flight) state of the spawned agent.
93
+ *
94
+ * Declared explicitly because `spawn-ops.ts` reads `result.status` to build
95
+ * the `SubagentStop` hook payload. Inherited from `CAAMPSpawnResult`'s bare
96
+ * `[key: string]: unknown` it resolved to `unknown`, so the value flowed
97
+ * into a typed payload field unchecked — and it does not even share that
98
+ * field's vocabulary ('completed' here vs 'complete' there). @task T12071
99
+ */
100
+ status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
101
+
82
102
  /** Reference to manifest entry if output was captured */
83
103
  manifestEntryId?: string;
84
104
  }
package/src/task.ts CHANGED
@@ -192,7 +192,23 @@ export type EvidenceAtom =
192
192
  failCount: number;
193
193
  skipCount: number;
194
194
  }
195
- | { kind: 'tool'; tool: string; exitCode: number; stdoutTail: string }
195
+ | {
196
+ kind: 'tool';
197
+ tool: string;
198
+ exitCode: number;
199
+ stdoutTail?: string;
200
+ /**
201
+ * The project has no such toolchain, so the gate is satisfied by absence
202
+ * rather than by a run (T12083).
203
+ *
204
+ * Recorded explicitly so the audit trail never implies a tool ran that
205
+ * did not. Only ever set for a GUESSED language default — a command
206
+ * declared in `project-context.json` is always executed.
207
+ */
208
+ notApplicable?: boolean;
209
+ /** Why the tool was judged inapplicable. Present iff `notApplicable`. */
210
+ reason?: string;
211
+ }
196
212
  | { kind: 'url'; url: string }
197
213
  | { kind: 'note'; note: string }
198
214
  | { kind: 'override'; reason: string }