@open-agent-toolkit/cli 0.3.0 → 0.3.2

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 (76) hide show
  1. package/assets/agents/oat-reviewer.md +10 -3
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/config/dispatch-matrix-recommendation.json +63 -43
  4. package/assets/docs/cli-utilities/configuration.md +20 -8
  5. package/assets/docs/cli-utilities/workflow-gates.md +1 -1
  6. package/assets/docs/contributing/index.md +1 -0
  7. package/assets/docs/contributing/updating-model-guidance.md +106 -0
  8. package/assets/docs/contributing/verifying-cursor-pins.md +30 -18
  9. package/assets/docs/provider-sync/providers.md +5 -2
  10. package/assets/docs/reference/cli-reference.md +1 -1
  11. package/assets/docs/reference/oat-directory-structure.md +1 -1
  12. package/assets/docs/workflows/projects/artifacts.md +1 -1
  13. package/assets/docs/workflows/projects/dispatch-ceiling.md +67 -50
  14. package/assets/docs/workflows/projects/evidence-layers.md +6 -2
  15. package/assets/docs/workflows/projects/implementation-execution.md +9 -4
  16. package/assets/docs/workflows/projects/lifecycle.md +8 -6
  17. package/assets/docs/workflows/projects/programmatic-execution.md +4 -3
  18. package/assets/public-package-versions.json +4 -4
  19. package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
  20. package/assets/skills/oat-dispatch-subagents/references/provider-claude.md +20 -7
  21. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +4 -4
  22. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +4 -4
  23. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  24. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +2 -0
  25. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +82 -25
  26. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +4 -4
  27. package/assets/skills/oat-project-implement/references/phase-execution.md +16 -8
  28. package/assets/skills/oat-project-import-plan/SKILL.md +7 -4
  29. package/assets/skills/oat-project-lite/SKILL.md +5 -3
  30. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +4 -4
  31. package/assets/skills/oat-project-plan/SKILL.md +7 -4
  32. package/assets/skills/oat-project-plan-writing/SKILL.md +28 -16
  33. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +4 -4
  34. package/assets/skills/oat-project-quick-start/SKILL.md +7 -4
  35. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +4 -4
  36. package/assets/skills/oat-project-review-provide/SKILL.md +7 -4
  37. package/assets/skills/subagent-orchestration/SKILL.md +1 -1
  38. package/assets/skills/subagent-orchestration/references/evidence-and-refresh.md +30 -29
  39. package/assets/skills/subagent-orchestration/references/provider-claude.md +41 -36
  40. package/assets/skills/subagent-orchestration/references/provider-codex.md +30 -35
  41. package/assets/skills/subagent-orchestration/references/provider-cursor.md +46 -38
  42. package/assets/templates/plan.md +5 -5
  43. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  44. package/dist/commands/gate/review-verdict.js +23 -1
  45. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  46. package/dist/commands/project/dispatch/record.js +27 -5
  47. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  48. package/dist/commands/project/dispatch-ceiling/index.js +112 -35
  49. package/dist/config/dispatch-matrix.d.ts.map +1 -1
  50. package/dist/config/dispatch-matrix.js +13 -0
  51. package/dist/providers/ceiling/registry.d.ts +7 -2
  52. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  53. package/dist/providers/ceiling/registry.js +35 -11
  54. package/dist/providers/claude/codec/materialize.d.ts +33 -0
  55. package/dist/providers/claude/codec/materialize.d.ts.map +1 -0
  56. package/dist/providers/claude/codec/materialize.js +149 -0
  57. package/dist/providers/claude/codec/sync-extension.d.ts +24 -0
  58. package/dist/providers/claude/codec/sync-extension.d.ts.map +1 -0
  59. package/dist/providers/claude/codec/sync-extension.js +312 -0
  60. package/dist/providers/claude/dispatch-envelope.d.ts +34 -0
  61. package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -0
  62. package/dist/providers/claude/dispatch-envelope.js +233 -0
  63. package/dist/providers/claude/targets.d.ts +48 -0
  64. package/dist/providers/claude/targets.d.ts.map +1 -0
  65. package/dist/providers/claude/targets.js +228 -0
  66. package/dist/providers/codex/codec/shared.d.ts +6 -0
  67. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  68. package/dist/providers/codex/codec/shared.js +8 -0
  69. package/dist/providers/cursor/codec/catalog.d.ts.map +1 -1
  70. package/dist/providers/cursor/codec/catalog.js +47 -45
  71. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +35 -0
  72. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -1
  73. package/dist/providers/identity/claude-runtime-observation.fixtures.js +225 -12
  74. package/dist/providers/shared/registry.d.ts.map +1 -1
  75. package/dist/providers/shared/registry.js +5 -2
  76. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { validateClaudeDispatchTarget } from '../providers/claude/targets.js';
1
2
  export const VALID_DISPATCH_MATRIX_TIERS = ['economy', 'balanced', 'high', 'frontier'];
