@open-agent-toolkit/cli 0.3.7 → 0.3.8

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 (71) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +15 -1
  3. package/assets/docs/cli-utilities/workflow-gates.md +37 -0
  4. package/assets/docs/provider-sync/commands.md +6 -0
  5. package/assets/docs/provider-sync/providers.md +20 -0
  6. package/assets/docs/provider-sync/scope-and-surface.md +2 -0
  7. package/assets/docs/reference/cli-reference.md +2 -1
  8. package/assets/docs/workflows/projects/evidence-layers.md +5 -1
  9. package/assets/docs/workflows/projects/retro.md +7 -0
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-agent-instructions-analyze/SKILL.md +1 -1
  12. package/assets/skills/oat-agent-instructions-analyze/scripts/resolve-providers.sh +17 -6
  13. package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
  14. package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +387 -0
  15. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +75 -0
  16. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  17. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  18. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  19. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +24 -22
  20. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  21. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
  22. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  23. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  24. package/assets/skills/oat-project-retro/SKILL.md +70 -6
  25. package/assets/skills/oat-project-review-provide/SKILL.md +3 -1
  26. package/assets/skills/oat-project-review-provide-remote/SKILL.md +3 -1
  27. package/dist/agents/canonical/resolve.d.ts +6 -4
  28. package/dist/agents/canonical/resolve.d.ts.map +1 -1
  29. package/dist/agents/canonical/resolve.js +6 -4
  30. package/dist/commands/gate/index.d.ts.map +1 -1
  31. package/dist/commands/gate/index.js +51 -2
  32. package/dist/commands/gate/review-verdict.d.ts +22 -0
  33. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  34. package/dist/commands/gate/review-verdict.js +111 -0
  35. package/dist/commands/project/dispatch/canonical-role.d.ts +33 -0
  36. package/dist/commands/project/dispatch/canonical-role.d.ts.map +1 -0
  37. package/dist/commands/project/dispatch/canonical-role.js +114 -0
  38. package/dist/commands/project/dispatch/index.d.ts.map +1 -1
  39. package/dist/commands/project/dispatch/index.js +3 -1
  40. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  41. package/dist/commands/project/dispatch/record.js +146 -4
  42. package/dist/commands/sync/apply.d.ts.map +1 -1
  43. package/dist/commands/sync/apply.js +28 -21
  44. package/dist/commands/tools/shared/sync-evidence.d.ts.map +1 -1
  45. package/dist/commands/tools/shared/sync-evidence.js +32 -5
  46. package/dist/config/oat-config.d.ts +8 -0
  47. package/dist/config/oat-config.d.ts.map +1 -1
  48. package/dist/config/oat-config.js +116 -1
  49. package/dist/engine/compute-plan.d.ts.map +1 -1
  50. package/dist/engine/compute-plan.js +31 -3
  51. package/dist/providers/claude/dispatch-envelope.d.ts +53 -0
  52. package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -1
  53. package/dist/providers/claude/dispatch-envelope.js +324 -48
  54. package/dist/providers/claude/rule-transform.js +1 -1
  55. package/dist/providers/copilot/rule-transform.js +1 -1
  56. package/dist/providers/cursor/rule-transform.js +1 -1
  57. package/dist/providers/identity/absolute-paths.d.ts +13 -0
  58. package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
  59. package/dist/providers/identity/absolute-paths.js +27 -14
  60. package/dist/providers/identity/generic-dispatch-record.d.ts +212 -0
  61. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -1
  62. package/dist/providers/identity/generic-dispatch-record.js +78 -19
  63. package/dist/providers/identity/oat-dispatch-record.d.ts +5 -0
  64. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
  65. package/dist/providers/identity/oat-dispatch-record.js +11 -2
  66. package/dist/providers/shared/registry.d.ts +6 -0
  67. package/dist/providers/shared/registry.d.ts.map +1 -1
  68. package/dist/providers/shared/registry.js +30 -0
  69. package/dist/rules/canonical/parse.d.ts.map +1 -1
  70. package/dist/rules/canonical/parse.js +38 -3
  71. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
- import { parseGenericDispatchRecord, } from '../identity/generic-dispatch-record.js';
1
+ import { collectAbsolutePathViolations } from '../identity/absolute-paths.js';
2
+ import { collectSensitiveDispatchContent, genericDispatchRecordBaseSchema, identityFieldsOf, parseGenericDispatchRecord, redactSensitiveValues, refineGenericDispatchRecord, } from '../identity/generic-dispatch-record.js';
2
3
  import YAML from 'yaml';