2
3
  const VALID_CODEX_PROVIDER_SCALARS = new Set([
3
4
  'low',
@@ -205,6 +206,18 @@ export function isCodexMaterializedRouteTarget(provider, target) {
205
206
  return (target.harness ?? provider) === 'codex';
206
207
  }
207
208
  export function validateDispatchRouteTarget(provider, target) {
209
+ if ((target.harness ?? provider) === 'claude') {
210
+ if (!target.model) {
211
+ return {
212
+ valid: false,
213
+ reason: 'Claude dispatch targets must provide a model.',
214
+ };
215
+ }
216
+ return validateClaudeDispatchTarget({
217
+ model: target.model,
218
+ ...(target.effort ? { effort: target.effort } : {}),
219
+ });
220
+ }
208
221
  if (!isCodexMaterializedRouteTarget(provider, target)) {
209
222
  return { valid: true };
210
223
  }
@@ -1,3 +1,4 @@
1
+ import { type ClaudeCapabilityEvidence } from '../claude/targets.js';
1
2
  /**
2
3
  * Provider ceiling adapter registry.
3
4
  *
@@ -9,8 +10,9 @@
9
10
  * to produce concrete dispatch args — skills never re-implement this logic.
10
11
  *
11
12
  * Codex enforces via sync-time materialized role variants selected from matrix
12
- * model+effort targets. Claude enforces via the per-call Task `model` argument
13
- * (no variant files). Every other provider is advisory by default.
13
+ * model+effort targets. Claude effort routes use materialized model-and-effort
14
+ * variants, while legacy model-only routes keep the per-call Task `model`
15
+ * argument. Every other provider is advisory by default.
14
16
  */
15
17
  export type EnforcementMechanism = 'pinned-variant' | 'model-arg' | 'none';
16
18
  export type ProviderSelectionAxis = 'model' | 'model-effort' | 'tier' | 'none';
@@ -18,9 +20,12 @@ export type CeilingRole = 'implementer' | 'reviewer';
18
20
  export interface CeilingCompileContext {
19
21
  /** The orchestrator's own tier, used to detect above-orchestrator upgrades. */
20
22
  orchestratorTier?: string;
23
+ /** Provider environment used by the resolver's capability derivation. */
24
+ env?: NodeJS.ProcessEnv;
21
25
  target?: {
22
26
  model?: string;
23
27
  effort?: string;
28
+ capabilityEvidence?: ClaudeCapabilityEvidence;
24
29
  } | null;
25
30
  }
26
31
  export type CeilingDispatchArgs = {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/providers/ceiling/registry.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAC3E,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE/E,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;CACV;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjB,IAAI,CAAC;AAKT,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,oBAAoB,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC;;;OAGG;IACH,qBAAqB,CACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,qBAAqB,GACzB,mBAAmB,CAAC;IACvB;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC;CACtE;AAMD,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAK9C,CAAC;AA6HF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB,CAM1E"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/providers/ceiling/registry.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,wBAAwB,EAE9B,MAAM,2BAA2B,CAAC;AAQnC;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAC3E,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE/E,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;KAC/C,GAAG,IAAI,CAAC;CACV;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjB,IAAI,CAAC;AAKT,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,oBAAoB,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC;;;OAGG;IACH,qBAAqB,CACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,qBAAqB,GACzB,mBAAmB,CAAC;IACvB;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC;CACtE;AAMD,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAA4B,CAAC;AAkK5E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB,CAM1E"}
@@ -1,6 +1,7 @@
1
1
  import { VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, } from '../../config/oat-config.js';
2
2
  import { getOwnKey } from '../../config/own-keys.js';
3
- import { buildCodexMaterializedTargetRoleName } from '../codex/codec/shared.js';
3
+ import { buildClaudeEffortVariantName, CLAUDE_MODEL_ORDER, validateClaudeDispatchCapability, } from '../claude/targets.js';
4
+ import { buildCodexMaterializedTargetRoleName, SUPPORTED_CODEX_ROLE_TARGETS, } from '../codex/codec/shared.js';
4
5
  import { findCursorModelPinMapping } from '../cursor/codec/catalog.js';
5
6
  import { buildCursorMaterializedRoleName } from '../cursor/codec/shared.js';
6
7
  const DIRECT_DISPATCH_ROLE_PATTERN = /^oat-(?:phase-implementer|reviewer)(?:-|$)/;
@@ -11,12 +12,7 @@ export function isDirectDispatchRoleName(value) {
11
12
  const CODEX_IMPLEMENTER_ROLE = 'oat-phase-implementer';
12
13
  const CODEX_REVIEWER_ROLE = 'oat-reviewer';
13
14
  /** Claude tier order, low → high, for above-orchestrator comparison. */
14
- export const CLAUDE_TIER_ORDER = [
15
- 'haiku',
16
- 'sonnet',
17
- 'opus',
18
- 'fable',
19
- ];
15
+ export const CLAUDE_TIER_ORDER = [...CLAUDE_MODEL_ORDER];
20
16
  const codexAdapter = {
21
17
  provider: 'codex',
22
18
  supportsCeiling: true,
@@ -24,7 +20,10 @@ const codexAdapter = {
24
20
  mechanism: 'pinned-variant',
25
21
  selectionAxis: 'model-effort',
26
22
  compileToDispatchArgs(value, role, ctx) {
27
- if (!VALID_CODEX_DISPATCH_CEILINGS.includes(value)) {
23
+ if (!VALID_CODEX_DISPATCH_CEILINGS.includes(value) &&
24
+ !SUPPORTED_CODEX_ROLE_TARGETS.some((candidate) => candidate.model === ctx.target?.model &&
25
+ candidate.effort === value &&
26
+ ctx.target?.effort === value)) {
28
27
  return null;
29
28
  }
30
29
  const target = ctx.target;
@@ -65,12 +64,37 @@ const claudeAdapter = {
65
64
  validValues: [...VALID_CLAUDE_DISPATCH_CEILINGS],
66
65
  mechanism: 'model-arg',
67
66
  selectionAxis: 'tier',
68
- compileToDispatchArgs(value) {
69
- if (isDirectDispatchRoleName(value) ||
67
+ compileToDispatchArgs(value, role, ctx) {
68
+ if (isDirectDispatchRoleName(value))
69
+ return null;
70
+ const target = ctx.target;
71
+ if (target?.effort) {
72
+ const model = target.model ?? value;
73
+ if (!validateClaudeDispatchCapability({
74
+ model,
75
+ effort: target.effort,
76
+ ...(target.capabilityEvidence
77
+ ? { capabilityEvidence: target.capabilityEvidence }
78
+ : {}),
79
+ }, ctx.env).valid) {
80
+ return null;
81
+ }
82
+ return {
83
+ variant: buildClaudeEffortVariantName({
84
+ agentName: role === 'reviewer' ? CODEX_REVIEWER_ROLE : CODEX_IMPLEMENTER_ROLE,
85
+ model,
86
+ effort: target.effort,
87
+ }),
88
+ };
89
+ }
90
+ const model = target?.model ?? value;
91
+ if (!target?.model &&
70
92
  !VALID_CLAUDE_DISPATCH_CEILINGS.includes(value)) {
71
93
  return null;
72
94
  }
73
- return { model: value };
95
+ if (!validateClaudeDispatchCapability({ model }).valid)
96
+ return null;
97
+ return { model };
74
98
  },
75
99
  // Verify only when the request is above the orchestrator tier (upgrade path).
76
100
  verifyOnDispatch(value, ctx) {
@@ -0,0 +1,33 @@
1
+ import type { CanonicalAgentDocument } from '../../../agents/canonical/index.js';
2
+ import { type ClaudeCapabilityEvidence } from '../targets.js';
3
+ export type ClaudeRoleOwner = 'user-config' | 'project-config';
4
+ export interface ClaudeMaterializationTarget {
5
+ model: string;
6
+ effort: string;
7
+ capabilityEvidence?: ClaudeCapabilityEvidence;
8
+ owner: ClaudeRoleOwner;
9
+ }
10
+ export interface ClaudeMaterializedAgent {
11
+ roleName: string;
12
+ fileName: string;
13
+ content: string;
14
+ owner: ClaudeRoleOwner;
15
+ target: ClaudeMaterializationTarget;
16
+ }
17
+ export declare function readOatManagedClaudeRole(content: string): {
18
+ roleName: string;
19
+ owner: ClaudeRoleOwner;
20
+ capabilityEvidence?: ClaudeCapabilityEvidence;
21
+ } | null;
22
+ export declare function materializeClaudeAgent(options: {
23
+ agent: CanonicalAgentDocument;
24
+ target: ClaudeMaterializationTarget;
25
+ env?: NodeJS.ProcessEnv;
26
+ }): ClaudeMaterializedAgent;
27
+ export declare function materializeClaudeAgents(options: {
28
+ agents: CanonicalAgentDocument[];
29
+ targets: ClaudeMaterializationTarget[];
30
+ env?: NodeJS.ProcessEnv;
31
+ }): ClaudeMaterializedAgent[];
32
+ export declare function assertNoUnmanagedClaudeAgentCollisions(scopeRoot: string, desiredNames: Iterable<string>): Promise<void>;
33
+ //# sourceMappingURL=materialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../../../src/providers/claude/codec/materialize.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAIhE,OAAO,EAGL,KAAK,wBAAwB,EAE9B,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAE/D,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;IAC9C,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,2BAA2B,CAAC;CACrC;AAqCD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,eAAe,CAAC;IACvB,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;CAC/C,GAAG,IAAI,CA8BP;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAC9C,KAAK,EAAE,sBAAsB,CAAC;IAC9B,MAAM,EAAE,2BAA2B,CAAC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,uBAAuB,CAmC1B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACjC,OAAO,EAAE,2BAA2B,EAAE,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,uBAAuB,EAAE,CAkB5B;AAWD,wBAAsB,sCAAsC,CAC1D,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -0,0 +1,149 @@
1
+ import { lstat, readFile, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CliError } from '../../../errors/index.js';
4
+ import YAML from 'yaml';
5
+ import { buildClaudeEffortVariantName, normalizeClaudeRoleName, validateClaudeDispatchCapability, } from '../targets.js';
6
+ const DISCOVERY_DIRECTORIES = [
7
+ { path: '.claude/agents', extension: '.md' },
8
+ { path: '.cursor/agents', extension: '.md' },
9
+ { path: '.codex/agents', extension: '.toml' },
10
+ ];
11
+ function managedComments(roleName, owner, capabilityEvidence) {
12
+ return [
13
+ '# oat-managed: true',
14
+ `# oat-role: ${roleName}`,
15
+ `# oat-owner: ${owner}`,
16
+ '# oat-provider: claude',
17
+ `# oat-capability-evidence: ${JSON.stringify(capabilityEvidence)}`,
18
+ ];
19
+ }
20
+ function frontmatter(content) {
21
+ const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
22
+ if (!match?.[1])
23
+ return null;
24
+ try {
25
+ const value = YAML.parse(match[1]);
26
+ return value && typeof value === 'object' && !Array.isArray(value)
27
+ ? { yaml: match[1], value: value }
28
+ : null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export function readOatManagedClaudeRole(content) {
35
+ const parsed = frontmatter(content);
36
+ if (!parsed)
37
+ return null;
38
+ const managed = parsed.yaml.match(/^# oat-managed: true$/gm)?.length === 1;
39
+ const provider = parsed.yaml.match(/^# oat-provider: claude$/gm)?.length === 1;
40
+ const role = /^# oat-role: ([a-z0-9]+(?:-[a-z0-9]+)*)$/m.exec(parsed.yaml)?.[1];
41
+ const owner = /^# oat-owner: (user-config|project-config)$/m.exec(parsed.yaml)?.[1];
42
+ const evidenceJson = /^# oat-capability-evidence: (\{.+\})$/m.exec(parsed.yaml)?.[1];
43
+ let capabilityEvidence;
44
+ if (evidenceJson) {
45
+ try {
46
+ capabilityEvidence = JSON.parse(evidenceJson);
47
+ }
48
+ catch {
49
+ // Malformed evidence makes the role stale but does not hide its ownership.
50
+ }
51
+ }
52
+ return managed && provider && role && owner
53
+ ? {
54
+ roleName: role,
55
+ owner,
56
+ ...(capabilityEvidence ? { capabilityEvidence } : {}),
57
+ }
58
+ : null;
59
+ }
60
+ export function materializeClaudeAgent(options) {
61
+ const validation = validateClaudeDispatchCapability(options.target, options.env);
62
+ if (!validation.valid)
63
+ throw new CliError(validation.reason ?? 'Invalid Claude target.');
64
+ const capabilityEvidence = validation.capabilityEvidence;
65
+ if (!capabilityEvidence) {
66
+ throw new CliError('Claude effort target has no validated capability evidence.');
67
+ }
68
+ const roleName = buildClaudeEffortVariantName({
69
+ agentName: options.agent.name,
70
+ model: options.target.model,
71
+ effort: options.target.effort,
72
+ });
73
+ const description = `${options.agent.description} Claude-native ${options.target.model}/${options.target.effort} effort variant managed by OAT.`;
74
+ const rendered = YAML.stringify({
75
+ name: roleName,
76
+ description,
77
+ model: options.target.model,
78
+ effort: options.target.effort,
79
+ ...(options.agent.tools !== undefined
80
+ ? { tools: options.agent.tools }
81
+ : {}),
82
+ }).trimEnd();
83
+ return {
84
+ roleName,
85
+ fileName: `${roleName}.md`,
86
+ content: `---\n${managedComments(roleName, options.target.owner, capabilityEvidence).join('\n')}\n${rendered}\n---\n${options.agent.body}`,
87
+ owner: options.target.owner,
88
+ target: { ...options.target, capabilityEvidence },
89
+ };
90
+ }
91
+ export function materializeClaudeAgents(options) {
92
+ const roles = options.agents.flatMap((agent) => options.targets.map((target) => materializeClaudeAgent({ agent, target, env: options.env })));
93
+ const names = new Set();
94
+ for (const role of roles) {
95
+ if (names.has(role.roleName)) {
96
+ throw new CliError(`Distinct Claude targets produced the same normalized role name ${role.roleName}. Refusing ambiguous role writes.`);
97
+ }
98
+ names.add(role.roleName);
99
+ }
100
+ return roles.sort((left, right) => left.roleName.localeCompare(right.roleName));
101
+ }
102
+ async function pathStats(path) {
103
+ try {
104
+ return await lstat(path);
105
+ }
106
+ catch (error) {
107
+ if (error.code === 'ENOENT')
108
+ return null;
109
+ throw error;
110
+ }
111
+ }
112
+ export async function assertNoUnmanagedClaudeAgentCollisions(scopeRoot, desiredNames) {
113
+ const desired = new Set(desiredNames);
114
+ for (const { path: directory, extension } of DISCOVERY_DIRECTORIES) {
115
+ const absoluteDirectory = join(scopeRoot, directory);
116
+ const stats = await pathStats(absoluteDirectory);
117
+ if (!stats)
118
+ continue;
119
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
120
+ throw new CliError(`Claude agent discovery path is unsafe: ${directory}.`);
121
+ }
122
+ for (const fileName of await readdir(absoluteDirectory)) {
123
+ if (!fileName.endsWith(extension))
124
+ continue;
125
+ const path = join(absoluteDirectory, fileName);
126
+ const fileStats = await pathStats(path);
127
+ if (!fileStats?.isFile() || fileStats.isSymbolicLink()) {
128
+ if (desired.has(normalizeClaudeRoleName(fileName.slice(0, -extension.length)))) {
129
+ throw new CliError(`Claude role collision at ${directory}/${fileName}: symbolic links are not writable targets.`);
130
+ }
131
+ continue;
132
+ }
133
+ const content = await readFile(path, 'utf8');
134
+ const parsed = extension === '.md' ? frontmatter(content) : null;
135
+ const declared = typeof parsed?.value.name === 'string'
136
+ ? normalizeClaudeRoleName(parsed.value.name)
137
+ : null;
138
+ const fileRole = normalizeClaudeRoleName(fileName.slice(0, -extension.length));
139
+ if (!desired.has(fileRole) && (!declared || !desired.has(declared)))
140
+ continue;
141
+ const managed = readOatManagedClaudeRole(content);
142
+ if (directory !== '.claude/agents' ||
143
+ !managed ||
144
+ !desired.has(managed.roleName)) {
145
+ throw new CliError(`Claude role name collision at ${directory}/${fileName}: existing definition is not the matching OAT-managed Claude variant.`);
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,24 @@
1
+ import { type CanonicalEntry } from '../../../engine/index.js';
2
+ import { type MaterializationApplyResult, type MaterializationContext, type MaterializationExtension, type MaterializationOperation, type MaterializationPlan, type MaterializationWriteOperation } from '../../shared/index.js';
3
+ import { type ClaudeRoleOwner } from './materialize.js';
4
+ export interface ClaudeMaterializationTargetOptions {
5
+ userConfigDir?: string;
6
+ projectPath?: string | null;
7
+ env?: NodeJS.ProcessEnv;
8
+ enabled?: boolean;
9
+ }
10
+ export interface ClaudeExtensionPlan extends MaterializationPlan<'claude', 'role', {
11
+ cleanupOwners: ClaudeRoleOwner[];
12
+ isPartialSync: boolean;
13
+ }> {
14
+ operations: (MaterializationWriteOperation<'claude', 'role'> & {
15
+ roleName?: string;
16
+ })[];
17
+ }
18
+ export declare function computeClaudeProjectExtensionPlan(scopeRoot: string, canonicalEntries: CanonicalEntry[], allowedCanonicalPaths?: string[], options?: ClaudeMaterializationTargetOptions): Promise<ClaudeExtensionPlan>;
19
+ export declare function applyClaudeProjectExtensionPlan(scopeRoot: string, plan: ClaudeExtensionPlan): Promise<MaterializationApplyResult>;
20
+ export declare function hasClaudeExtensionChanges(plan: ClaudeExtensionPlan): boolean;
21
+ export declare function summarizeClaudeExtension(plan: ClaudeExtensionPlan): import("../../shared/index.js").MaterializationPlanSummary;
22
+ export declare function toClaudeExtensionOperations(plan: ClaudeExtensionPlan): MaterializationOperation[];
23
+ export declare const claudeMaterializationExtension: MaterializationExtension<ClaudeExtensionPlan, MaterializationContext<ClaudeMaterializationTargetOptions>>;
24
+ //# sourceMappingURL=sync-extension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-extension.d.ts","sourceRoot":"","sources":["../../../../src/providers/claude/codec/sync-extension.ts"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AAIvB,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,6BAA6B,EACnC,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AAKvB,MAAM,WAAW,kCAAkC;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB,CAC9D,QAAQ,EACR,MAAM,EACN;IAAE,aAAa,EAAE,eAAe,EAAE,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAC7D;IACC,UAAU,EAAE,CAAC,6BAA6B,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG;QAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC,EAAE,CAAC;CACN;AAwLD,wBAAsB,iCAAiC,CACrD,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,OAAO,GAAE,kCAAuC,GAC/C,OAAO,CAAC,mBAAmB,CAAC,CAgH9B;AAyCD,wBAAsB,+BAA+B,CACnD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,0BAA0B,CAAC,CA+BrC;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAE5E;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,mBAAmB,0DAEjE;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,mBAAmB,GACxB,wBAAwB,EAAE,CAE5B;AAED,eAAO,MAAM,8BAA8B,EAAE,wBAAwB,CACnE,mBAAmB,EACnB,sBAAsB,CAAC,kCAAkC,CAAC,CAY3D,CAAC"}
@@ -0,0 +1,312 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { parseCanonicalAgentFile } from '../../../agents/canonical/index.js';
5
+ import { normalizeDispatchMatrix, walkDispatchMatrix, } from '../../../config/dispatch-matrix.js';
6
+ import { normalizeProjectPath, resolveActiveProject } from '../../../config/oat-config.js';
7
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
8
+ import { materializationCanonicalPathAllowed, } from '../../../engine/index.js';
9
+ import { CliError } from '../../../errors/index.js';
10
+ import { ensureDir, fileExists } from '../../../fs/io.js';
11
+ import { validateRealPathWithinScope } from '../../../fs/paths.js';
12
+ import { hasMaterializationChanges, summarizeMaterializationPlan, toMaterializationOperations, } from '../../shared/index.js';
13
+ import YAML from 'yaml';
14
+ import { assertNoUnmanagedClaudeAgentCollisions, materializeClaudeAgents, readOatManagedClaudeRole, } from './materialize.js';
15
+ const SUPPORTED_BASE_ROLES = ['oat-phase-implementer', 'oat-reviewer'];
16
+ function hash(content) {
17
+ return createHash('sha256').update(content).digest('hex');
18
+ }
19
+ function isUserScope(scopeRoot, options) {
20
+ return Boolean(options.userConfigDir &&
21
+ resolve(scopeRoot) === resolve(options.userConfigDir, '..'));
22
+ }
23
+ function collectTargets(value, owner, targets) {
24
+ if (value === undefined || typeof value === 'string')
25
+ return;
26
+ for (const ref of walkDispatchMatrix({ claude: value }, {
27
+ source: owner === 'user-config' ? 'user-config' : 'repo-config',
28
+ pathPrefix: 'workflow.dispatchCeiling.providers',
29
+ })) {
30
+ const target = ref.target;
31
+ if (!target?.model ||
32
+ !target.effort ||
33
+ (target.harness ?? 'claude') !== 'claude') {
34
+ continue;
35
+ }
36
+ targets.set(`${target.model}\0${target.effort}`, {
37
+ model: target.model,
38
+ effort: target.effort,
39
+ owner,
40
+ });
41
+ }
42
+ }
43
+ function frontmatter(content) {
44
+ const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
45
+ if (!match?.[1])
46
+ return null;
47
+ const parsed = YAML.parse(match[1]);
48
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
49
+ ? parsed
50
+ : null;
51
+ }
52
+ async function collectProjectStateTargets(scopeRoot, options, targets) {
53
+ const candidate = options.projectPath === undefined
54
+ ? (await resolveActiveProject(scopeRoot)).path
55
+ : options.projectPath;
56
+ if (!candidate)
57
+ return;
58
+ const normalized = normalizeProjectPath(scopeRoot, candidate);
59
+ if (!normalized)
60
+ return;
61
+ let projectRoot;
62
+ try {
63
+ projectRoot = (await validateRealPathWithinScope(join(scopeRoot, normalized), scopeRoot)).realPath;
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ const statePath = join(projectRoot, 'state.md');
69
+ if (!(await fileExists(statePath)))
70
+ return;
71
+ const parsed = frontmatter(await readFile(statePath, 'utf8'));
72
+ const policy = parsed?.['oat_dispatch_policy'];
73
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy))
74
+ return;
75
+ const matrix = normalizeDispatchMatrix(policy['matrix'], {
76
+ pathPrefix: 'oat_dispatch_policy.matrix',
77
+ compatibilityMode: 'project-state',
78
+ });
79
+ collectTargets(matrix.providers.claude, 'project-config', targets);
80
+ }
81
+ async function readTargets(scopeRoot, options) {
82
+ const config = await resolveEffectiveConfig(scopeRoot, options.userConfigDir ?? join(scopeRoot, '.oat', '__no-user-config__'), options.env);
83
+ const targets = new Map();
84
+ if (isUserScope(scopeRoot, options)) {
85
+ collectTargets(config.user.workflow?.dispatchCeiling?.providers?.claude, 'user-config', targets);
86
+ }
87
+ else {
88
+ collectTargets(config.shared.workflow?.dispatchCeiling?.providers?.claude, 'project-config', targets);
89
+ collectTargets(config.local.workflow?.dispatchCeiling?.providers?.claude, 'project-config', targets);
90
+ await collectProjectStateTargets(scopeRoot, options, targets);
91
+ }
92
+ return [...targets.values()].sort((left, right) => `${left.model}/${left.effort}`.localeCompare(`${right.model}/${right.effort}`));
93
+ }
94
+ async function optionalFile(path) {
95
+ return (await fileExists(path)) ? readFile(path, 'utf8') : null;
96
+ }
97
+ async function staleRoles(scopeRoot, desired, owners, baseRoles) {
98
+ let files = [];
99
+ try {
100
+ files = await readdir(join(scopeRoot, '.claude', 'agents'));
101
+ }
102
+ catch {
103
+ // The managed directory does not exist before the first materialization.
104
+ }
105
+ const stale = [];
106
+ for (const file of files) {
107
+ if (!file.endsWith('.md'))
108
+ continue;
109
+ const roleName = basename(file, '.md');
110
+ if (desired.has(roleName))
111
+ continue;
112
+ const content = await optionalFile(join(scopeRoot, '.claude', 'agents', file));
113
+ const managed = content ? readOatManagedClaudeRole(content) : null;
114
+ const matchesBaseRole = !baseRoles ||
115
+ [...baseRoles].some((baseRole) => roleName.startsWith(`${baseRole}-claude-`));
116
+ if (matchesBaseRole &&
117
+ managed?.roleName === roleName &&
118
+ owners.includes(managed.owner))
119
+ stale.push(roleName);
120
+ }
121
+ return stale.sort();
122
+ }
123
+ function removedSupportedBaseRoles(entries, allowedCanonicalPaths) {
124
+ if (!allowedCanonicalPaths?.length)
125
+ return new Set();
126
+ const present = new Set(entries
127
+ .filter((entry) => entry.type === 'agent' && entry.isFile)
128
+ .map((entry) => entry.name.replace(/\.md$/, '')));
129
+ return new Set(SUPPORTED_BASE_ROLES.filter((role) => allowedCanonicalPaths.includes(`.agents/agents/${role}.md`) &&
130
+ !present.has(role)));
131
+ }
132
+ export async function computeClaudeProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths, options = {}) {
133
+ const partial = Boolean(allowedCanonicalPaths?.length);
134
+ const cleanupOwners = isUserScope(scopeRoot, options)
135
+ ? ['user-config']
136
+ : ['project-config'];
137
+ const empty = () => ({
138
+ provider: 'claude',
139
+ operations: [],
140
+ managedEntries: [],
141
+ aggregateHash: hash(''),
142
+ metadata: { cleanupOwners, isPartialSync: partial },
143
+ });
144
+ if (options.enabled === false)
145
+ return empty();
146
+ const entries = canonicalEntries.filter((entry) => materializationCanonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths));
147
+ const removedBaseRoles = removedSupportedBaseRoles(entries, allowedCanonicalPaths);
148
+ if (partial && entries.length === 0 && removedBaseRoles.size === 0)
149
+ return empty();
150
+ const targets = await readTargets(scopeRoot, options);
151
+ const agents = [];
152
+ for (const entry of entries) {
153
+ if (entry.type !== 'agent' || !entry.isFile || !entry.name.endsWith('.md'))
154
+ continue;
155
+ const agent = await parseCanonicalAgentFile(entry.canonicalPath);
156
+ if (SUPPORTED_BASE_ROLES.includes(agent.name))
157
+ agents.push(agent);
158
+ }
159
+ if (!partial && isUserScope(scopeRoot, options) && targets.length > 0) {
160
+ const present = new Set(agents.map(({ name }) => name));
161
+ const missing = SUPPORTED_BASE_ROLES.filter((name) => !present.has(name));
162
+ if (missing.length > 0) {
163
+ throw new CliError(`Bundled managed Claude role definitions are unavailable for user sync: ${missing.join(', ')}. Refusing stale user-role cleanup.`);
164
+ }
165
+ }
166
+ const roles = materializeClaudeAgents({
167
+ agents,
168
+ targets,
169
+ env: options.env,
170
+ });
171
+ const desired = new Set(roles.map(({ roleName }) => roleName));
172
+ await assertNoUnmanagedClaudeAgentCollisions(scopeRoot, desired);
173
+ const stale = partial
174
+ ? await staleRoles(scopeRoot, desired, cleanupOwners, removedBaseRoles)
175
+ : await staleRoles(scopeRoot, desired, cleanupOwners);
176
+ const operations = [];
177
+ for (const role of roles) {
178
+ const relativePath = `.claude/agents/${role.fileName}`;
179
+ const existing = await optionalFile(join(scopeRoot, relativePath));
180
+ if (existing === null) {
181
+ operations.push({
182
+ provider: 'claude',
183
+ action: 'create',
184
+ target: 'role',
185
+ path: relativePath,
186
+ reason: 'managed Claude effort variant missing',
187
+ entryName: role.roleName,
188
+ roleName: role.roleName,
189
+ content: role.content,
190
+ });
191
+ continue;
192
+ }
193
+ const managed = readOatManagedClaudeRole(existing);
194
+ if (!managed ||
195
+ managed.roleName !== role.roleName ||
196
+ !cleanupOwners.includes(managed.owner)) {
197
+ throw new CliError(`Refusing to update Claude role ${role.roleName}: existing file is unmanaged or has incompatible ownership.`);
198
+ }
199
+ const current = existing.trimEnd() === role.content.trimEnd();
200
+ operations.push({
201
+ provider: 'claude',
202
+ action: current ? 'skip' : 'update',
203
+ target: 'role',
204
+ path: relativePath,
205
+ reason: current
206
+ ? 'managed Claude effort variant already in sync'
207
+ : 'managed Claude effort variant differs from desired state',
208
+ entryName: role.roleName,
209
+ roleName: role.roleName,
210
+ ...(current ? {} : { content: role.content }),
211
+ });
212
+ }
213
+ for (const roleName of stale) {
214
+ operations.push({
215
+ provider: 'claude',
216
+ action: 'remove',
217
+ target: 'role',
218
+ path: `.claude/agents/${roleName}.md`,
219
+ reason: 'stale managed Claude effort variant removed',
220
+ entryName: roleName,
221
+ roleName,
222
+ });
223
+ }
224
+ return {
225
+ provider: 'claude',
226
+ operations,
227
+ managedEntries: [...desired, ...stale],
228
+ aggregateHash: hash(roles.map(({ content }) => content).join('\0')),
229
+ metadata: { cleanupOwners, isPartialSync: partial },
230
+ };
231
+ }
232
+ async function stats(path) {
233
+ try {
234
+ return await lstat(path);
235
+ }
236
+ catch (error) {
237
+ if (error.code === 'ENOENT')
238
+ return null;
239
+ throw error;
240
+ }
241
+ }
242
+ async function assertSafePath(scopeRoot, path) {
243
+ const root = resolve(scopeRoot);
244
+ const rel = relative(root, path);
245
+ if (rel === '..' ||
246
+ rel.startsWith(`..${sep}`) ||
247
+ resolve(root, rel) !== path) {
248
+ throw new CliError(`Claude materialization path escapes the sync scope: ${path}`);
249
+ }
250
+ let current = root;
251
+ for (const part of rel.replaceAll('\\', '/').split('/').slice(0, -1)) {
252
+ current = join(current, part);
253
+ const value = await stats(current);
254
+ if (!value)
255
+ break;
256
+ if (value.isSymbolicLink() || !value.isDirectory()) {
257
+ throw new CliError(`Claude materialization parent is unsafe: ${relative(root, current)}`);
258
+ }
259
+ }
260
+ if ((await stats(path))?.isSymbolicLink()) {
261
+ throw new CliError(`Claude materialization target is a symbolic link: ${rel}`);
262
+ }
263
+ }
264
+ export async function applyClaudeProjectExtensionPlan(scopeRoot, plan) {
265
+ const operations = [];
266
+ for (const operation of plan.operations) {
267
+ if (operation.action === 'skip') {
268
+ operations.push({ ...operation, status: 'current' });
269
+ continue;
270
+ }
271
+ const path = resolve(scopeRoot, operation.path);
272
+ try {
273
+ await assertSafePath(scopeRoot, path);
274
+ if (operation.action === 'remove')
275
+ await rm(path, { force: true });
276
+ else {
277
+ await ensureDir(dirname(path));
278
+ await writeFile(path, operation.content ?? '', 'utf8');
279
+ }
280
+ operations.push({ ...operation, status: 'changed' });
281
+ }
282
+ catch {
283
+ operations.push({
284
+ ...operation,
285
+ status: 'failed',
286
+ failure: 'Materialization failed; inspect local verbose diagnostics and retry sync.',
287
+ });
288
+ }
289
+ }
290
+ return {
291
+ applied: operations.filter(({ status }) => status === 'changed').length,
292
+ failed: operations.filter(({ status }) => status === 'failed').length,
293
+ skipped: operations.filter(({ status }) => status === 'current').length,
294
+ operations,
295
+ };
296
+ }
297
+ export function hasClaudeExtensionChanges(plan) {
298
+ return hasMaterializationChanges(plan);
299
+ }
300
+ export function summarizeClaudeExtension(plan) {
301
+ return summarizeMaterializationPlan(plan);
302
+ }
303
+ export function toClaudeExtensionOperations(plan) {
304
+ return toMaterializationOperations(plan);
305
+ }
306
+ export const claudeMaterializationExtension = {
307
+ provider: 'claude',
308
+ computePlan(context) {
309
+ return computeClaudeProjectExtensionPlan(context.scopeRoot, context.canonicalEntries, context.allowedCanonicalPaths, context.options);
310
+ },
311
+ applyPlan: applyClaudeProjectExtensionPlan,
312
+ };