3
4
  import { z } from 'zod';
4
5
  import { readOatManagedClaudeRole } from './codec/materialize.js';
@@ -97,76 +98,166 @@ function definitionFrontmatter(content) {
97
98
  }
98
99
  return parsed;
99
100
  }
100
- function assertSameTarget(left, right) {
101
- if (left.model !== right.model ||
102
- left.effort !== right.effort ||
103
- JSON.stringify(left.capabilityEvidence) !==
104
- JSON.stringify(right.capabilityEvidence)) {
105
- throw new Error('Claude resolver target and selection target disagree on model, effort, or capability evidence.');
101
+ /** Render a Zod issue path as `a.b[0].c`, or `<root>` when empty. */
102
+ export function formatIssuePath(prefix, path) {
103
+ let rendered = prefix ?? '';
104
+ for (const segment of path) {
105
+ rendered +=
106
+ typeof segment === 'number'
107
+ ? `[${segment}]`
108
+ : rendered === ''
109
+ ? segment
110
+ : `.${segment}`;
106
111
  }
112
+ return rendered === '' ? '<root>' : rendered;
113
+ }
114
+ export function zodIssueViolations(stage, prefix, error) {
115
+ return error.issues.map((issue) => ({
116
+ stage,
117
+ path: formatIssuePath(prefix, issue.path),
118
+ message: issue.message,
119
+ }));
107
120
  }
108
121
  /**
109
- * Validate the managed resolver result against the generated provider role and
110
- * the exact payload that will be launched. This is the production boundary
111
- * used by the project dispatch-record producer for managed Claude dispatches.
122
+ * The launch consistency checks, in the order the throwing boundary has always
123
+ * applied them, so its first reported error is unchanged. A check that needs
124
+ * an input which did not parse — the launch payload, the managed role marker,
125
+ * or the definition frontmatter — is skipped rather than guessed at.
112
126
  */
113
- export function acceptClaudeLaunchEnvelope(input) {
114
- const resolution = resolutionSchema.parse(input.resolution);
127
+ function launchConsistencyViolations(resolution, launch, definition) {
128
+ const violations = [];
115
129
  const provider = resolution.providers.claude;
116
- const launch = launchPayloadSchema.parse(input.launch);
117
- assertSameTarget(provider.target, provider.selection.target);
118
- const validation = validateClaudeDispatchCapability(provider.selection.target);
130
+ const selected = provider.selection.target;
131
+ if (provider.target.model !== selected.model ||
132
+ provider.target.effort !== selected.effort ||
133
+ JSON.stringify(provider.target.capabilityEvidence) !==
134
+ JSON.stringify(selected.capabilityEvidence)) {
135
+ violations.push({
136
+ path: 'resolution.providers.claude.target',
137
+ message: 'Claude resolver target and selection target disagree on model, effort, or capability evidence.',
138
+ });
139
+ }
140
+ const validation = validateClaudeDispatchCapability(selected);
119
141
  if (!validation.valid) {
120
- throw new Error(validation.reason ?? 'Resolver selected an invalid Claude target.');
142
+ violations.push({
143
+ path: 'resolution.providers.claude.selection.target',
144
+ message: validation.reason ?? 'Resolver selected an invalid Claude target.',
145
+ });
121
146
  }
122
147
  const baseRole = provider.selection.role === 'reviewer'
123
148
  ? 'oat-reviewer'
124
149
  : 'oat-phase-implementer';
125
150
  const expectedVariant = buildClaudeEffortVariantName({
126
151
  agentName: baseRole,
127
- model: provider.selection.target.model,
128
- effort: provider.selection.target.effort,
152
+ model: selected.model,
153
+ effort: selected.effort,
129
154
  });
130
155
  if (provider.dispatchArgs.variant !== expectedVariant) {
131
- throw new Error(`Managed Claude resolver variant ${provider.dispatchArgs.variant} does not match selected target ${expectedVariant}.`);
156
+ violations.push({
157
+ path: 'resolution.providers.claude.dispatchArgs.variant',
158
+ message: `Managed Claude resolver variant ${provider.dispatchArgs.variant} does not match selected target ${expectedVariant}.`,
159
+ });
132
160
  }
133
- if (launch.variant !== expectedVariant) {
134
- throw new Error(`Claude launch variant ${launch.variant} does not match resolver-selected variant ${expectedVariant}.`);
161
+ if (launch !== null && launch.variant !== expectedVariant) {
162
+ violations.push({
163
+ path: 'payload.variant',
164
+ message: `Claude launch variant ${launch.variant} does not match resolver-selected variant ${expectedVariant}.`,
165
+ });
135
166
  }
136
- const managed = readOatManagedClaudeRole(input.definition);
167
+ const managed = readOatManagedClaudeRole(definition);
137
168
  if (!managed || managed.roleName !== expectedVariant) {
138
- throw new Error(`Generated Claude definition ${expectedVariant} is absent or is not the matching OAT-managed role.`);
169
+ violations.push({
170
+ path: 'definition',
171
+ message: `Generated Claude definition ${expectedVariant} is absent or is not the matching OAT-managed role.`,
172
+ });
139
173
  }
140
- if (JSON.stringify(managed.capabilityEvidence) !==
141
- JSON.stringify(provider.selection.target.capabilityEvidence)) {
142
- throw new Error('Generated Claude definition capability evidence does not match the resolver-selected capability.');
174
+ if (managed &&
175
+ JSON.stringify(managed.capabilityEvidence) !==
176
+ JSON.stringify(selected.capabilityEvidence)) {
177
+ violations.push({
178
+ path: 'definition',
179
+ message: 'Generated Claude definition capability evidence does not match the resolver-selected capability.',
180
+ });
143
181
  }
144
- const frontmatter = definitionFrontmatter(input.definition);
145
- if (frontmatter.name !== expectedVariant) {
146
- throw new Error(`Generated Claude definition name does not match ${expectedVariant}.`);
182
+ let frontmatter = null;
183
+ try {
184
+ frontmatter = definitionFrontmatter(definition);
147
185
  }
148
- if (frontmatter.model !== provider.selection.target.model) {
149
- throw new Error(`Generated Claude definition model does not match selected model ${provider.selection.target.model}.`);
186
+ catch (error) {
187
+ violations.push({
188
+ path: 'definition',
189
+ message: error instanceof Error ? error.message : String(error),
190
+ });
150
191
  }
151
- if (frontmatter.effort !== provider.selection.target.effort) {
152
- throw new Error(`Generated Claude definition effort does not match selected effort ${provider.selection.target.effort}.`);
192
+ if (frontmatter !== null) {
193
+ if (frontmatter.name !== expectedVariant) {
194
+ violations.push({
195
+ path: 'definition',
196
+ message: `Generated Claude definition name does not match ${expectedVariant}.`,
197
+ });
198
+ }
199
+ if (frontmatter.model !== selected.model) {
200
+ violations.push({
201
+ path: 'definition',
202
+ message: `Generated Claude definition model does not match selected model ${selected.model}.`,
203
+ });
204
+ }
205
+ if (frontmatter.effort !== selected.effort) {
206
+ violations.push({
207
+ path: 'definition',
208
+ message: `Generated Claude definition effort does not match selected effort ${selected.effort}.`,
209
+ });
210
+ }
153
211
  }
154
- if (launch.model !== undefined &&
155
- launch.model !== provider.selection.target.model) {
156
- throw new Error(`Per-call Claude model ${launch.model} conflicts with generated definition model ${provider.selection.target.model}.`);
212
+ if (launch !== null &&
213
+ launch.model !== undefined &&
214
+ launch.model !== selected.model) {
215
+ violations.push({
216
+ path: 'payload.model',
217
+ message: `Per-call Claude model ${launch.model} conflicts with generated definition model ${selected.model}.`,
218
+ });
157
219
  }
158
- if (provider.modelAxis !== `selected:${provider.selection.target.model}`) {
159
- throw new Error('Claude resolver model axis disagrees with its target.');
220
+ if (provider.modelAxis !== `selected:${selected.model}`) {
221
+ violations.push({
222
+ path: 'resolution.providers.claude.modelAxis',
223
+ message: 'Claude resolver model axis disagrees with its target.',
224
+ });
160
225
  }
161
- if (provider.effortAxis !== `selected:${provider.selection.target.effort}`) {
162
- throw new Error('Claude resolver effort axis disagrees with its target.');
226
+ if (provider.effortAxis !== `selected:${selected.effort}`) {
227
+ violations.push({
228
+ path: 'resolution.providers.claude.effortAxis',
229
+ message: 'Claude resolver effort axis disagrees with its target.',
230
+ });
163
231
  }
232
+ return violations;
233
+ }
234
+ /**
235
+ * Validate the managed resolver result against the generated provider role and
236
+ * the exact payload that will be launched. This is the production boundary
237
+ * used by the project dispatch-record producer for managed Claude dispatches.
238
+ * It stops at the first failure; {@link collectClaudeLaunchViolations} reports
239
+ * every failure for a caller building its input.
240
+ */
241
+ export function acceptClaudeLaunchEnvelope(input) {
242
+ const resolution = resolutionSchema.parse(input.resolution);
243
+ const provider = resolution.providers.claude;
244
+ const launch = launchPayloadSchema.parse(input.launch);
245
+ const [first] = launchConsistencyViolations(resolution, launch, input.definition);
246
+ if (first)
247
+ throw new Error(first.message);
248
+ const baseRole = provider.selection.role === 'reviewer'
249
+ ? 'oat-reviewer'
250
+ : 'oat-phase-implementer';
164
251
  return {
165
252
  schemaVersion: 1,
166
253
  provider: 'claude',
167
254
  resolverRole: provider.selection.role,
168
255
  baseRole,
169
- variant: expectedVariant,
256
+ variant: buildClaudeEffortVariantName({
257
+ agentName: baseRole,
258
+ model: provider.selection.target.model,
259
+ effort: provider.selection.target.effort,
260
+ }),
170
261
  model: provider.selection.target.model,
171
262
  capabilityEvidence: provider.selection.target.capabilityEvidence,
172
263
  effort: provider.selection.target.effort,
@@ -175,6 +266,195 @@ export function acceptClaudeLaunchEnvelope(input) {
175
266
  payload: launch,
176
267
  };
177
268
  }
269
+ const LAUNCH_CONSISTENCY_SKIPPED = 'claudeLaunch consistency checks (need a parsed resolution)';
270
+ const PAYLOAD_CHECKS_SKIPPED = 'claudeLaunch payload variant and model checks (need a parsed payload)';
271
+ const ACTION_ROLE_SKIPPED = 'recordBase action/role check (needs a parsed resolution)';
272
+ /**
273
+ * Report every `claudeLaunch` violation in one pass: each schema issue in the
274
+ * resolution and the payload, then every consistency check the parsed inputs
275
+ * allow. Consistency checks depend on a parsed resolution, and the two payload
276
+ * checks on a parsed payload; those are skipped and named in `skipped` rather
277
+ * than reported against a value that failed to parse.
278
+ */
279
+ export function collectClaudeLaunchViolations(claudeLaunch) {
280
+ if (!claudeLaunch ||
281
+ typeof claudeLaunch !== 'object' ||
282
+ Array.isArray(claudeLaunch)) {
283
+ return {
284
+ resolverRole: null,
285
+ violations: [
286
+ {
287
+ stage: 'claudeLaunch',
288
+ path: '<root>',
289
+ message: 'Managed Claude claudeLaunch must be a JSON object with resolution, definition, and payload.',
290
+ },
291
+ ],
292
+ skipped: [LAUNCH_CONSISTENCY_SKIPPED],
293
+ };
294
+ }
295
+ const launchInput = claudeLaunch;
296
+ const violations = [];
297
+ const resolution = resolutionSchema.safeParse(launchInput.resolution);
298
+ if (!resolution.success) {
299
+ violations.push(...zodIssueViolations('claudeLaunch', 'resolution', resolution.error));
300
+ }
301
+ const launch = launchPayloadSchema.safeParse(launchInput.payload);
302
+ if (!launch.success) {
303
+ violations.push(...zodIssueViolations('claudeLaunch', 'payload', launch.error));
304
+ }
305
+ if (!resolution.success) {
306
+ return {
307
+ resolverRole: null,
308
+ violations,
309
+ skipped: [LAUNCH_CONSISTENCY_SKIPPED],
310
+ };
311
+ }
312
+ const skipped = launch.success ? [] : [PAYLOAD_CHECKS_SKIPPED];
313
+ violations.push(...launchConsistencyViolations(resolution.data, launch.success ? launch.data : null, String(launchInput.definition ?? '')).map((violation) => ({ stage: 'claudeLaunch', ...violation })));
314
+ return {
315
+ resolverRole: resolution.data.providers.claude.selection.role,
316
+ violations,
317
+ skipped,
318
+ };
319
+ }
320
+ const PROTECTED_FIELD_MASK = Object.fromEntries(PROTECTED_RECORD_FIELDS.map((field) => [field, true]));
321
+ /**
322
+ * The caller-authored half of a managed record: the closed generic field set
323
+ * with every derived field omitted. The cross-field rules are applied
324
+ * separately (see {@link crossFieldViolations}) because a Zod refinement never
325
+ * runs once the object parse aborts, which would push those rules to a later
326
+ * run.
327
+ */
328
+ const managedRecordBaseSchema = genericDispatchRecordBaseSchema.omit(PROTECTED_FIELD_MASK);
329
+ /**
330
+ * Run the generic cross-field rules directly on the caller-owned fields,
331
+ * whether or not the object parse succeeded. The rules read every field
332
+ * defensively, so a missing or mistyped field elsewhere does not hide a
333
+ * cross-field violation until the next run.
334
+ */
335
+ function crossFieldViolations(callerOwned) {
336
+ const violations = [];
337
+ const context = {
338
+ path: [],
339
+ addIssue: (issue) => {
340
+ violations.push({
341
+ stage: 'recordBase',
342
+ path: formatIssuePath(null, issue.path ?? []),
343
+ message: issue.message ?? 'Invalid',
344
+ });
345
+ },
346
+ };
347
+ refineGenericDispatchRecord(callerOwned, context);
348
+ return violations;
349
+ }
350
+ function actionConflict(resolverRole, action) {
351
+ return (resolverRole === 'reviewer' && action !== 'review') ||
352
+ (resolverRole === 'implementer' &&
353
+ action !== 'implementation' &&
354
+ action !== 'fix')
355
+ ? `Managed Claude resolver role ${resolverRole} conflicts with record action ${String(action)}.`
356
+ : null;
357
+ }
358
+ /**
359
+ * Report every `recordBase` violation in one pass: each derived field that is
360
+ * present, the action/role check, every missing, mistyped, or unknown field,
361
+ * every absolute path in an identity or control field, and every sensitive key
362
+ * or value. The action/role check needs a parsed resolution and is skipped
363
+ * without one.
364
+ */
365
+ export function collectClaudeRecordBaseViolations(candidate, resolverRole) {
366
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
367
+ return {
368
+ violations: [
369
+ {
370
+ stage: 'recordBase',
371
+ path: '<root>',
372
+ message: 'Managed Claude dispatch recordBase must be a JSON object.',
373
+ },
374
+ ],
375
+ skipped: [],
376
+ };
377
+ }
378
+ const base = candidate;
379
+ const protectedFields = new Set(PROTECTED_RECORD_FIELDS);
380
+ const violations = [];
381
+ for (const field of PROTECTED_RECORD_FIELDS) {
382
+ if (Object.hasOwn(base, field)) {
383
+ violations.push({
384
+ stage: 'recordBase',
385
+ path: field,
386
+ message: `Managed Claude dispatch recordBase must omit derived field ${field}.`,
387
+ });
388
+ }
389
+ }
390
+ const skipped = [];
391
+ if (resolverRole === null) {
392
+ skipped.push(ACTION_ROLE_SKIPPED);
393
+ }
394
+ else {
395
+ const conflict = actionConflict(resolverRole, base.action);
396
+ if (conflict !== null) {
397
+ violations.push({
398
+ stage: 'recordBase',
399
+ path: 'action',
400
+ message: conflict,
401
+ });
402
+ }
403
+ }
404
+ const callerOwned = Object.fromEntries(Object.entries(base).filter(([key]) => !protectedFields.has(key)));
405
+ const parsed = managedRecordBaseSchema.safeParse(callerOwned);
406
+ if (!parsed.success) {
407
+ violations.push(...zodIssueViolations('recordBase', null, parsed.error));
408
+ }
409
+ violations.push(...crossFieldViolations(callerOwned));
410
+ for (const [field, value] of Object.entries(identityFieldsOf(callerOwned))) {
411
+ violations.push(...collectAbsolutePathViolations(value, field).map((violation) => ({
412
+ stage: 'recordBase',
413
+ ...violation,
414
+ })));
415
+ }
416
+ violations.push(...collectSensitiveDispatchContent(base, 'recordBase').map((violation) => ({
417
+ stage: 'recordBase',
418
+ path: violation.path.slice('recordBase.'.length) || '<root>',
419
+ message: violation.message,
420
+ })));
421
+ return { violations, skipped };
422
+ }
423
+ function oneLine(text) {
424
+ return text.replace(/\r?\n/g, ' ');
425
+ }
426
+ /**
427
+ * One error for a whole validation run: a header, then one
428
+ * `stage path: message` line per violation, then the checks that were skipped
429
+ * because they depend on a stage that failed to parse.
430
+ */
431
+ export class ManagedClaudeDispatchValidationError extends Error {
432
+ violations;
433
+ skipped;
434
+ constructor(violations, skipped = []) {
435
+ // Scrub each violation on its own before joining. A scrub over the joined
436
+ // text lets one unterminated secret (a private-key header with no END
437
+ // marker) consume every later line; per violation it ends with that
438
+ // violation. Echoed newlines are folded so each violation stays one line
439
+ // and the line count matches the header.
440
+ const scrubbed = violations.map((violation) => ({
441
+ stage: violation.stage,
442
+ path: oneLine(redactSensitiveValues(violation.path)),
443
+ message: oneLine(redactSensitiveValues(violation.message)),
444
+ }));
445
+ const lines = [
446
+ `Managed Claude dispatch input has ${scrubbed.length} violation${scrubbed.length === 1 ? '' : 's'}:`,
447
+ ...scrubbed.map(({ stage, path, message }) => `${stage} ${path}: ${message}`),
448
+ ];
449
+ if (skipped.length > 0) {
450
+ lines.push(`Skipped until the above are fixed: ${skipped.join('; ')}.`);
451
+ }
452
+ super(lines.join('\n'));
453
+ this.name = 'ManagedClaudeDispatchValidationError';
454
+ this.violations = scrubbed;
455
+ this.skipped = skipped;
456
+ }
457
+ }
178
458
  function recordBase(value) {
179
459
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
180
460
  throw new Error('Managed Claude dispatch recordBase must be a JSON object.');
@@ -190,13 +470,9 @@ function recordBase(value) {
190
470
  /** Build configured invocation evidence only from an accepted launch envelope. */
191
471
  export function buildClaudeDispatchRecord(input) {
192
472
  const base = recordBase(input.recordBase);
193
- const action = base.action;
194
- if ((input.envelope.resolverRole === 'reviewer' && action !== 'review') ||
195
- (input.envelope.resolverRole === 'implementer' &&
196
- action !== 'implementation' &&
197
- action !== 'fix')) {
198
- throw new Error(`Managed Claude resolver role ${input.envelope.resolverRole} conflicts with record action ${String(action)}.`);
199
- }
473
+ const conflict = actionConflict(input.envelope.resolverRole, base.action);
474
+ if (conflict !== null)
475
+ throw new Error(conflict);
200
476
  return parseGenericDispatchRecord({
201
477
  ...base,
202
478
  provider: 'claude',
@@ -8,7 +8,7 @@ function parsePaths(value) {
8
8
  return undefined;
9
9
  }
10
10
  export function transformCanonicalToClaudeRule(canonicalContent, canonicalPath) {
11
- const rule = parseCanonicalRuleMarkdown(canonicalContent);
11
+ const rule = parseCanonicalRuleMarkdown(canonicalContent, canonicalPath);
12
12
  const frontmatter = rule.activation === 'glob' && rule.globs ? { paths: rule.globs } : null;
13
13
  return appendGeneratedMarker(renderMarkdownWithFrontmatter(frontmatter, rule.body), canonicalPath);
14
14
  }
@@ -33,7 +33,7 @@ function isRepoWideApplyTo(globs) {
33
33
  return globs?.length === 1 && globs[0] === '**';
34
34
  }
35
35
  export function transformCanonicalToCopilotRule(canonicalContent, canonicalPath) {
36
- const rule = parseCanonicalRuleMarkdown(canonicalContent);
36
+ const rule = parseCanonicalRuleMarkdown(canonicalContent, canonicalPath);
37
37
  if (rule.activation === 'glob' && rule.globs) {
38
38
  assertCopilotGlobsAreRepresentable(rule.globs);
39
39
  }
@@ -24,7 +24,7 @@ function buildCursorFrontmatter(activation, description, globs) {
24
24
  };
25
25
  }
26
26
  export function transformCanonicalToCursorRule(canonicalContent, canonicalPath) {
27
- const rule = parseCanonicalRuleMarkdown(canonicalContent);
27
+ const rule = parseCanonicalRuleMarkdown(canonicalContent, canonicalPath);
28
28
  const frontmatter = buildCursorFrontmatter(rule.activation, rule.description, rule.globs);
29
29
  return appendGeneratedMarker(renderMarkdownWithFrontmatter(frontmatter, rule.body), canonicalPath);
30
30
  }
@@ -49,6 +49,19 @@ export declare function redactAbsolutePaths(value: string): string;
49
49
  * a malformed identifier.
50
50
  */
51
51
  export declare function redactAbsolutePathsDeep<T>(value: T): T;
52
+ /** One located validation violation. The message never echoes the value. */
53
+ export interface DispatchValueViolation {
54
+ path: string;
55
+ message: string;
56
+ }
57
+ /**
58
+ * Collect every absolute path in an identity or control value, in the same
59
+ * depth-first order {@link assertNoAbsolutePath} visits them, so a caller that
60
+ * reports all violations at once and one that stops at the first agree on
61
+ * which hit comes first. Each message names the field, never the path found
62
+ * there, so the report itself cannot disclose it.
63
+ */
64
+ export declare function collectAbsolutePathViolations(value: unknown, path: string): DispatchValueViolation[];
52
65
  /**
53
66
  * Recursively reject any absolute path. Used for identity and control fields,
54
67
  * where a path is never a legitimate value and silently rewriting one would
@@ -1 +1 @@
1
- {"version":3,"file":"absolute-paths.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/absolute-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA6BH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAM/C;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE3D;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CActD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,sCAAsC,CACpD,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAChD,IAAI,CAIN"}
1
+ {"version":3,"file":"absolute-paths.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/absolute-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA6BH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAM/C;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE3D;AAYD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CActD;AAED,4EAA4E;AAC5E,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,GACX,sBAAsB,EAAE,CAoB1B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAGvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,sCAAsC,CACpD,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAChD,IAAI,CAIN"}
@@ -107,26 +107,39 @@ export function redactAbsolutePathsDeep(value) {
107
107
  return value;
108
108
  }
109
109
  /**
110
- * Recursively reject any absolute path. Used for identity and control fields,
111
- * where a path is never a legitimate value and silently rewriting one would
112
- * corrupt the identifier it claims to be.
110
+ * Collect every absolute path in an identity or control value, in the same
111
+ * depth-first order {@link assertNoAbsolutePath} visits them, so a caller that
112
+ * reports all violations at once and one that stops at the first agree on
113
+ * which hit comes first. Each message names the field, never the path found
114
+ * there, so the report itself cannot disclose it.
113
115
  */
114
- export function assertNoAbsolutePath(value, path) {
116
+ export function collectAbsolutePathViolations(value, path) {
115
117
  if (typeof value === 'string') {
116
- if (containsAbsolutePath(value)) {
117
- throw new Error(`A dispatch record must not carry an absolute filesystem path at ${path}.`);
118
- }
119
- return;
118
+ return containsAbsolutePath(value)
119
+ ? [
120
+ {
121
+ path,
122
+ message: `A dispatch record must not carry an absolute filesystem path at ${path}.`,
123
+ },
124
+ ]
125
+ : [];
120
126
  }
121
127
  if (Array.isArray(value)) {
122
- value.forEach((entry, index) => assertNoAbsolutePath(entry, `${path}[${index}]`));
123
- return;
128
+ return value.flatMap((entry, index) => collectAbsolutePathViolations(entry, `${path}[${index}]`));
124
129
  }
125
130
  if (value === null || typeof value !== 'object')
126
- return;
127
- for (const [key, entry] of Object.entries(value)) {
128
- assertNoAbsolutePath(entry, `${path}.${key}`);
129
- }
131
+ return [];
132
+ return Object.entries(value).flatMap(([key, entry]) => collectAbsolutePathViolations(entry, `${path}.${key}`));
133
+ }
134
+ /**
135
+ * Recursively reject any absolute path. Used for identity and control fields,
136
+ * where a path is never a legitimate value and silently rewriting one would
137
+ * corrupt the identifier it claims to be.
138
+ */
139
+ export function assertNoAbsolutePath(value, path) {
140
+ const [first] = collectAbsolutePathViolations(value, path);
141
+ if (first)
142
+ throw new Error(first.message);
130
143
  }
131
144
  /**
132
145
  * The publication postcondition, scoped to what is actually guaranteed.